本地数据库IndexedDB - 学员管理系统之登录(一)
IndexedDB是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB允许存储大量数据,提供查找接口,还能建立索引。这些都是LocalStorage或Cookie不具备的。就数据库类型而言,IndexedDB不属于关系型数据库(不支持SQL查询语句),更接近NoSQL数据库。
IndexedDB具有以下特点:
- 键值对存储:IndexedDB内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括JS对象。对象仓库中,数据以”键值对“的形式保存,每一个数据记录都对应的主键,主键是独一无二的,不能有重复,否则会抛出错误。
- 异步:IndexedDB操作时不会锁死浏览器,用户依然可以进行其他操作,这与LocalStorage形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。
- 支持事务:IndexedDB支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
- 同源限制:IndexedDB受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。
- 存储空间大:IndexedDB的存储空间比LocalStore大很多,一般来说不少于250MB,甚至没有上限。
- 支持二进制存储:IndexedDB不仅可以存储字符串,还可以存储二进制数据(ArrayBuffer对象和Blob对象)。
通过上篇讲解,大家已经熟悉了IndexedDB的一些基本用法。这里将使用IndexedDB写个简单的”学员管理系统(本地版)“,如对IndexedDB基础还不了解的,请翻看上一篇,地址如下:
本地数据库IndexedDB - 初学者(一)_觉醒法师的博客-CSDN博客
使用的技术框架是Vue.js + element-ui + vuex + axios + sass + indexedDB,系统栏目分类有:管理员列表、年级管理、班级管理、教师列表和学员列表。
如下图所示:
一、搭建项目
Vue项目的搭建这里也不再阐述,如有对vue全家桶不了解的同学,可以先阅读以下几篇文章:
Vue.js快速入门之一:安装和配置_觉醒法师的博客-CSDN博客_vue.js配置
Vue.js快速入门之二:使用状态管理工具Vuex_觉醒法师的博客-CSDN博客
Vue.js快速入门之三:Vue-Router路由_觉醒法师的博客-CSDN博客
Vue.js快速入门之四:axios安装和使用_觉醒法师的博客-CSDN博客_axios怎么安装
二、数据库创建
Vue项目搭建好后,需要在src目录下创建db目录,用来存储操作IndexedDB数据库文件。比如数据库操作文件、对应表封装类等。
2.1 定义表
虽然IndexedDB不属性于“关系性数据库”,这里还是按“关系性数据库”进行表的定义和关联。
系统管理员表:
字段名 | 类型 | 描述 |
---|---|---|
id | int | ID |
name | varchar | 管理员账号 |
password | varchar | 密码 |
phone | varchar | 手机号 |
createtime | datetime | 创建日期 |
updatetime | datetime | 更新日期 |
年级表:
字段名 | 类型 | 描述 |
---|---|---|
id | int | ID |
name | varchar | 年级名称 |
createtime | datetime | 创建日期 |
updatetime | datetime | 更新日期 |
班级表:
字段名 | 类型 | 描述 |
---|---|---|
id | int | ID |
name | varchar | 班级名称 |
mid | varchar | 班主任 |
gid | int | 关联年级 |
ts | Array | 关联授权教师 |
createtime | datetime | 创建日期 |
updatetime | datetime | 更新日期 |
教师表:
字段名 | 类型 | 描述 |
---|---|---|
id | int | ID |
name | varchar | 教师姓名 |
registration | varchar | 户籍 |
address | varchar | 现居住地 |
phone | varchar | 手机号 |
cids | char | 关联班级ID,通过逗号分隔 |
birthday | datetime | 出生年月日 |
createtime | datetime | 创建日期 |
updatetime | datetime | 更新日期 |
学员表:
字段名 | 类型 | 描述 |
---|---|---|
id | int | ID |
name | varchar | 学员姓名 |
registration | varchar | 户籍 |
address | varchar | 现居住地 |
phone | varchar | 手机号 |
cid | int | 关联班级ID |
birthday | datetime | 出生年月日 |
createtime | datetime | 创建日期 |
updatetime | datetime | 更新日期 |
2.2 MD5加密
由于IndexedDB是浏览器中数据库,当项目运行后可直接查看数据库中的数据,这时相关私密性的内容就需要通过加密进行处理了。所以在创建数据库表前,我们先在src目录下创建utils工具类目录,添加md5.js文件,用来给登录密码进行加密处理。
md5.js代码如下:
/** Configurable variables. You may need to tweak these to be compatible with* the server-side, but the defaults work in most cases.*/
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode *//** These are the functions you'll usually want to call* They take string arguments and return either hex or base-64 encoded strings*/
function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }/** Perform a simple self-test to see if the VM is working*/
function md5_vm_test()
{return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
}/** Calculate the MD5 of an array of little-endian words, and a bit length*/
function core_md5(x, len)
{/* append padding */x[len >> 5] |= 0x80 << ((len) % 32);x[(((len + 64) >>> 9) << 4) + 14] = len;var a = 1732584193;var b = -271733879;var c = -1732584194;var d = 271733878;for(var i = 0; i < x.length; i += 16){var olda = a;var oldb = b;var oldc = c;var oldd = d;a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);c = md5_ff(c, d, a, b, x[i+10], 17, -42063);b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);a = safe_add(a, olda);b = safe_add(b, oldb);c = safe_add(c, oldc);d = safe_add(d, oldd);}return Array(a, b, c, d);}/** These functions implement the four basic operations the algorithm uses.*/
function md5_cmn(q, a, b, x, s, t)
{return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}/** Calculate the HMAC-MD5, of a key and some data*/
function core_hmac_md5(key, data)
{var bkey = str2binl(key);if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz);var ipad = Array(16), opad = Array(16);for(var i = 0; i < 16; i++){ipad[i] = bkey[i] ^ 0x36363636;opad[i] = bkey[i] ^ 0x5C5C5C5C;}var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);return core_md5(opad.concat(hash), 512 + 128);
}/** Add integers, wrapping at 2^32. This uses 16-bit operations internally* to work around bugs in some JS interpreters.*/
function safe_add(x, y)
{var lsw = (x & 0xFFFF) + (y & 0xFFFF);var msw = (x >> 16) + (y >> 16) + (lsw >> 16);return (msw << 16) | (lsw & 0xFFFF);
}/** Bitwise rotate a 32-bit number to the left.*/
function bit_rol(num, cnt)
{return (num << cnt) | (num >>> (32 - cnt));
}/** Convert a string to an array of little-endian words* If chrsz is ASCII, characters >255 have their hi-byte silently ignored.*/
function str2binl(str)
{var bin = Array();var mask = (1 << chrsz) - 1;for(var i = 0; i < str.length * chrsz; i += chrsz)bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);return bin;
}/** Convert an array of little-endian words to a string*/
function binl2str(bin)
{var str = "";var mask = (1 << chrsz) - 1;for(var i = 0; i < bin.length * 32; i += chrsz)str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);return str;
}/** Convert an array of little-endian words to a hex string.*/
function binl2hex(binarray)
{var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";var str = "";for(var i = 0; i < binarray.length * 4; i++){str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF);}return str;
}/** Convert an array of little-endian words to a base-64 string*/
function binl2b64(binarray)
{var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";var str = "";for(var i = 0; i < binarray.length * 4; i += 3){var triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16)| (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 )| ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);for(var j = 0; j < 4; j++){if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);}}return str;
}export {hex_md5,b64_md5,str_md5
}
这里除了直接引用MD5文件外,也可以通过npm或yarn等一些包管理工具,使用命令进行安装,这部分自行选择适合自己项目的方法即可。
2.2 打开数据库
以上将所有表结构定义好后,咱们可以封装操作数据库文件了。在src目录下创建db目录,然后新建index.js文件,用来定义数据库打开及表结构创建等等。
在index.js文件中,我们封装一个DBIsLoadSuccess函数,用来判断IndexedDB是否打开成功,因为open请求不会立即打开数据库或者开始一个事务,而是在onsuccess执行时;并且结合Promise,来实现此功能。具体代码如下:
let Database;
let Request = window.indexedDB.open("TestDatabase", 2);
let isError = false;// 数据库打开失败
Request.onerror = function(event) {isError = true;throw "Why didn't you allow my web app to use IndexedDB?!";
};// 数据库打开成功
Request.onsuccess = function(event) {isError = false;Database = event.target.result;
};let handle = null;/*** 数据库增删改查 封装*/
export const DBIsLoadSuccess = () => {return new Promise((resolve, reject) => {//如果数据库打开成功,直接回调resolve函数,并返回实例对象if(Database){resolve(Database);}//如果数据库还未打开,添加定时器 进行查询,打开成功后,else{handle = setInterval(() => {if(Database){clearInterval(handle);resolve(Database);}//如果数据库打开失败,执行reject函数else if(isError){clearInterval(handle);reject();}}, 200);}});
}
2.3 表结构创建
在onupgradeneeded事件中,允许你在处理函数中更新数据库模式;所以我们在onupgradeneeded执行时,来创建对应的表及表结构。
在管理员表创建时,需要默认添加一个管理员账号,并且通过hex_md5对密码进行处理后再存储到用户表中。创建管理员账号时,无须添加accesstoken键值对,因为此时用户还未登录,无token信息;如默认添加空值,则索引accesstoken中则会有相应关联值;这里accesstoken索引是用来判断用户是否登录的,是唯一的,只有在登录状态下才会赋值。
在index.js文件中增加以下代码:
import { hex_md5 } from '@/utils/md5'//数据库表名
let usersName = "users"; //用户表
let gradeName = "grade"; //年级表
let classifyName = "classify"; //班级表
let studentName = "student"; //学生表
let teacherName = "teacher"; //老师表
let buildingName = "building"; //楼栋// 执行success前执行函数
Request.onupgradeneeded = function(e){let db = e.target.result;//用户表if(!db.objectStoreNames.contains(usersName)){let store = db.createObjectStore(usersName, {keyPath: "id", autoIncrement: true});//创建索引 - 用户名store.createIndex('name', 'name', {unique: true});//创建索引 - 登录tokenstore.createIndex('accesstoken', 'accesstoken', {unique: true});//添加默认账号store.add({name: "admin",password: hex_md5('123456'),phone: "13233332222",cratetime: new Date().getTime(),updatetime: new Date().getTime()});}//年级表if(!db.objectStoreNames.contains(gradeName)){let store = db.createObjectStore(gradeName, {keyPath: "id", autoIncrement: true});//创建索引 - 年级名称(年级为唯一,故不能重复)store.createIndex('name', 'name', {unique: true});}//班级表if(!db.objectStoreNames.contains(classifyName)){let store = db.createObjectStore(classifyName, {keyPath: "id", autoIncrement: true});//创建索引 - 班级名称store.createIndex('name', 'name', {unique: false});//创建索引 - 年级名称store.createIndex('grade', 'grade', {unique: true});}//学生表if(!db.objectStoreNames.contains(studentName)){let store = db.createObjectStore(studentName, {keyPath: "id", autoIncrement: true});//创建索引 - 学生姓名store.createIndex('name', 'name', {unique: false});}//老师表if(!db.objectStoreNames.contains(teacherName)){let store = db.createObjectStore(teacherName, {keyPath: "id", autoIncrement: true});//创建索引 - 老师姓名store.createIndex('name', 'name', {unique: false});}//楼栋表if(!db.objectStoreNames.contains(buildingName)){let store = db.createObjectStore(buildingName, {keyPath: "id", autoIncrement: true});//创建索引 - 楼栋名称store.createIndex('name', 'name', {unique: true, multiEntry: true});}//if end
}
注意:onupgradeneeded只会在第一次打开数据库时触发,后期想要触onupgradeneeded监听事件,则需要修改open中第二位参数的版本号。
我们在项目中引用index.js文件并执行,此时我们F12显示控制台,切换到“Application”,在IndexedDB中,则可以看到TestDatabase数据库了,以及创建相应的表及索引。
如下图:
三、项目结构
在项目中,如下图在src目录下,创建相应的项目文件。
在router以上部分,会在后面会详细讲解,这里我们先讲下如果定义路由和状态管理仓库。
3.1 vue-ls
Vue-ls用来控制数据存储在localStorage或者sessionStorage中,并且可以控制其存储时效性。
安装:
npm install vue-ls --save
main.js中引入:
import Storage from 'vue-ls'Vue.use(Storage, {namespace: 'system_',name: 'ls',storeage: 'local'
});
基本用法如下:
//存储数据
Vue.ls.set('foo', 'boo', 60 * 60 * 1000); //缓存1小时
Vue.ls.get('foo');
Vue.ls.get('boo', 10); //如果没有获取到boo数据,默认返回10Vue.ls.on('foo', callback) //监听foo值变化,触发Callback回调函数
Vue.ls.off('foo', callback) //卸载foo监听事件//移除foo对应缓存数据
Vue.ls.remove('foo');
3.2 store状态管理
Vuex是一个专为Vue.js应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件状态,并在相应的规则保证状态以一种可预测的方式发生变化。
什么情况下会使用到Vuex:比如登录的用户信息,接口的统一访问令牌等,会频繁使用和全站通用的数据,可以寄存在状态管理器中。
现在在store目录创建相应文件:
3.2.1 创建state.js文件
代码如下:
/*** 状态,变量库*/
const state = {/*** 访问令牌*/token: "",/*** 用户信息*/userInfo: null
}export default state;
3.2.2 创建mutationsType.js文件
代码如下:
/*** 用户信息*/
export const USERINFO = "USERINFO";/*** 访问令牌*/
export const TOKEN = "TOKEN";
3.2.3 创建mutations.js文件
代码如下:
import { USERINFO, TOKEN } from './mutationsType'const mutations = {/*** 修改访问令牌信息*/[TOKEN](state, param){state.token = param;},/*** 修改用户信息*/[USERINFO](state, param){state.userInfo = param;}
}
export default mutations;
3.2.4 创建getters.js文件
代码如下:
const getters = {/*** 用户信息*/userInfo(state){return state.userInfo;},/*** 访问令牌*/accessToken(state){return state.token;}
}
export default getters;
3.2.5 创建actions.js文件
代码如下:
import Vue from 'vue'
import { USERINFO, TOKEN } from './mutationsType'/*** 业务层*/
const actions = {/*** 检查是否登录*/checkIsLogin(){let token = Vue.ls.get(TOKEN);return new Promise((resolve, reject) => {if(token){resolve();}else{reject();}});},/*** 保存登录信息*/saveLoginInfo({commit}, param){if(param['token']) {commit(TOKEN, param.token);Vue.ls.set(TOKEN, param.token);}if(param['userinfo']) {commit(USERINFO, param.userinfo);Vue.ls.set(USERINFO, param.userinfo);}},/*** 退出登录*/exitLogin({commit}, param){commit(TOKEN, '');commit(USERINFO, '');Vue.ls.remove(TOKEN);Vue.ls.remove(USERINFO);}
}export default actions;
3.2.6 创建index.js文件
代码如下:
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import getters from './getters'
import actions from './actions'
import mutations from './mutations'Vue.use(Vuex);export default new Vuex.Store({state,getters,actions,mutations
})
3.2.7 main.js中引入
代码如下:
import Vue from 'vue'
import App from './App'
import elementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import 'element-ui/lib/theme-chalk/base.css'
import Storage from 'vue-ls'
import store from '@/store/index'Vue.use(elementUI);Vue.use(Storage, {namespace: 'system_',name: 'ls',storeage: 'local'
});Vue.config.productionTip = false/* eslint-disable no-new */
new Vue({el: '#app',store,components: { App },template: '<App/>'
})
这里store本地状态管理器中文件就创建和定义完了,后续可直接使用this.$store进行调用即可。
3.3 router路由定义
在定义路由前,先在pages目录下,对应创建所有页面文件,如管理员列表页、年级列表页、班级列表页、教师列表页、学员列表页,以及错误页和登录页。
另外,我们需要在components组件中,新建Layout.vue文件,用来定义菜单和内容区域,作为公共部分。代码如下:
html代码部分:
<template><div class="layout-wrap"><el-container class="container"><el-aside width="200px" class="aside-box"><div class="title"><h3>学员管理系统</h3></div><el-menu default-active="1" class="el-menu-vertical" :router="true"><el-menu-item index="1" :route="{path: '/sys/mange'}"><i class="el-icon-s-custom"></i><span slot="title">管理员列表</span></el-menu-item><el-menu-item index="2" :route="{path: '/sys/grade'}"><i class="el-icon-house"></i><span slot="title">年级管理</span></el-menu-item><el-menu-item index="3" :route="{path: '/sys/classify'}"><i class="el-icon-collection-tag"></i><span slot="title">班级管理</span></el-menu-item><el-menu-item index="4" :route="{path: '/sys/teacher'}"><i class="el-icon-user"></i><span slot="title">教师列表</span></el-menu-item><el-menu-item index="5" :route="{path: '/sys/student'}"><i class="el-icon-user"></i><span slot="title">学员列表</span></el-menu-item></el-menu><div class="btn-box"><el-button type="danger" size="mini" @click="logoutEvent">退出登录</el-button></div></el-aside><el-main><transition name="el-zoom-in-center"><router-view /></transition></el-main></el-container></div>
</template>
JS部分:
<script>
export default {name: 'Layout',data () {return { }},methods: {/*** 退出登录*/logoutEvent(){}}
}
</script>
样式部分:
<style lang="scss" scoped>.container{ height: 100vh; }.el-menu{ border-right: 0; }.el-menu-vertical{ height: 100%; }.aside-box{position: relative;padding-top: 80px;border-right: 1px solid #e6e6e6;.title{width: 100%;padding: 30px 0;text-align: center;position: absolute;left: 0;top: 0;z-index: 10;h3{font-size: 20px;color: #409EFF;}}}
</style>
待这些页页创建成功后,我们将在router/index.js中引入这些模块,用来定义页面跳转路由路径,代码如下:
import Vue from 'vue'
import Router from 'vue-router'
import Layout from '@/components/Layout'
import Error404 from '@/pages/Error/err404'
import Index from '@/pages/index'
import Student from '@/pages/student'
import Mange from '@/pages/mange'
import Grade from '@/pages/grade'
import Classify from '@/pages/classify'
import Teacher from '@/pages/teacher'
import Login from '@/pages/login'
import store from '@/store'Vue.use(Router);let _router = new Router({routes: [{path: '/',name: "Home",component: Layout,redirect: '/sys/index',children: [{path: '/sys/index',name: 'Index',component: Index,},{path: '/sys/student',name: 'Student',component: Student,},{path: '/sys/mange',name: 'Mange',component: Mange,},{path: '/sys/grade',name: 'Grade',component: Grade,},{path: '/sys/classify',name: 'Classify',component: Classify,},{path: '/sys/teacher',name: 'Teacher',component: Teacher,}]},{path: '/login',name: 'Login',component: Login,},{path: '*',name: 'Error404',component: Error404,},]
});_router.beforeEach((toRoute, fromRoute, next) => {next();
});export default _router;
此时,我们可以点击页面左侧菜单,进行页面跳转了。关于登录功能,和登录页面跳转,将会在后面继续讲解。
四、登录功能
4.1 权限校验
在前面我们定义好了路由相关跳转路径,那我们如何跳转到登录页呢。此时大家可以往前翻看“3.2.5 创建actions.js文件”中,定义了checkIsLogin函数,用来判断用户是否登录了。在里我们则可以对router/index.js中的 路由卫士进行 稍微调整即可,代码如下:
_router.beforeEach((toRoute, fromRoute, next) => {store.dispatch('checkIsLogin').then(() => {next();}).catch(() => {if(toRoute.path=='/login'){next();}else{next('/login');}});
});
通过执行checkIsLogin函数,来判断用户是否已登录,如登录则直接跳转到下一路由页面中,否则跳转到登录页,完成登录后,则可以正常访问系统页面。为什么要在此判断呢,因为所有页面的访问都要经过“路由卫士”,系统登录是有时效性的,一旦超时token则会自动失效;所以当token失败时,用户点击下一步操作,则会自动跳转到登录页面。
另外,由于这里引用了Promise,所以会出现以下错误:
vue-router Uncaught (in promise) NavigationDuplicated: Avoided redundant navigation to current
这时按以下方法修改即可:
//存储push
let originPush=Router.prototype.push
let originReplace=Router.prototype.replace//重写
Router.prototype.push=function(location,resole,reject){if(resole&&reject){originPush.call(this,location,resole,reject)}else{originPush.call(this,location,()=>{},()=>{})}
}
Router.prototype.replace=function(location,resole,reject){if(resole&&reject){originReplace.call(this,location,resole,reject)}else{originReplace.call(this,location,()=>{},()=>{})}
}_router.beforeEach((toRoute, fromRoute, next) => {store.dispatch('checkIsLogin').then(() => {next();}).catch(() => {if(toRoute.path=='/login'){next();}else{next('/login');}});
});
4.2 登录页面
此时开始着手完成登录页部分的代码,这里直接贴代码了。
html部分:
<template><div class="login-box"><h3>学员管理系统</h3><h4><span>———</span> <span class="tit">安全登录</span> <span>———</span></h4><el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm"><el-form-item label="用户名" prop="username"><el-input type="text" v-model="ruleForm.username" autocomplete="off"></el-input></el-form-item><el-form-item label="密码" prop="password"><el-input type="password" v-model="ruleForm.password" autocomplete="off"></el-input></el-form-item><el-form-item><el-button type="primary" :disabled="disabledButton" @click="submitForm('ruleForm')">登录</el-button><el-button :disabled="disabledButton" @click="resetForm('ruleForm')">重置</el-button></el-form-item></el-form></div>
</template>
js部分:
<script>export default {data(){var validateUsername = (rule, value, callback) => {if (value === '') {callback(new Error('请输入用户名'));} else {callback();}};var validatePass = (rule, value, callback) => {if (value === '') {callback(new Error('请输入密码'));} else {if (this.ruleForm.checkPass !== '') {this.$refs.ruleForm.validateField('checkPass');}callback();}};return {disabledButton: false, //是否禁用按钮ruleForm: {username: '',password: '',},rules: {username: [{ validator: validateUsername, trigger: 'blur' }],password: [{ validator: validatePass, trigger: 'blur' }]},}},methods: {/*** 提交数据* @param {Object} formName*/submitForm(formName) {this.$refs[formName].validate((valid) => {if (valid) {console.log(this.ruleForm);} else {console.log('error submit!!');return false;}});},/*** 重置表单* @param {Object} formName*/resetForm(formName) {this.$refs[formName].resetFields();}}}
</script>
样式部分:
.login-box{width: 600px;height: 390px;padding: 50px 70px;box-sizing: border-box;box-shadow: 0 0 10px rgba(0, 0, 0, .1);border-radius: 10px;overflow: hidden;position: absolute;left: 50%;top: 50%;margin-left: -300px;margin-top: -200px;z-index: 10;h3, h4{font-family: "Microsoft YaHei","微软雅黑",Arial,sans-serif;text-align: center;}h3{font-size: 26px;color: #409eff;}h4{font-size: 14px;color: #999999;font-weight: normal;padding: 10px 0 40px;span{display: inline-block;vertical-align: middle;&.tit{padding: 0 26px;}}}
}
登录页界面如下图:
4.3 添加打开事务功能
在2.2中我们完成了“数据库打开”操作文件的代码,此时需要在内部添加两个打开事务的执行函数,以及一个读写的常量值(IndexedDB中有对应常量值,但已废弃,这里自己定义即可),在db/index.js文件中增加代码如下:
/*** 返回是只读或读写模式*/
export const CONST_READ = {READONLY: "readonly",READWRITE: "readwrite"
}/*** 打开索引用户名游标*/
export const openTransactionIndex = (storeName, indexName, mode) => {if(!storeName){throw '请指定打开事务的表名!';}if(!indexName){throw '请指定需查询索引名!';}mode = mode || CONST_READ.READONLY;//开启事务let transaction = Database.transaction([storeName], mode);//连接对象仓库let store = transaction.objectStore(storeName);//获取对应索引let index = store.index(indexName);//返回游标let cursor = index.openCursor();return {store,index,cursor}
}
4.4 api请求定义
在src目录下新建api/index.js文件,用来定义接口请求功能函数。我们先在db/model目录下创建user.js,用来操作管理员表的增删改查;创建好后,先定义好login和ogout函数,用来处理登录和退出功能,代码如下:
/*** 通过索引获取对应数据*/
export const login = data => {}/*** 退出登录*/
export const logout = token => {}
在db/model/user.js创建好后,我们可以在api/index.js中定义登录和退出接口请求了,代码如下:
import { login, logout } from '@/db/model/user'/*** 登录*/
export const loginInfo = (params) => {return login(params);
}/*** 退出*/
export const logoutInfo = params => {return logout(params);
}
4.5 登录和退出业务功能
在4.3中,我们在db/index.js中添加了添加了处理索引事务函数,我们将其引入到db/model/user.js表中,用来实现登录用户查询和accesstoken信息查询。
另外,用户密码保存时是通过md5加密处理的,所以登录时匹配用户密码时,也需要使用到md5处理函数,这里也需要引入。
用户登录成功时候,需要生成accesstoken随机字符串,这里咱们把这类功能函数放在utils/utils.js工具类中;还有在所有数据请求成功后,需要返回一个统一的数据格式JSON文件,我们也在工具类文件中定义个rJson函数,工具类文件代码如下:
/*** 随机生成字符串* @param {*} _len*/
export const randomStrName = _len => {let _string = 'abcdefghijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ1234567890',_returnStr = '';_len = 'number'===typeof _len ? _len : 50;for(var i = 0; i < _len; i++){_returnStr += _string.charAt( Math.floor(Math.random() * (_string.length-2)) )}return _returnStr;
}/*** 返回数据对象* @param code 状态码, 0 表示错误, 1表示正常* @param data 返回数据集* @param msg 返回提示信息*/
export const rJson = (code, data, msg) => {code = code || '0';data = data || null;msg = msg || '';return {code,data,msg}
}
以上准备工具做好后,我们还要在user.js文件中定义storeName,指定当前文件操作的表。login和logout功能函数实现上,使用Promise来异步操作,这样使回调数据更为灵活点。
代码如下:
import { openTransactionIndex, CONST_READ } from '@/db'
import { rJson, randomStrName } from '@/utils/utils'
import { hex_md5 } from '@/utils/md5'let storeName = 'users';/*** 通过索引获取对应数据*/
export const login = data => {return new Promise((resolve, reject) => {});
}/*** 退出登录*/
export const logout = token => {return new Promise((resolve, reject) => {});
}
4.6 实现登录功能
现在各项关系的文件都创建好了,并且已关联上,咱们可以找到“4.2 登录页面”的js部分,引入api/index.js中的loginInfo,来实现登录数据上传并校验。另外,之前在“3.2.5 创建actions.js文件”中,定义了登录保存用户信息业务函数saveLoginInfo,在登录成功后,需调用此函数将用户信息和accesstoken保存为全局变量中,以及缓存本地存储中。打开pages/login/index.vue文件,代码如下:
<script>import { loginInfo } from '@/api'export default {data(){// ...return {disabledButton: false, //是否禁用按钮ruleForm: {username: '',password: '',},rules: {username: [{ validator: validateUsername, trigger: 'blur' }],password: [{ validator: validatePass, trigger: 'blur' }]},}},methods: {/*** 提交数据*/submitForm(formName) {this.$refs[formName].validate((valid) => {if (valid) {loginInfo(this.ruleForm).then(res => {if(res.code==1){//缓存数据this.$store.dispatch('saveLoginInfo', {token: res.data.accesstoken,userinfo: res.data.userinfo})}this.$message.success(res.msg);this.$router.push('/');}).catch(e => {this.$message.error(e.msg);this.resetForm('ruleForm');});} else {return false;}});},// ...}}
</script>
这里输入用户名和密码进行登录,是没有返回数据的,因为我们还没实现数据库查询功能,现在大家打开db/model/user.js文件,实现login函数部分代码。
在前面“2.3 表结构创建”中,有创建name和accesstoken两个索引;又在“4.3 添加事务功能”中新增了事务创建相关功能函数,这里咱们就可以用上了。首先登录用户名是唯一的,这里通过name索引进行匹配查询,这里openTransactionIndex函数获取相应处理对象。
代码如下:
export const login = data => {return new Promise((resolve, reject) => {//打开事务let { index, store } = openTransactionIndex(storeName, 'name', CONST_READ.READWRITE);//通过用户名,获取用户数据let indexData = index.get(data.username);indexData.onerror = function(e){reject(rJson(0, e, '查询出错了~'));}indexData.onsuccess = function(e){let result = e.target.result;if(result){//判断密码是否一致if(result.password==hex_md5(data.password)){let accesstoken = randomStrName(80);//记录token值result['accesstoken'] = accesstoken;//保存tokenstore.put(result);//返回结果数据resolve(rJson(1, {accesstoken,userinfo: {id: result.id,username: result.name,phone: result.phone}}, '登录成功~'))}else{reject(rJson(0, null, '密码错误~'))}}else{reject(rJson(0, null, '用户名错误~'))}//if end}});
}
以上功能完成后,我们就可以实现登录了,这时我们在登录界面输入用户名和密码,则可以跳转到首页了。在“2.3 表结构创建”中,默认添加了admin账号,暂时可以通过这个账号登录系统,登录后界面如下:
4.7 实现退出功能
在“3.3 router路由定义”中的JS部分,我们调用下退出接口,完成退出功能,并通过“3.2.5 创建actions.js文件”中定义的exitLogin函数,清除本地缓存数据。代码如下:
<script>
import { logoutInfo } from '@/api'
import { TOKEN } from '@/store/mutationsType'
export default {name: 'Layout',data () {return { }},methods: {/*** 退出登录*/logoutEvent(){this.$confirm('确认好退出登录吗?', '提示', {confirmButtonText: '退出',cancelButtonText: '取消',type: 'warning'}).then(() => {logoutInfo(this.$ls.get(TOKEN)).then(res => {this.$store.dispatch('exitLogin');this.$message.success(res.msg);this.$router.push('/login');}).catch(e => {this.$message.error(e.msg);this.$router.push('/login');})}).catch(() => {this.$message({type: 'info',message: '已取消退出'})})}}
}
</script>
同样,现在点击退出按钮不会得到响应,需要将db/model/user.js中的logout功能实现后,才能真正实现退出功能。
代码如下:
export const logout = token => {return new Promise((resolve, reject) => {//打开事务let {index} = openTransactionIndex(storeName, 'accesstoken', CONST_READ.READWRITE);//通过token,获取对应用户数据let indexData = index.get(token);indexData.onerror = function(e){reject(rJson(0, e, '查询出错了~'));}indexData.onsuccess = function(e){let result = e.target.result,source = e.target.source;if(result){let store = source.objectStore;//判断token信息是否匹配if(result.accesstoken == token){//删除accesstokendelete result['accesstoken'];//更新数据store.put(result);resolve(rJson(1, null, '退出成功~'));}else{reject(rJson(0, null, '登录失效~'))}} else{reject(rJson(0, null, '未查询到登录信息~'))}}});
}
这里我们在登录状态下,点击退出按钮,退出成功后就会直接跳转到登录界面了。
4.8 校验缓存数据中的登录信息
我们又要回到 “ 3.2.5 创建actions.js文件 ”中,为什么当初在定里定义函数判断token失效,而不是直接放到 路由卫生中获取token,直接进行判断呢;因为这里除了要校验本地是否缓存token信息,同时也要判断数据库中token是否存在或失效,话不多说,直接上代码。
在db/model/user.js中添加checkToken函数,代码如下:
/*** 校验token是否存在*/
export const checkToken = token => {}
在api/index.js中添加tokenIsFailure函数,代码如下:
import { login, logout, checkToken } from '@/db/model/user'/*** 判断数据库中token是否失效*/
export const tokenIsFailure = token => {return checkToken(token);
}
store/actions.js文件中的checkIsLogin进行改造,代码如下:
checkIsLogin(){let token = Vue.ls.get(TOKEN);return new Promise((resolve, reject) => {if(token){tokenIsFailure(token).then(() => {resolve();}).catch(() => {commit(TOKEN, '');Vue.ls.remove(TOKEN);reject();});}else{reject();}});}
当读取到本地缓存的token信息后,还需要进行数据库校验;如果数据库中不存在,则会直接跳转到登录页,并清除本地缓存的token信息。
另外,我们之前在“2.2 打开数据库”中定义过DBIsLoadSuccess函数,因为IndexedDB的open()函数不是执行后就返回实例对象的,所有刚进入系统,很有可能会遇到实例对象为空的情况。这时我们需要特别小心,在调用事务前,先判断数据库实例对象是否存在。
接下来让我们完成db/model/user.js中的checkToken函数功能,代码如下:
export const checkToken = token => {return new Promise((resolve, reject) => {//判断数据库是否打开DBIsLoadSuccess().then(() => {//打开事务let {index} = openTransactionIndex(storeName, 'accesstoken');//通过token,查询数据let indexKey = index.getKey(token);indexKey.onerror = function(e){reject(rJson(0, e, '查询出错了~'));}indexKey.onsuccess = function(e){let result = e.target.result;if(result){resolve(rJson(1, e, '校验成功~'))}else{reject(rJson(0, e, '登录失效~'));}}}).catch(() => {reject(rJson(0, e, '数据库打开失败~'));})});
}
这里我们先手动清除数据表中的accesstoken,然后点击任意页面跳转,此时会发现页面直接跳转到登录页了。
正常情况下,在登录时创建的token,是带有时效的,这块这里就不细讲,大家可以通过自己的理解,去完善此部分。
至此,该篇内容已讲解完了,该系统其他功能完善请看后续篇幅。
下一篇地址:本地数据库IndexedDB - 学员管理系统之列表管理(二)_觉醒法师的博客-CSDN博客
本地数据库IndexedDB - 学员管理系统之登录(一)相关推荐
- 本地数据库IndexedDB - 初学者
IndexedDB 是一种可以让你在用户的浏览器内持久化存储数据的方法.IndexedDB 为生成 Web Application 提供了丰富的查询能力,使我们的应用在在线和离线时都可以正常工作. 一 ...
- HTML5 本地数据库IndexedDB数据库
在HTML 5中,新增一种被称为indexedDB的数据库,该数据库是一种存储在客户端本地的NoSQL数据库. window.indexedDB=window.indexedDB||window.we ...
- indexDB 本地数据库
indexDB 本地数据库 IndexedDB是一种使用浏览器存储大量数据的方法,它创造的数据可以被查询,并且可以离线使用. 优点:空间大小,大于250M:支持二进制:IndexedDB不但可以存储对 ...
- mysql数据库小系统_Mysql数据库基础小实例 学员管理系统菜单
package test; import java.sql.*; import java.util.Scanner; public class testSql002_StudentTest { /** ...
- SQL Server 本地数据库登录不上 解决方法
SQL Server 本地数据库登录不上 解决方法 参考文章: (1)SQL Server 本地数据库登录不上 解决方法 (2)https://www.cnblogs.com/cr-cool/p/10 ...
- 微信授权登录后,把获取的信息存储到本地数据库中(超详细有用)!
微信授权登录后,把获取的信息存储到本地数据库中(超详细有用)!下面一起来看看吧! <?php namespace Home\Controller; use Think\Controller; c ...
- sqlplus命令无需监听服务就可以登录本地数据库的登录方式的研究
实验一 1.执行sqlplus命令,然后输入账号密码登录,发现成功登录orcl 2.关掉默认监听服务,确认系统上没有运行的监听服务 3.继续用1的方法登录数据库,依然成功登录 结论一: 1.在本地直接 ...
- Oracle11g本地数据库创建用户和密码,附赠使用PL/SQL软件登录用户
Oracle11g创建用户和密码 步骤: 管理员身份打开cmd命令窗口: 输入sqlplus 先使用管理员账户登录,为了创建普通用户: 用户名:sys as sysdba 密码:随便输 创建新用户,我 ...
- 《MySQL入门很简单》学习笔记(22)之第22章驾校学员管理系统(关键词:数据库/MySQL)
第22章 驾校学员管理系统 略. 参考文献: 1.<MySQL入门很简单>.
最新文章
- 对python里的装饰器
- Arduino初初教程10——温湿度传感器
- jQuery加载一个html页面到指定的div里
- python打包exe os模块_python打包成exe格式的方法求教
- 在AD中恢复误删除的对象
- drupal linux安装,在Debian 10(Buster) Linux服务器中安装drupal 8.8.0的说明
- 前端学习(2592):当前用户显示
- 文件打开特别慢_“Origin进不去、下载慢”的解决办法合集
- mysql连接超时timeout问题
- Element-UI Form表单 resetFields() 重置表单无效问题
- rx java操作符_RxJava 常用操作符大全(一)
- 台式计算机显示器的分辨率,台式电脑分辨率多少合适,测试电脑分辨率
- 目标检测应用竞赛 | 天池铝型材表面瑕疵(含数据集、详细解决方案)
- 阿里月饼事件,猿方怎么看?
- Django批量修改 get_field_display foreignkey
- Kryo官方文档-中文翻译
- 2018-2019-2 20189206 《密码与安全新技术专题》 第六次作业
- (2020年最新版)App Store 审核指南
- oracle 发的邮件qq,python基于SMTP发送QQ邮件
- android bilibili sd卡,移动bilibili缓存视频到SD卡