├── _image ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png ├── 9.png ├── 10.png ├── 11.png ├── qr_code.png └── simple-logic.png ├── server ├── login │ ├── yunpian-sdk-php │ │ └── .readme │ ├── forget-password.php │ ├── update-userinfo.php │ ├── signup.php │ ├── get-auth-code.php │ └── login.php ├── get-help-message.php ├── submit-order.php ├── randomword.js ├── backend │ ├── js │ │ ├── manage.js │ │ ├── manual.js │ │ └── scan.js │ ├── css │ │ ├── manual.css │ │ ├── login.css │ │ ├── manage.css │ │ └── scan.css │ ├── index.html │ ├── manage.php │ ├── message.php │ └── operate.php ├── getdata.php └── database_details.sql ├── src ├── common │ ├── error.png │ ├── loading.gif │ ├── senddata.js │ ├── fullscreen.js │ └── getdata.js ├── router │ └── routes.js ├── components │ ├── login-css │ │ ├── login-signup.css │ │ ├── login-forget-password.css │ │ ├── login-userinfo.css │ │ ├── login-login.css │ │ └── login-normal.css │ ├── overlay.vue │ ├── container.vue │ ├── content │ │ ├── book-item.vue │ │ ├── book-card.vue │ │ └── content.vue │ ├── loading.vue │ ├── menu │ │ ├── help.vue │ │ ├── setting.vue │ │ ├── menu.vue │ │ └── book-list.vue │ ├── header.vue │ └── login.vue ├── main.js ├── vuex │ └── store.js └── app.vue ├── index.html ├── package.json ├── webpack.config.js ├── additional.md └── README.md /_image/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percy507/vue-book/HEAD/_image/1.png -------------------------------------------------------------------------------- /_image/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percy507/vue-book/HEAD/_image/2.png -------------------------------------------------------------------------------- /_image/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percy507/vue-book/HEAD/_image/3.png -------------------------------------------------------------------------------- /_image/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percy507/vue-book/HEAD/_image/4.png -------------------------------------------------------------------------------- /_image/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percy507/vue-book/HEAD/_image/5.png -------------------------------------------------------------------------------- /_image/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percy507/vue-book/HEAD/_image/6.png -------------------------------------------------------------------------------- /_image/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percy507/vue-book/HEAD/_image/7.png -------------------------------------------------------------------------------- /_image/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percy507/vue-book/HEAD/_image/8.png -------------------------------------------------------------------------------- /_image/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percy507/vue-book/HEAD/_image/9.png -------------------------------------------------------------------------------- /_image/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percy507/vue-book/HEAD/_image/10.png -------------------------------------------------------------------------------- /_image/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percy507/vue-book/HEAD/_image/11.png -------------------------------------------------------------------------------- /server/login/yunpian-sdk-php/.readme: -------------------------------------------------------------------------------- 1 | Put your yunpian-sdk-php files inside this folder. -------------------------------------------------------------------------------- /_image/qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percy507/vue-book/HEAD/_image/qr_code.png -------------------------------------------------------------------------------- /src/common/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percy507/vue-book/HEAD/src/common/error.png -------------------------------------------------------------------------------- /src/common/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percy507/vue-book/HEAD/src/common/loading.gif -------------------------------------------------------------------------------- /_image/simple-logic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percy507/vue-book/HEAD/_image/simple-logic.png -------------------------------------------------------------------------------- /src/router/routes.js: -------------------------------------------------------------------------------- 1 | const routes = [{ 2 | path: '/', 3 | redirect: '/home' 4 | }]; 5 | 6 | export default routes; -------------------------------------------------------------------------------- /src/components/login-css/login-signup.css: -------------------------------------------------------------------------------- 1 | .signup-page .form { 2 | padding-top: 20%; 3 | } 4 | 5 | .signup-page .return { 6 | position: absolute; 7 | z-index: 333; 8 | top: 20px; 9 | left: 20px; 10 | color: rgba(255, 255, 255, 0.6); 11 | font-size: 16px; 12 | } -------------------------------------------------------------------------------- /src/components/login-css/login-forget-password.css: -------------------------------------------------------------------------------- 1 | .forget-password-page .form { 2 | padding-top: 20%; 3 | } 4 | 5 | .forget-password-page .form-item:nth-last-child(1) input { 6 | color: #F79D2D; 7 | } 8 | 9 | .forget-password-page .return { 10 | position: absolute; 11 | z-index: 333; 12 | top: 20px; 13 | right: 20px; 14 | color: rgba(255, 255, 255, 0.6); 15 | font-size: 16px; 16 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 伯牙书舍 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/login-css/login-userinfo.css: -------------------------------------------------------------------------------- 1 | .userinfo-page .form { 2 | padding-top: 20%; 3 | } 4 | 5 | .userinfo-page .return { 6 | position: absolute; 7 | top: 10px; 8 | left: 0; 9 | right: 0; 10 | z-index: 333; 11 | width: 60px; 12 | margin: 0 auto; 13 | text-align: center; 14 | color: rgba(255, 255, 255, 0.6); 15 | font-size: 16px; 16 | } 17 | 18 | .userinfo-page .return p { 19 | margin: 0; 20 | } 21 | 22 | .userinfo-page .return p:first-child { 23 | margin-bottom: 10px; 24 | } -------------------------------------------------------------------------------- /src/components/login-css/login-login.css: -------------------------------------------------------------------------------- 1 | #just-see { 2 | position: absolute; 3 | top: 20px; 4 | right: 20px; 5 | z-index: 333; 6 | color: rgba(255, 255, 255, 0.6); 7 | font-size: 16px; 8 | } 9 | 10 | #forget-password { 11 | position: absolute; 12 | top: 20px; 13 | left: 20px; 14 | z-index: 333; 15 | color: rgba(255, 255, 255, 0.6); 16 | font-size: 16px; 17 | } 18 | 19 | #login-login { 20 | margin-right: 40px; 21 | } 22 | 23 | .login-page .form-item:nth-last-child(1) input { 24 | color: #03a9f4; 25 | } -------------------------------------------------------------------------------- /server/get-help-message.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | set_charset("utf8"); 18 | 19 | // 将订单信息插入数据库 20 | $sql = "SELECT message FROM $table_name ORDER BY message_id DESC LIMIT 1"; 21 | 22 | $result = $mysqli->query($sql); 23 | 24 | if ($result->num_rows > 0) { 25 | $row = $result->fetch_row(); 26 | 27 | echo json_encode($row, 256); 28 | } else { 29 | echo "暂无消息"; 30 | } 31 | 32 | // 断开与数据库的连接 33 | $mysqli->close(); -------------------------------------------------------------------------------- /src/common/senddata.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 利用 Ajax 发送数据 3 | * 4 | * @param {string} url 地址 5 | * @param {object} dataObj 包含发送数据的对象 6 | * @returns promise 对象 7 | */ 8 | 9 | const sendData = function(url, dataObj) { 10 | return new Promise(function(resolve, reject) { 11 | let request = new XMLHttpRequest(); 12 | 13 | request.onload = function() { 14 | resolve(this.responseText.trim()); 15 | }; 16 | 17 | request.onerror = function() { 18 | reject(new Error(this.statusText)); 19 | }; 20 | 21 | let root = new FormData(); 22 | 23 | for (let key of Object.keys(dataObj)) { 24 | root.append(key, dataObj[key]); 25 | } 26 | 27 | request.open("POST", url, true); 28 | request.send(root); 29 | }); 30 | } 31 | 32 | export default sendData; -------------------------------------------------------------------------------- /server/submit-order.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 13 | 14 | set_charset("utf8"); 26 | 27 | // 将订单信息插入数据库 28 | $sql = "INSERT INTO $table_name(phone_number,order_details) VALUES('$phone_number','$order_details')"; 29 | 30 | if ($mysqli->query($sql)) { 31 | echo "success"; 32 | $mysqli->close(); 33 | exit(); 34 | } else { 35 | echo "unknown_wrong"; 36 | $mysqli->close(); 37 | exit(); 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/components/overlay.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/container.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 34 | 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new", 3 | "description": "A Vue.js project", 4 | "version": "2.0", 5 | "author": "percy507 ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", 9 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" 10 | }, 11 | "dependencies": { 12 | "normalize.css": "^6.0.0", 13 | "vue": "^2.2.1", 14 | "vue-lazyload": "^1.0.3", 15 | "vue-router": "^2.5.2", 16 | "vuex": "^2.3.1" 17 | }, 18 | "devDependencies": { 19 | "babel-core": "^6.0.0", 20 | "babel-loader": "^6.0.0", 21 | "babel-preset-latest": "^6.0.0", 22 | "cross-env": "^3.0.0", 23 | "css-loader": "^0.25.0", 24 | "extract-text-webpack-plugin": "^2.1.0", 25 | "file-loader": "^0.9.0", 26 | "style-loader": "^0.16.1", 27 | "stylus": "^0.54.5", 28 | "stylus-loader": "^3.0.1", 29 | "vue-loader": "^11.1.4", 30 | "vue-template-compiler": "^2.2.1", 31 | "webpack": "^2.2.0", 32 | "webpack-dev-server": "^2.2.0" 33 | } 34 | } -------------------------------------------------------------------------------- /server/randomword.js: -------------------------------------------------------------------------------- 1 | // 生成随机的指定数量的数据 2 | 3 | let temp = "INSERT INTO qingong_books(book_title,book_author,book_type,book_cover,rest_number,book_tags) VALUES"; 4 | let time = 10000; // 定义生成数据的数量 5 | 6 | let book_type = ['理学院', '材料与纺织学院、丝绸学院', '服装学院', '信息学院', '机械与自动控制学院', '建筑工程学院', '生命科学学院', '经济管理学院', '艺术与设计学院', '法政学院', '外国语学院', '史量才新闻与传播学院', '马克思主义学院', '启新学院', '继续教育学院', '科技与艺术学院', '其它']; 7 | 8 | let letters = 'ABCDEFGHIGKLMNOPQRSTUVWXYZ01234567890123456789'; 9 | let letterArr = letters.split(''); 10 | let len = letterArr.length; 11 | 12 | function randomWord(num) { 13 | let word = ''; 14 | for (let k = 0; k < Math.floor(Math.random() * num + 5); k++) { 15 | word += letterArr[Math.floor(Math.random() * len)]; 16 | } 17 | return word; 18 | } 19 | 20 | for (let i = 0; i < time; i++) { 21 | let data = `('${randomWord(20)}' , '${randomWord(10)}','${book_type[Math.floor(Math.random() * 17)]}','https://img1.doubanio.com/lpic/s2831${Math.floor(Math.random() * 8888 + 1000)}.jpg', ${Math.floor(Math.random() * 100 + 1)},'${randomWord(5)+"|"+randomWord(5)+"|"+randomWord(5)}'),`; 22 | temp += data; 23 | if (i + 1 == time) { 24 | temp = temp.slice(0, temp.length - 1); 25 | temp += ';'; 26 | } 27 | } -------------------------------------------------------------------------------- /server/backend/js/manage.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | let whichWay = document.querySelector('.which-way'); 3 | 4 | let chooseScan = document.querySelector('.choose-scan'); 5 | let chooseManual = document.querySelector('.choose-manual'); 6 | let scanBook = document.querySelector('.scan-add-book'); 7 | let manualBook = document.querySelector('.manual-add-book'); 8 | 9 | let switchWay = document.querySelector('.switch-way'); 10 | let scan = document.getElementById('scan'); 11 | let manual = document.getElementById('manual'); 12 | 13 | chooseScan.onclick = function() { 14 | whichWay.classList.add('hidden'); 15 | scanBook.classList.remove('hidden'); 16 | switchWay.classList.remove('hidden'); 17 | startScan(); 18 | }; 19 | 20 | chooseManual.onclick = function() { 21 | whichWay.classList.add('hidden'); 22 | manualBook.classList.remove('hidden'); 23 | switchWay.classList.remove('hidden'); 24 | }; 25 | 26 | scan.onclick = function() { 27 | scanBook.classList.remove('hidden'); 28 | manualBook.classList.add('hidden'); 29 | startScan(); 30 | }; 31 | 32 | manual.onclick = function() { 33 | scanBook.classList.add('hidden'); 34 | manualBook.classList.remove('hidden'); 35 | }; 36 | })(); -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import VueLazyload from 'vue-lazyload'; 4 | import routes from './router/routes.js'; // 引入路由配置 5 | import store from './vuex/store.js'; 6 | import App from './app.vue'; 7 | import 'normalize.css'; 8 | import './common/loading.gif'; 9 | import './common/error.png'; 10 | 11 | // 为了方便遍历,这里为一些类数组对象注册数组的 forEach 方法 12 | NodeList.prototype.forEach = Array.prototype.forEach; 13 | Storage.prototype.forEach = Array.prototype.forEach; 14 | 15 | // 万能脚本,自动屏蔽 pc 16 | window.addEventListener('DOMContentLoaded', function() { 17 | if (!((/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i).test(navigator.userAgent))) { 18 | document.documentElement.innerHTML = "

请使用手机打开此页面哦~

"; 19 | } 20 | }); 21 | 22 | Vue.use(VueRouter); 23 | 24 | Vue.use(VueLazyload, { 25 | preLoad: 1.3, 26 | error: './public/error.png', 27 | loading: './public/loading.gif', 28 | attempt: 1 29 | }); 30 | 31 | const router = new VueRouter({ 32 | routes 33 | }); 34 | 35 | new Vue({ 36 | router, 37 | store, 38 | render: h => h(App) 39 | }).$mount('#app'); 40 | 41 | 42 | /* 43 | // 通用的关闭页面时提示,对微信内置浏览器无效 44 | window.addEventListener("beforeunload", function() { 45 | var message = "你真的要离开吗?"; 46 | event.returnValue = message; 47 | return message; 48 | }, false); 49 | */ -------------------------------------------------------------------------------- /src/common/fullscreen.js: -------------------------------------------------------------------------------- 1 | // 兼容各个浏览器 2 | 3 | const launchFullscreen = function(element) { 4 | if (element.requestFullscreen) { 5 | element.requestFullscreen(); 6 | } else if (element.mozRequestFullScreen) { 7 | element.mozRequestFullScreen(); 8 | } else if (element.msRequestFullscreen) { 9 | element.msRequestFullscreen(); 10 | } else if (element.webkitRequestFullscreen) { 11 | element.webkitRequestFullScreen(); 12 | } 13 | }; 14 | 15 | const exitFullscreen = function() { 16 | if (document.exitFullscreen) { 17 | document.exitFullscreen(); 18 | } else if (document.msExitFullscreen) { 19 | document.msExitFullscreen(); 20 | } else if (document.mozCancelFullScreen) { 21 | document.mozCancelFullScreen(); 22 | } else if (document.webkitExitFullscreen) { 23 | document.webkitExitFullscreen(); 24 | } 25 | }; 26 | 27 | const registFullscreenChangeEvent = function(handler) { 28 | document.addEventListener("fullscreenchange", handler); 29 | document.addEventListener("webkitfullscreenchange", handler); 30 | document.addEventListener("mozfullscreenchange", handler); 31 | document.addEventListener("MSFullscreenChange", handler); 32 | }; 33 | 34 | export default { 35 | launch: launchFullscreen, 36 | exit: exitFullscreen, 37 | registChangeEvent: registFullscreenChangeEvent 38 | } -------------------------------------------------------------------------------- /src/components/content/book-item.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | 24 | -------------------------------------------------------------------------------- /src/common/getdata.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 利用 Ajax 获取数据 3 | * 4 | * @param {string} url 地址 5 | * @param {string} [method='get'] 方法 6 | * @param {object} queryObj url查询对象 7 | * @returns promise 对象 8 | */ 9 | 10 | /* 11 | 注意:下面函数未针对 POST 方法进行优化 12 | */ 13 | 14 | const getData = function(url, method = 'get', queryObj) { 15 | 16 | // 解析 URL 17 | if (typeof method !== 'string') { 18 | queryObj = method; 19 | method = 'get'; 20 | } 21 | if (queryObj && Object.prototype.toString.call(queryObj) == '[object Object]') { 22 | let keyArr = Object.keys(queryObj); 23 | let len = keyArr.length; 24 | let queryStr = ''; 25 | for (let i = 0; i < len; i++) { 26 | queryStr += encodeURIComponent(keyArr[i]) + '=' + encodeURIComponent(queryObj[keyArr[i]]); 27 | if (i + 1 != len) { 28 | queryStr += '&'; 29 | } 30 | } 31 | if (url.charAt(url.length - 1) != '?') { 32 | url += '?'; 33 | } 34 | url += queryStr; 35 | } 36 | 37 | return new Promise(function(resolve, reject) { 38 | 39 | let request = new XMLHttpRequest(); 40 | 41 | request.onload = function() { 42 | resolve(this.responseText); 43 | }; 44 | 45 | request.onerror = reject; 46 | request.open(method, url); 47 | request.send(); 48 | }); 49 | } 50 | 51 | /* example 52 | let url = 'http://blog.percymong.com/2017/04/14/'; 53 | getData(url).then(function(result) { 54 | console.log(result); 55 | }).catch(function() { 56 | alert('获取数据失败~'); 57 | }); 58 | */ 59 | 60 | export default getData; -------------------------------------------------------------------------------- /server/login/forget-password.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 39 | 40 | set_charset("utf8"); 52 | 53 | // 数据正确,检测 $user 的唯一性,即用户是否存在 54 | $sql = "SELECT phone_number FROM $table_name WHERE phone_number = '$user'"; 55 | $result = $mysqli->query($sql); 56 | 57 | if ($result->num_rows == 0) { 58 | echo 'not_found'; 59 | $mysqli->close(); 60 | exit(); 61 | } 62 | 63 | // 若用户存在,则更新密码 64 | $sql = "UPDATE $table_name SET password = '$user_password' WHERE phone_number = '$user'"; 65 | 66 | if ($mysqli->query($sql)) { 67 | echo "change_password_success"; 68 | $mysqli->close(); 69 | exit(); 70 | } else { 71 | echo "unknown_wrong"; 72 | $mysqli->close(); 73 | exit(); 74 | } -------------------------------------------------------------------------------- /server/login/update-userinfo.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 41 | 42 | set_charset("utf8"); 54 | 55 | // 若用户存在,则更新密码 56 | $sql = "UPDATE $table_name SET name = '$name',id_number = '$id_number',academy = '$academy',address = '$address' WHERE phone_number = '$user'"; 57 | 58 | if ($mysqli->query($sql)) { 59 | echo "update_userinfo_success"; 60 | $mysqli->close(); 61 | exit(); 62 | } else { 63 | echo "unknown_wrong"; 64 | $mysqli->close(); 65 | exit(); 66 | } -------------------------------------------------------------------------------- /server/login/signup.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 39 | 40 | set_charset("utf8"); 52 | 53 | // 数据正确,检测 $user 的唯一性,即用户是否存在 54 | $sql = "SELECT phone_number FROM $table_name WHERE phone_number = '$user'"; 55 | $result = $mysqli->query($sql); 56 | 57 | if ($result->num_rows != 0) { 58 | echo 'phone_number_existed'; 59 | $mysqli->close(); 60 | exit(); 61 | } 62 | 63 | // 若用户不存在,则插入数据库 64 | $sql = "INSERT INTO $table_name(phone_number,password) VALUES('$user', '$user_password')"; 65 | 66 | if ($mysqli->query($sql)) { 67 | echo "signup_success"; 68 | $mysqli->close(); 69 | exit(); 70 | } else { 71 | echo "unknown_wrong"; 72 | $mysqli->close(); 73 | exit(); 74 | } 75 | 76 | -------------------------------------------------------------------------------- /server/login/get-auth-code.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 27 | 28 | set_charset("utf8"); 40 | 41 | // 数据正确,检测 $user 的唯一性,即用户是否存在 42 | $sql = "SELECT phone_number FROM $table_name WHERE phone_number = '$phone_number'"; 43 | $result = $mysqli->query($sql); 44 | 45 | // 如果是忘记密码页面,则需要检测用户是否已存在 46 | if ($flag === 'forget_password' && $result->num_rows === 0) { 47 | echo 'not_found'; 48 | $mysqli->close(); 49 | exit(); 50 | } 51 | 52 | // 如果是注册页面,也需要检测用户是否已存在 53 | if ($flag === 'signup' && $result->num_rows !== 0) { 54 | echo 'user_existed'; 55 | exit(); 56 | } 57 | 58 | ?> 59 | 60 | single_send($data); 69 | 70 | if ($result->success) { 71 | echo 'success'; 72 | exit(); 73 | } else { 74 | echo 'send_wrong'; 75 | exit(); 76 | } 77 | -------------------------------------------------------------------------------- /src/vuex/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | Vue.use(Vuex); 5 | 6 | const state = { 7 | isShowSidebar: false, 8 | isLoading: false, 9 | contentType: 'home', 10 | symbol: '_&_', // 定义分割书名和作者名的标识符,标识符必须唯一并且不被包含于书名或作者名中 11 | keyword: '', 12 | isShowCard: false, 13 | isShowLogin: true, 14 | isShowHelp: false, 15 | isShowBookList: false, 16 | isShowSetting: false, 17 | // user 对象用来存储登录成功后从服务器接收到的数据 18 | user: { 19 | phone_number: '', 20 | name: '', 21 | id_number: '', 22 | academy: '', 23 | address: '' 24 | }, 25 | user_state: '' 26 | }; 27 | 28 | const mutations = { 29 | TOGGLE_SIDEBAR(state) { 30 | state.isShowSidebar = state.isShowSidebar ? false : true; 31 | }, 32 | TOGGLE_LOADING(state) { 33 | state.isLoading = state.isLoading ? false : true; 34 | }, 35 | CHANGE_CONTENTTYPE(state, type) { 36 | state.contentType = type; 37 | }, 38 | CHANGE_KEYWORD(state, keyword) { 39 | state.keyword = keyword; 40 | }, 41 | TOGGLE_CARD(state) { 42 | state.isShowCard = state.isShowCard ? false : true; 43 | }, 44 | TOGGLE_LOGIN_PAGE(state) { 45 | state.isShowLogin = state.isShowLogin ? false : true; 46 | }, 47 | CHANGE_USER_STATE(state, user_state) { 48 | state.user_state = user_state; 49 | }, 50 | TOGGLE_BOOK_LIST(state) { 51 | state.isShowBookList = state.isShowBookList ? false : true; 52 | }, 53 | TOGGLE_SETTING(state) { 54 | state.isShowSetting = state.isShowSetting ? false : true; 55 | }, 56 | TOGGLE_HELP(state) { 57 | state.isShowHelp = state.isShowHelp ? false : true; 58 | } 59 | } 60 | 61 | export default new Vuex.Store({ 62 | state, 63 | mutations 64 | }) -------------------------------------------------------------------------------- /server/backend/css/manual.css: -------------------------------------------------------------------------------- 1 | .manual-add-book { 2 | width: 100%; 3 | padding-bottom: 50px; 4 | } 5 | 6 | .manual-add-book>h1 { 7 | width: 100%; 8 | height: 50px; 9 | margin: 0; 10 | line-height: 50px; 11 | background: #03a9f4; 12 | color: #FFF; 13 | font-size: 1.6em; 14 | font-weight: bold; 15 | text-align: center; 16 | } 17 | 18 | .form-item { 19 | position: relative; 20 | display: flex; 21 | width: 80%; 22 | height: 30px; 23 | line-height: 30px; 24 | margin: 15px auto; 25 | } 26 | 27 | .form-item:nth-child(1) { 28 | margin-top: 30px; 29 | } 30 | 31 | .form-item label { 32 | font-size: 1.2em; 33 | color: #333; 34 | font-weight: bold; 35 | } 36 | 37 | input[type=text] { 38 | flex-grow: 1; 39 | padding: 2px 4px; 40 | } 41 | 42 | label[for=upload-img] { 43 | box-sizing: border-box; 44 | width: 100%; 45 | display: block; 46 | height: 30px; 47 | background: #aaa; 48 | border: 1px solid #aaa; 49 | border-radius: 5px; 50 | text-align: center; 51 | } 52 | 53 | #upload-img { 54 | position: absolute; 55 | top: 0; 56 | left: 0; 57 | z-index: 9; 58 | opacity: 0; 59 | display: block; 60 | } 61 | 62 | .form-item:nth-last-child(1) { 63 | position: relative; 64 | box-sizing: border-box; 65 | width: 100%; 66 | height: 50px; 67 | padding: 10px 20px; 68 | margin-top: 30px; 69 | line-height: 30px; 70 | } 71 | 72 | .submit-data { 73 | box-sizing: border-box; 74 | position: absolute; 75 | width: 70px; 76 | height: 30px; 77 | border: none; 78 | } 79 | 80 | #submit-in { 81 | left: 60px; 82 | } 83 | 84 | #submit-out { 85 | right: 60px; 86 | } 87 | 88 | .submit-close { 89 | pointer-events: none; 90 | } -------------------------------------------------------------------------------- /src/components/loading.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | 32 | -------------------------------------------------------------------------------- /server/getdata.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 35 | 36 | 39 | 40 | set_charset("utf8"); 52 | 53 | // 读取数据 54 | $needed_prop = "book_title,book_author,book_cover,rest_number,book_type"; 55 | if ($type === 'home') { 56 | if ($user_state === 'login_success' and $academy) { 57 | $sql = "SELECT $needed_prop FROM $table_name ORDER BY book_type <> '$academy' LIMIT $data_num OFFSET $offset"; 58 | } else { 59 | $sql = "SELECT $needed_prop FROM $table_name LIMIT $data_num OFFSET $offset"; 60 | } 61 | } elseif ($type === 'search') { 62 | if ($user_state === 'login_success' and $academy) { 63 | $sql = "SELECT $needed_prop FROM $table_name WHERE book_title LIKE '%$keyword%' OR book_author LIKE '%$keyword%' OR book_tags LIKE '%$keyword%' ORDER BY book_type <> '$academy' LIMIT $data_num OFFSET $offset"; 64 | } else { 65 | $sql = "SELECT $needed_prop FROM $table_name WHERE book_title LIKE '%$keyword%' OR book_author LIKE '%$keyword%' OR book_tags LIKE '%$keyword%' LIMIT $data_num OFFSET $offset"; 66 | } 67 | } else { 68 | echo 'unknown error'; 69 | exit(); 70 | } 71 | 72 | $result = $mysqli->query($sql); 73 | 74 | // 待删,方便前端显示加载效果,设定 1 秒的延迟 75 | sleep(1); 76 | 77 | if ($result->num_rows > 0) { 78 | // $allData = $result->fetch_all(); 79 | $allData = []; 80 | while ($row = $result->fetch_row()) { 81 | array_push($allData, $row); 82 | } 83 | echo json_encode($allData, 256); 84 | } else { 85 | echo "nothing"; 86 | } 87 | 88 | // 断开与数据库的连接 89 | $mysqli->close(); 90 | 91 | 92 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var ExtractTextPlugin = require("extract-text-webpack-plugin"); 4 | 5 | module.exports = { 6 | entry: './src/main.js', 7 | output: { 8 | path: path.resolve(__dirname, './public'), 9 | publicPath: '/public/', 10 | filename: './build.js' 11 | }, 12 | module: { 13 | rules: [{ 14 | test: /\.vue$/, 15 | loader: 'vue-loader', 16 | options: { 17 | loaders: { 18 | css: ExtractTextPlugin.extract({ 19 | use: 'css-loader' 20 | }), 21 | stylus: ExtractTextPlugin.extract({ 22 | use: ['css-loader', 'stylus-loader'] 23 | }) 24 | } 25 | } 26 | }, 27 | { 28 | test: /\.css$/, 29 | use: ExtractTextPlugin.extract({ 30 | use: "css-loader" 31 | }) 32 | }, 33 | { 34 | test: /\.js$/, 35 | loader: 'babel-loader', 36 | exclude: /node_modules/ 37 | }, 38 | { 39 | test: /\.(png|jpg|ttf|eot|svg|woff|gif|svg)$/, 40 | loader: 'file-loader', 41 | options: { 42 | name: '[name].[ext]?[hash]', 43 | publicPath: './' 44 | } 45 | } 46 | ] 47 | }, 48 | plugins: [ 49 | new ExtractTextPlugin("./app.css"), 50 | ], 51 | resolve: { 52 | alias: { 53 | 'vue$': 'vue/dist/vue.esm.js' 54 | } 55 | }, 56 | devServer: { 57 | historyApiFallback: true, 58 | noInfo: true 59 | }, 60 | performance: { 61 | hints: false 62 | }, 63 | devtool: '#eval-source-map' 64 | } 65 | 66 | if (process.env.NODE_ENV === 'production') { 67 | module.exports.devtool = '#source-map' 68 | // http://vue-loader.vuejs.org/en/workflow/production.html 69 | module.exports.plugins = (module.exports.plugins || []).concat([ 70 | new webpack.DefinePlugin({ 71 | 'process.env': { 72 | NODE_ENV: '"production"' 73 | } 74 | }), 75 | new webpack.optimize.UglifyJsPlugin({ 76 | sourceMap: true, 77 | compress: { 78 | warnings: false 79 | } 80 | }), 81 | new webpack.LoaderOptionsPlugin({ 82 | minimize: true 83 | }) 84 | ]) 85 | } -------------------------------------------------------------------------------- /server/database_details.sql: -------------------------------------------------------------------------------- 1 | # 2 | # 注意先建好下面的数据库表 3 | # 要是建表时出现错误,注意检查是不是因为 MySQL 版本太低 4 | # 5 | 6 | -- 创建数据库 7 | CREATE DATABASE qingong_db CHARACTER SET utf8 COLLATE utf8_general_ci; 8 | 9 | 10 | -- 创建表 qingong_books,存储书籍信息 11 | CREATE TABLE qingong_books( 12 | book_id INT NOT NULL AUTO_INCREMENT, # 并无实际用处,仅作为主键 13 | book_title VARCHAR(100) NOT NULL, # 标题 14 | book_author VARCHAR(100) NOT NULL, # 作者 15 | book_type VARCHAR(200) NOT NULL, # 书籍分类,这里按学院分 16 | book_cover VARCHAR(1000) NOT NULL, # 书籍封面图片链接地址 17 | rest_number INT NOT NULL, # 书籍库存量 18 | book_tags VARCHAR(1000), # 书籍被标记的标签 19 | PRIMARY KEY(book_id) 20 | )ENGINE=InnoDB AUTO_INCREMENT=20170001 DEFAULT CHARACTER SET utf8; 21 | 22 | 23 | -- 创建表 qingong_users,存储用户信息 24 | CREATE TABLE qingong_users( 25 | phone_number VARCHAR(11) NOT NULL, # 用户名,这里为手机号 26 | password VARCHAR(100) NOT NULL, # 密码 27 | name VARCHAR(100) NOT NULL DEFAULT "default", # 用户姓名 28 | id_number VARCHAR(50) NOT NULL DEFAULT "default", # 用户学号 29 | academy VARCHAR(100) NOT NULL DEFAULT "default", # 用户所在学院 30 | address VARCHAR(100) NOT NULL DEFAULT "default", # 用户寝室地址 31 | token VARCHAR(100) NOT NULL DEFAULT "default", # 一个标识符,用来在用户自动登录时进行信息确定 32 | PRIMARY KEY(phone_number) 33 | )ENGINE=InnoDB DEFAULT CHARACTER SET utf8; 34 | 35 | -- 在本地测试时,可以先用以下代码插入一个用户,方便测试 36 | INSERT INTO qingong_users(phone_number, password, name, id_number, academy, address) 37 | VALUES('17826856666', '123456', '张三丰', '666666', '信息学院', '武当山'); 38 | 39 | 40 | -- 创建表 qingong_orders,存储用户每次的订单信息 41 | CREATE TABLE qingong_orders( 42 | order_id INT NOT NULL AUTO_INCREMENT, # 并无实际用处,仅作为主键 43 | phone_number VARCHAR(11) NOT NULL, # 用户名,手机号 44 | # 自动生成订单的日期 45 | order_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 46 | order_details VARCHAR(1000) NOT NULL, # 订单的细节,这里以 JSON 字符串进行存储 47 | FOREIGN KEY (phone_number) REFERENCES qingong_users(phone_number), 48 | PRIMARY KEY(order_id) 49 | )ENGINE=InnoDB AUTO_INCREMENT=20170001 DEFAULT CHARACTER SET utf8; 50 | 51 | 52 | -- 创建表 qingong_message,存储每次管理员更新的帮助信息 53 | CREATE TABLE qingong_message( 54 | message_id INT NOT NULL AUTO_INCREMENT, # 并无实际用处,仅作为主键 55 | message VARCHAR(5000) NOT NULL DEFAULT '暂无消息', # 帮助信息 56 | PRIMARY KEY(message_id) 57 | )ENGINE=InnoDB AUTO_INCREMENT=20170001 DEFAULT CHARACTER SET utf8; 58 | 59 | -------------------------------------------------------------------------------- /src/app.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 63 | 64 | -------------------------------------------------------------------------------- /server/backend/css/login.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | background: #03a9f4; 5 | } 6 | 7 | .container { 8 | box-sizing: border-box; 9 | height: 100%; 10 | padding-top: 40%; 11 | color: #FFF; 12 | } 13 | 14 | .container h1 { 15 | margin-bottom: 10%; 16 | text-align: center; 17 | } 18 | 19 | .form-item { 20 | box-sizing: border-box; 21 | display: flex; 22 | width: 80%; 23 | height: 44px; 24 | border: 2px solid #FFF; 25 | margin: 10px auto; 26 | overflow: hidden; 27 | } 28 | 29 | .form-item:nth-last-child(1) input { 30 | background: #FFF; 31 | color: #03a9f4; 32 | text-align: center; 33 | font-size: 1.6em; 34 | justify-content: center; 35 | } 36 | 37 | .form-item label { 38 | box-sizing: border-box; 39 | font-size: 1.2em; 40 | word-break: keep-all; 41 | font-weight: bolder; 42 | line-height: 40px; 43 | } 44 | 45 | .form-item label span { 46 | padding: 0 16px; 47 | } 48 | 49 | .form-item input { 50 | box-sizing: border-box; 51 | flex-grow: 1; 52 | height: 40px; 53 | border: none; 54 | padding: 3px 10px; 55 | } 56 | 57 | .form-item input:focus { 58 | outline: none; 59 | } 60 | 61 | .submit-close { 62 | pointer-events: none; 63 | } 64 | 65 | 66 | /* loading effects */ 67 | 68 | .loading { 69 | display: none; 70 | position: absolute; 71 | top: 0; 72 | width: 100%; 73 | height: 100%; 74 | z-index: 9; 75 | justify-content: center; 76 | align-items: center; 77 | background: rgba(222, 222, 222, 0.3); 78 | } 79 | 80 | .show-loading { 81 | display: flex; 82 | } 83 | 84 | .spinner { 85 | position: relative; 86 | width: 60px; 87 | height: 60px; 88 | } 89 | 90 | .double-bounce1, 91 | .double-bounce2 { 92 | width: 100%; 93 | height: 100%; 94 | border-radius: 50%; 95 | background-color: #333; 96 | opacity: 0.6; 97 | position: absolute; 98 | top: 0; 99 | left: 0; 100 | -webkit-animation: sk-bounce 2.0s infinite ease-in-out; 101 | animation: sk-bounce 2.0s infinite ease-in-out; 102 | } 103 | 104 | .double-bounce2 { 105 | -webkit-animation-delay: -1.0s; 106 | animation-delay: -1.0s; 107 | } 108 | 109 | @-webkit-keyframes sk-bounce { 110 | 0%, 111 | 100% { 112 | -webkit-transform: scale(0.3) 113 | } 114 | 50% { 115 | -webkit-transform: scale(1.0) 116 | } 117 | } 118 | 119 | @keyframes sk-bounce { 120 | 0%, 121 | 100% { 122 | transform: scale(0.3); 123 | -webkit-transform: scale(0.3); 124 | } 125 | 50% { 126 | transform: scale(1.0); 127 | -webkit-transform: scale(1.0); 128 | } 129 | } -------------------------------------------------------------------------------- /src/components/login-css/login-normal.css: -------------------------------------------------------------------------------- 1 | /* page layput 2 | **************************/ 3 | 4 | #login-container { 5 | position: fixed; 6 | z-index: 999; 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | .forget-password-page, 12 | .login-page, 13 | .signup-page, 14 | .userinfo-page { 15 | position: absolute; 16 | width: 100%; 17 | height: 100%; 18 | transition: 0.4s; 19 | } 20 | 21 | .forget-password-page, 22 | .signup-page, 23 | .userinfo-page { 24 | z-index: 666; 25 | } 26 | 27 | .forget-password-page { 28 | background: #F79D2D; 29 | transform: translateX(-100%); 30 | } 31 | 32 | .login-page { 33 | background: #03a9f4; 34 | } 35 | 36 | .signup-page { 37 | background: #B89E97; 38 | transform: translateX(100%); 39 | } 40 | 41 | .userinfo-page { 42 | background: #CF9B61; 43 | transform: translateY(100%); 44 | overflow: hidden; 45 | } 46 | 47 | .show-page { 48 | transform: translateY(0); 49 | transform: translateX(0); 50 | } 51 | 52 | 53 | /* other 54 | **************************************/ 55 | 56 | .form { 57 | position: relative; 58 | box-sizing: border-box; 59 | width: 100%; 60 | height: 100%; 61 | padding-top: 40%; 62 | color: #FFF; 63 | transition: 0.6s; 64 | } 65 | 66 | .form-up { 67 | transform: translateY(-120px); 68 | } 69 | 70 | .form h1 { 71 | margin-bottom: 10%; 72 | text-align: center; 73 | } 74 | 75 | .form-item { 76 | box-sizing: border-box; 77 | display: flex; 78 | width: 80%; 79 | height: 44px; 80 | border: 2px solid #FFF; 81 | margin: 10px auto; 82 | overflow: hidden; 83 | } 84 | 85 | ::-webkit-input-placeholder { 86 | /* WebKit, Blink, Edge */ 87 | color: #aaa !important; 88 | font-size: 14px !important; 89 | } 90 | 91 | .form-item input { 92 | box-sizing: border-box; 93 | flex-grow: 1; 94 | height: 40px; 95 | border: none; 96 | padding: 3px 10px; 97 | color: #888; 98 | font-size: 1.2em; 99 | } 100 | 101 | .form-item input:focus { 102 | outline: none; 103 | } 104 | 105 | .form-item:nth-last-child(1) { 106 | margin-top: 20px; 107 | border: none; 108 | } 109 | 110 | .form-item:nth-last-child(1) input { 111 | display: inline-block; 112 | background: #FFF; 113 | text-align: center; 114 | font-size: 1.4em; 115 | justify-content: center; 116 | } 117 | 118 | .form-item label { 119 | box-sizing: border-box; 120 | font-size: 1.2em; 121 | word-break: keep-all; 122 | font-weight: bolder; 123 | line-height: 40px; 124 | } 125 | 126 | .form-item label span { 127 | padding: 0 16px; 128 | } 129 | 130 | .submit-close { 131 | pointer-events: none; 132 | } -------------------------------------------------------------------------------- /server/backend/css/manage.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | .which-way { 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | .choose { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | width: 100%; 16 | height: 50%; 17 | color: #fff; 18 | font-size: 6em; 19 | font-weight: bold; 20 | } 21 | 22 | .choose-scan { 23 | background: #d11b70; 24 | } 25 | 26 | .choose-manual { 27 | background: #03a9f4; 28 | } 29 | 30 | 31 | /* loading effects */ 32 | 33 | .loading { 34 | display: none; 35 | position: fixed; 36 | top: 0; 37 | width: 100%; 38 | height: 100%; 39 | z-index: 9; 40 | justify-content: center; 41 | align-items: center; 42 | background: rgba(222, 222, 222, 0.5); 43 | } 44 | 45 | .show-loading { 46 | display: flex; 47 | } 48 | 49 | .spinner { 50 | position: relative; 51 | width: 60px; 52 | height: 60px; 53 | } 54 | 55 | .double-bounce1, 56 | .double-bounce2 { 57 | width: 100%; 58 | height: 100%; 59 | border-radius: 50%; 60 | background-color: #333; 61 | opacity: 0.6; 62 | position: absolute; 63 | top: 0; 64 | left: 0; 65 | -webkit-animation: sk-bounce 2.0s infinite ease-in-out; 66 | animation: sk-bounce 2.0s infinite ease-in-out; 67 | } 68 | 69 | .double-bounce2 { 70 | -webkit-animation-delay: -1.0s; 71 | animation-delay: -1.0s; 72 | } 73 | 74 | @-webkit-keyframes sk-bounce { 75 | 0%, 76 | 100% { 77 | -webkit-transform: scale(0.3) 78 | } 79 | 50% { 80 | -webkit-transform: scale(1.0) 81 | } 82 | } 83 | 84 | @keyframes sk-bounce { 85 | 0%, 86 | 100% { 87 | transform: scale(0.3); 88 | -webkit-transform: scale(0.3); 89 | } 90 | 50% { 91 | transform: scale(1.0); 92 | -webkit-transform: scale(1.0); 93 | } 94 | } 95 | 96 | 97 | /* switch way */ 98 | 99 | .switch-way { 100 | display: flex; 101 | position: fixed; 102 | bottom: 0; 103 | width: 100%; 104 | height: 50px; 105 | background: #333; 106 | opacity: 0.5; 107 | transition: 0.5s; 108 | } 109 | 110 | .switch-way:hover { 111 | opacity: 1; 112 | } 113 | 114 | #scan { 115 | width: 50%; 116 | line-height: 50px; 117 | text-align: center; 118 | background: #d11b70; 119 | color: #fff; 120 | } 121 | 122 | #manual { 123 | width: 50%; 124 | line-height: 50px; 125 | text-align: center; 126 | background: #03a9f4; 127 | color: #fff; 128 | } 129 | 130 | .hidden { 131 | display: none; 132 | } -------------------------------------------------------------------------------- /src/components/menu/help.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 63 | 64 | 114 | -------------------------------------------------------------------------------- /server/backend/css/scan.css: -------------------------------------------------------------------------------- 1 | .scan-add-book { 2 | width: 100%; 3 | padding-bottom: 50px; 4 | } 5 | 6 | .scan-add-book>h1 { 7 | width: 100%; 8 | height: 50px; 9 | margin: 0; 10 | line-height: 50px; 11 | background: #d11b70; 12 | color: #FFF; 13 | font-size: 1.6em; 14 | font-weight: bold; 15 | text-align: center; 16 | } 17 | 18 | #scanner { 19 | padding-bottom: 30px; 20 | } 21 | 22 | #scanner video { 23 | display: block; 24 | width: 100%; 25 | height: auto; 26 | } 27 | 28 | #scanner canvas { 29 | display: none; 30 | } 31 | 32 | 33 | /* code holder */ 34 | 35 | #code-holder { 36 | padding: 0 30px; 37 | font-size: 1.2em; 38 | line-height: 1.6; 39 | } 40 | 41 | #code-holder span { 42 | color: #444; 43 | font-weight: bolder; 44 | } 45 | 46 | 47 | /* book info 48 | *****************************************/ 49 | 50 | .load-message { 51 | opacity: 0; 52 | width: 80%; 53 | margin: 0 auto; 54 | font-size: 1.2em; 55 | color: red; 56 | transition: 0.2s; 57 | } 58 | 59 | .show-message { 60 | opacity: 1; 61 | } 62 | 63 | #book-info { 64 | color: #666; 65 | } 66 | 67 | #book-info h1 { 68 | text-align: center; 69 | color: #333; 70 | font-size: 1.4em; 71 | } 72 | 73 | .book-title, 74 | .book-author { 75 | padding: 0 30px; 76 | font-size: 1.2em; 77 | line-height: 1.6; 78 | } 79 | 80 | .book-title span, 81 | .book-author span { 82 | color: #444; 83 | font-weight: bolder; 84 | } 85 | 86 | .book-image img { 87 | display: block; 88 | width: 90%; 89 | margin: 0 auto; 90 | } 91 | 92 | .add-to-db { 93 | position: relative; 94 | box-sizing: border-box; 95 | width: 100%; 96 | height: 50px; 97 | padding: 10px 20px; 98 | margin: 30px 0; 99 | line-height: 30px; 100 | } 101 | 102 | .add-to-db label { 103 | font-size: 1.4em; 104 | font-weight: bold; 105 | color: #444; 106 | } 107 | 108 | #add-number { 109 | box-sizing: border-box; 110 | width: 50px; 111 | height: 30px; 112 | padding: 5px 10px; 113 | } 114 | 115 | #select-kind { 116 | box-sizing: border-box; 117 | position: absolute; 118 | right: 20px; 119 | width: 100px; 120 | height: 30px; 121 | } 122 | 123 | .submit { 124 | position: relative; 125 | box-sizing: border-box; 126 | width: 100%; 127 | height: 50px; 128 | padding: 10px 20px; 129 | margin: 0 0 30px; 130 | line-height: 30px; 131 | } 132 | 133 | .submit-data { 134 | box-sizing: border-box; 135 | position: absolute; 136 | width: 70px; 137 | height: 30px; 138 | border: none; 139 | } 140 | 141 | #submit-in { 142 | left: 60px; 143 | } 144 | 145 | #submit-out { 146 | right: 60px; 147 | } 148 | 149 | .submit-close { 150 | pointer-events: none; 151 | } -------------------------------------------------------------------------------- /additional.md: -------------------------------------------------------------------------------- 1 | 2 | > 下面是一些前后端数据交互时比较重要的一些接口 3 | 4 | ### login.vue 5 | 6 | ```json 7 | // login 8 | { 9 | "phone_number": "", 10 | "password": "" 11 | } 12 | ``` 13 | 14 | ```json 15 | // signup 16 | { 17 | "phone_number": "", 18 | "password": "", 19 | "auth_code": "" // 验证码 20 | } 21 | ``` 22 | 23 | ```json 24 | // forget_password 25 | { 26 | "phone_number": "", 27 | "password": "", 28 | "auth_code": "" 29 | } 30 | ``` 31 | 32 | ```json 33 | // userinfo 34 | { 35 | "phone_number": "", // 手机号 36 | "name": "", // 姓名 37 | "id_number":"", // 学号 38 | "academy":"", // 学院 39 | "address":"" // 地址 40 | } 41 | ``` 42 | 43 | ### book-list.vue 44 | 45 | ```json 46 | { 47 | "phone_number": "", // 用户名 48 | "order_details": "" // 订单详细信息(一个 JSON 数组) 49 | } 50 | ``` 51 | 52 | ### content.vue 53 | 54 | ```json 55 | // queryObj 56 | { 57 | "user_state": "", // not_login or login_success 58 | "type": "home", // 记录请求时所在的分类 type: home(default)、search 59 | "data_num": 10, // 一次请求返回的数据个数 60 | "request_count": 0, // 记录请求的次数,方便实现下滑继续加载 61 | "keyword": "", // 搜索关键字(only for type='search') 62 | "academy": "" // 用户所在学院(only for user_state="login_success") 63 | } 64 | ``` 65 | 66 | ### scan.js || manual.js 67 | 68 | ```txt 69 | { 70 | "flag": "in-scan", // 记录操作的方式 71 | // flag: in-scan、out-scan、in-manual、out-manual 72 | "kind": "", // 书的分类 73 | "title": "", // 书名 74 | "author": "", // 作者 75 | "imageSrc || imageFile": "", // 封面图片(链接或文件) 76 | "number": 10 // 书的数量 77 | } 78 | ``` 79 | 80 | ### operate.php 81 | 82 | ```php 83 | $way // 'scan' || 'manual' 84 | $flag // 'in' || 'out' 85 | $type // 书的分类 86 | $title // 书名 87 | $author // 作者 88 | $imageSrc // 封面图片链接 89 | $number // 书籍数量 90 | ``` 91 | 92 | ```php 93 | // 检测数据库中是否存在指定书籍(使用书名和作者名进行检测书籍的唯一性) 94 | 95 | if($flag == 'in'){ 96 | if($way == 'scan'){ 97 | if('书籍存在'){ 98 | // 只更新书籍数量 99 | } 100 | if('书籍不存在'){ 101 | // 插入新的书籍(INSERT INTO...) 102 | } 103 | } 104 | if($way == 'manual'){ 105 | if('书籍存在'){ 106 | // 处理上传的图片 107 | // 更新书籍所有信息 108 | } 109 | if('书籍不存在'){ 110 | // 处理上传的图片 111 | // 插入新的书籍(INSERT INTO...) 112 | } 113 | } 114 | } 115 | 116 | if($flag == 'out'){ 117 | if($way == 'scan'){ 118 | if('书籍存在'){ 119 | // 只更新书籍数量 120 | // 注意对书籍数量进行边界值检测 121 | } 122 | if('书籍不存在'){ 123 | // 报错 124 | } 125 | } 126 | if($way == 'manual'){ 127 | if('书籍存在'){ 128 | // 只更新书籍数量 129 | // 注意对书籍数量进行边界值检测 130 | } 131 | if('书籍不存在'){ 132 | // 报错 133 | } 134 | } 135 | } 136 | 137 | ``` -------------------------------------------------------------------------------- /server/login/login.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 41 | 42 | set_charset("utf8"); 54 | 55 | // 数据正确,检测 $user 的唯一性,即用户是否存在 56 | $sql = "SELECT phone_number FROM $table_name WHERE phone_number = '$user'"; 57 | $result = $mysqli->query($sql); 58 | 59 | if ($result->num_rows == 0) { 60 | echo 'not_found'; 61 | $mysqli->close(); 62 | exit(); 63 | } 64 | 65 | if (isset($_POST['token'])) { 66 | // 若用户存在,则检测 token 是否一致 67 | $sql = "SELECT * FROM $table_name WHERE phone_number = '$user' AND token = '$token'"; 68 | 69 | $result = $mysqli->query($sql); 70 | 71 | if ($result->num_rows == 0) { 72 | echo $sql; 73 | echo 'wrong_token'; 74 | $mysqli->close(); 75 | exit(); 76 | } 77 | } else { 78 | // 若用户存在,则检测密码是否正确 79 | $sql = "SELECT phone_number,password FROM $table_name WHERE phone_number = '$user' AND password = '$user_password'"; 80 | 81 | $result = $mysqli->query($sql); 82 | 83 | if ($result->num_rows == 0) { 84 | echo 'wrong_password'; 85 | $mysqli->close(); 86 | exit(); 87 | } 88 | } 89 | 90 | // 用户名、密码都正确,再检测用户的其它信息是否已完善 91 | $sql = "SELECT name FROM $table_name WHERE phone_number = '$user'"; 92 | 93 | $result = $mysqli->query($sql); 94 | $row = $result->fetch_row(); 95 | 96 | if ($row[0] === "default") { 97 | echo 'should_update_userinfo|'; 98 | echo "$user"; 99 | 100 | $mysqli->close(); 101 | exit(); 102 | } else { 103 | echo 'login_success|'; 104 | 105 | if (!isset($_POST['token'])) { 106 | // 获取时间戳,作为 token 107 | $token = (string)time(); 108 | echo "$token|"; 109 | 110 | // 将 token 存入数据库,方便下次自动登入时进行比较 111 | $sql = "UPDATE $table_name SET token = '$token' WHERE phone_number = '$user'"; 112 | $mysqli->query($sql); 113 | } 114 | 115 | // 将用户数据返回给前端 116 | $sql = "SELECT phone_number,name,id_number,academy,address FROM $table_name WHERE phone_number = '$user'"; 117 | 118 | $result = $mysqli->query($sql); 119 | 120 | if ($result->num_rows > 0) { 121 | $row = $result->fetch_row(); 122 | echo json_encode($row, 256); 123 | } 124 | 125 | $mysqli->close(); 126 | exit(); 127 | } 128 | -------------------------------------------------------------------------------- /server/backend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 后台登录界面 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

登录后台

16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | 42 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/components/content/book-card.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 89 | 90 | -------------------------------------------------------------------------------- /server/backend/js/manual.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | let bookTitle = document.getElementById('book-title'); 3 | let bookAuthor = document.getElementById('book-author'); 4 | let selectKind = document.getElementById('select-kind'); 5 | let bookTags = document.getElementById('book-tags'); 6 | let bookNumber = document.getElementById('book-number'); 7 | let uploadImg = document.getElementById('upload-img'); 8 | 9 | uploadImg.onchange = function() { 10 | uploadImg.previousElementSibling.innerHTML = '已选中 ' + uploadImg.files.length + ' 个文件'; 11 | }; 12 | 13 | // 提交数据到后台 14 | let submitIn = document.getElementById('submit-in'); 15 | let submitOut = document.getElementById('submit-out'); 16 | 17 | let loading = document.querySelector('.loading'); 18 | 19 | submitIn.onclick = function() { 20 | let flag = 'in-manual'; 21 | 22 | if (!detectData(flag)) { 23 | return; 24 | } 25 | 26 | loading.classList.add('show-loading'); 27 | submitIn.classList.add('submit-close'); 28 | 29 | sendData(flag).then(function(result) { 30 | alert(result.trim()); 31 | loading.classList.remove('show-loading'); 32 | submitIn.classList.remove('submit-close'); 33 | }).catch(function() { 34 | alert('录入数据失败~'); 35 | }); 36 | }; 37 | 38 | submitOut.onclick = function() { 39 | let flag = 'out-manual'; 40 | 41 | if (!detectData(flag)) { 42 | return; 43 | } 44 | 45 | loading.classList.add('show-loading'); 46 | submitOut.classList.add('submit-close'); 47 | 48 | sendData(flag).then(function(result) { 49 | alert(result.trim()); 50 | loading.classList.remove('show-loading'); 51 | submitOut.classList.remove('submit-close'); 52 | }).catch(function() { 53 | alert('录出数据失败~'); 54 | }); 55 | }; 56 | 57 | // 验证数据 58 | function detectData(flag) { 59 | let isRight = true; 60 | 61 | if (!bookTitle.value) { 62 | alert('请输入书名~'); 63 | isRight = false; 64 | } else if (!bookAuthor.value) { 65 | alert('请输入作者~'); 66 | isRight = false; 67 | } else if (!bookNumber.value.match(/^[1-9][0-9]*$/)) { 68 | alert('请输入有效的数量~'); 69 | isRight = false; 70 | } 71 | 72 | // 如果是录出模式,则不需要上传图片 73 | if (flag.match(/^in/) && isRight) { 74 | // 检测是否上传封面图片 75 | if (uploadImg.files.length != 1) { 76 | alert('请上传一张封面图片~'); 77 | isRight = false; 78 | } 79 | 80 | // 客户端检测文件类型是否为图片 81 | if (!uploadImg.files[0].type.match(/image.*/)) { 82 | alert('请上传有效的图片类型~'); 83 | isRight = false; 84 | } 85 | 86 | // 限制上传图片的大小(小于 200KB) 87 | if (uploadImg.files[0].size > 200000) { 88 | alert('请上传一张小于 200KB 的图片~'); 89 | isRight = false; 90 | } 91 | } 92 | 93 | return isRight; 94 | } 95 | 96 | // 利用 Ajax 向服务器提交数据 97 | function sendData(flag) { 98 | return new Promise(function(resolve, reject) { 99 | let url = "./operate.php"; 100 | let request = new XMLHttpRequest(); 101 | 102 | request.onload = function() { 103 | resolve(this.responseText); 104 | }; 105 | 106 | request.onerror = reject; 107 | 108 | var bookData = new FormData(); 109 | 110 | bookData.append('flag', flag); 111 | bookData.append('kind', selectKind.value.trim()); 112 | bookData.append('title', bookTitle.value.trim()); 113 | bookData.append('author', bookAuthor.value.trim()); 114 | bookData.append('imageFile', uploadImg.files[0]); 115 | bookData.append('number', bookNumber.value); 116 | bookData.append('tags', bookTags.value.trim()); 117 | 118 | request.open("POST", url, true); // 初始化一个请求 119 | request.send(bookData); // 发送请求 120 | }); 121 | } 122 | })(); -------------------------------------------------------------------------------- /src/components/header.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 81 | 82 | -------------------------------------------------------------------------------- /server/backend/manage.php: -------------------------------------------------------------------------------- 1 | '; 4 | echo '

0.0

'; 5 | echo '

Please login first~

'; 6 | exit; 7 | } 8 | ?> 9 | 10 | 14 | 15 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 录入||录出 书籍 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 |
50 |
51 |
52 |
53 |
54 | 55 |
56 |
扫 码
57 |
手 动
58 |
59 | 60 | 68 | 69 | 123 | 127 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue-book 2.0 2 | 3 | ## 说明 4 | 5 | * **前端: Vue.js + Vuex + Webpack2** 6 | * **后端: php + MySQL** 7 | 8 | > 要是你对 PHP 和 MySQL 没啥基础,可以逛逛[**我的博客**](http://blog.percymong.com/archives/),有几篇文章是用来介绍这些基础的。 9 | 10 | 本项目是一个简单的全栈项目,前端新手可以拿来练练手。 11 | 12 | 项目实现了一些简单的功能,后台可以对图书进行录入录出(扫码或手动),前台显示录入的图书,并且前台注册登录后可以将书的订单发给服务器,并存到服务器。具体请看下面的实现逻辑图。 13 | 14 | ![logic](https://github.com/percy507/vue-book/blob/master/_image/simple-logic.png) 15 | 16 | 我在自己的服务器上把这个项目搭建好了,但是,目前不便给出登录后台的链接,只给出前台的链接,本项目只针对移动端,所以最好在手机上查看链接 ^_^ 17 | 18 | ## Demo && 演示 19 | 20 | > 由于我没有继续给主机空间续费,所以被停用了,所以没发进行线上演示了 21 | 22 | --- 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ## Build Setup 34 | 35 | ``` bash 36 | # install dependencies 37 | npm install 38 | 39 | # serve with hot reload at localhost:8080 40 | npm run dev 41 | 42 | # build for production with minification 43 | npm run build 44 | ``` 45 | 46 | > 我在本地测试用的服务器是 [WAMP Server](http://www.wampserver.com/en/)。 47 | 48 | 为了方便大家阅读源码,我列出了前后端数据交互时比较重要一些的接口,方便大家进行参考![(点我查看)](additional.md) 49 | 50 | ## 项目目录说明 51 | 52 | ```bash 53 | Vue-book directory 54 | | 55 | ├── server # 存放服务端操作的文件夹 56 | | ├── backend 57 | | ├── css # 存放后台样式文件 58 | | ├── login.css # 登录后台页面的样式 59 | | ├── manage.css # 后台操作页面的一部分样式 60 | | ├── manual.css # 后台手动操作的样式 61 | | └── scan.css # 后台扫码操作的样式 62 | | ├── js 63 | | ├── manage.js # 进入管理界面的效果脚本 64 | | ├── manual.js # 后台手动操作的脚本 65 | | └── scan.js # 后台扫码操作的脚本 66 | | ├── index.html # 后台登录页面 67 | | ├── manage.php # 登录后台成功后返回的管理页面 68 | | ├── message.php # 后台更改前台公告的脚本 69 | | └── operate.php # 定义后台操作与数据库交互的逻辑 70 | | ├── login 71 | | ├── yunpian-sdk-php # 存放云片网的 SDK(外包短信服务) 72 | | ├── forget-password.php # 忘记密码时的后台脚本 73 | | ├── get-auth-code.php # 获取验证码时的后台脚本 74 | | ├── login.php # 前台登录时的后台验证脚本 75 | | ├── signup.php # 注册时的后台脚本 76 | | └── update-userinfo.php # 完善或更新个人信息时的后台脚本 77 | | ├── database_details.sql # 数据库表的定义 78 | | ├── randomword.js # 生成指定数量随机数据的脚本(测试时可用) 79 | | ├── get-help-message.php # 前端获取公告时的后端脚本 80 | | ├── getdata.php # 前端获取书籍时的后端脚本 81 | | └── submit-order.php # 前端提交书单(订单)的后端脚本 82 | ├── src # 存放前端源码 83 | | ├── common 84 | | ├── error.png # 图片加载失败时默认显示的图片 85 | | ├── fullscreen.js # 全屏显示脚本 86 | | ├── getdata.js # Ajax GET 获取数据脚本 87 | | ├── loading.gif # 图片加载中时默认显示的图片 88 | | └── senddata.js # Ajax POST 发送数据脚本 89 | | ├── components # 盛放各种组件 90 | | ├── content 91 | | ├── book-card.vue # 书籍详细信息 92 | | ├── book-item.vue # 书籍简要信息 93 | | └── content.vue # 内容块 94 | | ├── menu 95 | | ├── book-list.vue # 我的书单 96 | | ├── help.vue # 帮助 97 | | ├── menu.vue # 菜单 98 | | └── setting.vue # 设置 99 | | ├── login-css # 定义前台登录界面的 css 100 | | ├── login-forget-password.css 101 | | ├── login-login.css 102 | | ├── login-normal.css 103 | | ├── login-signup.css 104 | | └── login-userinfo.css 105 | | ├── login.vue # 前台登录 106 | | ├── container.vue # 大包含块 107 | | ├── header.vue # 页面头 108 | | ├── loading.vue # 载入中 109 | | └── overlay.vue # 覆盖层(显示侧边栏时出现) 110 | | ├── router 111 | | └── routes.js # 路由(好吧,好像我没怎么用) 112 | | └── vuex 113 | | └── store.js # Vuex 状态管理 114 | | ├── app.vue 115 | | ├── main.js # 程序入口文件 116 | ├── additional.md # 前后数据交互接口简要说明文件 117 | ├── index.html 118 | ├── package.json # 程序的相关依赖 119 | ├── README.md 120 | └── webpack.config.js # Webpack 配置相关信息 121 | ``` 122 | 123 | ## 实现的功能 124 | 125 | * 前台用户手机验证码注册、登录以及忘记密码 126 | * 前台数据图片懒加载 127 | * 前台向后台请求数据时有数量限定(比如一次返回 20 条数据) 128 | * 搜索功能 129 | * sessionStorage 实现我的书单功能(类似购物车) 130 | * 使用时间戳以及 cookie 实现一小时内自动登录 131 | * 增加全屏显示菜单(因为项目在微信上用,所以全屏显示的代码先被注释掉了) 132 | * 扫条形码录入录出书籍(书籍信息基于豆瓣书籍 API) 133 | * 手动录入录出书籍 134 | * 后台登录更改公告信息 135 | 136 | ## 未解决问题 137 | 138 | * 切换内容页面时,自动滚动到内容最顶部(content.vue) 139 | * 退出页面时提示(浏览器上可以监听 beforeunload 事件,但是微信上不行) 140 | 141 | ## 心得与遗憾 142 | 143 | * 要是在写代码之前先认认真真地把项目各个模块的流程图(或逻辑流程图)先画出来的话,感觉写代码效率会大大提高。(或者说写代码之前先把产品整体的构思与架构先画个图表示出来) 144 | * 遗憾是,项目虽然引入了 vue-router,但是基本上没用到,整个页面都是基于事件开发出来的,没有路由,那就下个项目再用 vue-router 吧 ~ 145 | 146 | ## Licence 147 | 148 | MIT Licence 149 | -------------------------------------------------------------------------------- /server/backend/message.php: -------------------------------------------------------------------------------- 1 | 更新公告
确认更新
'; 16 | } else { 17 | echo "wrong"; 18 | } 19 | } elseif (isset($_POST['textarea'])) { 20 | $text = $_POST['textarea']; 21 | 22 | // 验证数据的合法性 23 | if (preg_match("/(select)|(delete)|(create)|(update)|(like)|(drop)/i", $text)) { 24 | echo "出现了非法关键词,请重新输入 ~"; 25 | exit(); 26 | }; 27 | 28 | // 连接数据库 29 | $servername = "p:localhost"; 30 | $username = "root"; 31 | $password = ""; 32 | $db_name = "qingong_db"; 33 | $table_name = "qingong_message"; 34 | 35 | // 创建与数据库系统的连接 36 | $mysqli = new mysqli($servername, $username, $password, $db_name); 37 | 38 | // 设置默认的客户端字符集 39 | $mysqli->set_charset("utf8"); 40 | 41 | $sql = "INSERT INTO $table_name(message) VALUES('$text')"; 42 | 43 | if ($mysqli->query($sql)) { 44 | echo "success"; 45 | $mysqli->close(); 46 | exit(); 47 | } else { 48 | echo "未知错误 ~"; 49 | $mysqli->close(); 50 | exit(); 51 | } 52 | } else { 53 | ?> 54 | 55 | 56 | 57 | 58 | 公告更新 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |
67 |
68 |

公告更新

69 |
70 | 71 | 72 |
73 |
74 | 75 | 76 |
77 |
78 | 79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | 96 | 97 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /server/backend/operate.php: -------------------------------------------------------------------------------- 1 | 53 | 54 | 66 | 67 | set_charset("utf8"); 80 | 81 | // 检测数据库中是否存在指定书籍(使用书名和作者名进行检测书籍的唯一性) 82 | $sql = "SELECT * FROM $table_name WHERE book_title='$title' AND book_author='$author'"; 83 | $result = $mysqli->query($sql); 84 | if ($result->num_rows == 0) { 85 | if ($flag == "out") { 86 | echo "数据库中还没有这本书呢,快去录入吧~"; 87 | $mysqli->close(); 88 | exit(); 89 | } else { 90 | $isBookExist = false; 91 | } 92 | } else { 93 | $isBookExist = true; 94 | 95 | if ($flag == "out") { 96 | // 若书籍存在,获取其剩余数量,方便录出书籍时使用 97 | $sql = "SELECT rest_number FROM $table_name WHERE book_title='$title' AND book_author='$author'"; 98 | $result = $mysqli->query($sql); 99 | $row = $result->fetch_row(); 100 | $rest_number = $row[0]; 101 | } 102 | } 103 | 104 | 105 | // 处理 $way='scan' 的逻辑 106 | if ($way == 'scan') { 107 | if ($flag == 'in') { 108 | if ($isBookExist) { 109 | // 若书籍存在,那么扫码录入仅更新 rest_number 110 | $sql = "UPDATE $table_name SET rest_number=rest_number+$number WHERE book_title='$title' AND book_author='$author'"; 111 | 112 | if ($mysqli->query($sql)) { 113 | echo "录入数据成功~"; 114 | $mysqli->close(); 115 | exit(); 116 | } 117 | } else { 118 | // 若书籍不存在,那么插入书籍的所有信息 119 | $needed_prop = "book_title,book_author,book_type,book_cover,rest_number,book_tags"; 120 | $sql = "INSERT INTO $table_name($needed_prop) VALUES('$title','$author','$type','$imageSrc',$number,'$tags')"; 121 | 122 | if ($mysqli->query($sql)) { 123 | echo "录入数据成功~"; 124 | $mysqli->close(); 125 | exit(); 126 | } 127 | } 128 | } elseif ($flag == 'out') { 129 | if ($isBookExist) { 130 | if ($number < $rest_number) { 131 | // 若录出书籍小于剩余书籍,则正常录出 132 | $sql = "UPDATE $table_name SET rest_number=rest_number-$number WHERE book_title='$title' AND book_author='$author'"; 133 | } elseif ($number == $rest_number) { 134 | // 若录处书籍正好等于剩余书籍,则从数据库中删除该书籍 135 | $sql = "DELETE FROM $table_name WHERE book_title='$title' AND book_author='$author'"; 136 | } else { 137 | // 这种情况下是录出书籍大于剩余书籍,报错 138 | echo "录出参数错误,仓库没有那么多书啦~"; 139 | $mysqli->close(); 140 | exit(); 141 | } 142 | 143 | if ($mysqli->query($sql)) { 144 | echo "录出数据成功~"; 145 | $mysqli->close(); 146 | exit(); 147 | } 148 | } 149 | } 150 | } 151 | 152 | // 处理 $way='manual' 的逻辑 153 | if ($way == 'manual') { 154 | if ($flag == 'in') { 155 | if ($isBookExist) { 156 | // 若书籍存在,那么手动录入将更新书籍的全部信息 157 | $updated_prop = "book_title='$title',book_author='$author',book_type='$type',book_cover='$imageSrc',rest_number=rest_number+$number"; 158 | $sql = "UPDATE $table_name SET $updated_prop WHERE book_title='$title' AND book_author='$author'"; 159 | 160 | if ($mysqli->query($sql)) { 161 | echo "录入数据成功~"; 162 | $mysqli->close(); 163 | exit(); 164 | } 165 | } else { 166 | // 若书籍不存在,那么插入书籍的所有信息 167 | $needed_prop = "book_title,book_author,book_type,book_cover,rest_number,book_tags"; 168 | $sql = "INSERT INTO $table_name($needed_prop) VALUES('$title','$author','$type','$imageSrc',$number,'$tags')"; 169 | 170 | if ($mysqli->query($sql)) { 171 | echo "录入数据成功~"; 172 | $mysqli->close(); 173 | exit(); 174 | } 175 | } 176 | } elseif ($flag == 'out') { 177 | if ($isBookExist) { 178 | if ($number < $rest_number) { 179 | // 若录出书籍小于剩余书籍,则正常录出 180 | $sql = "UPDATE $table_name SET rest_number=rest_number-$number WHERE book_title='$title' AND book_author='$author'"; 181 | } elseif ($number == $rest_number) { 182 | // 若录处书籍正好等于剩余书籍 183 | // 删除上传的书籍封面图片 184 | $sql = "SELECT book_cover FROM $table_name WHERE book_title='$title' AND book_author='$author'"; 185 | $result = $mysqli->query($sql); 186 | $row = $result->fetch_row(); 187 | $imgPath = $row[0]; 188 | if (file_exists($imgPath)) { 189 | unlink($imgPath); 190 | } 191 | 192 | // 从数据库中删除该书籍 193 | $sql = "DELETE FROM $table_name WHERE book_title='$title' AND book_author='$author'"; 194 | } else { 195 | // 这种情况下是录出书籍大于剩余书籍,报错 196 | echo "录出参数错误,仓库没有那么多书啦~"; 197 | $mysqli->close(); 198 | exit(); 199 | } 200 | 201 | if ($mysqli->query($sql)) { 202 | echo "录出数据成功~"; 203 | $mysqli->close(); 204 | exit(); 205 | } 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/components/menu/setting.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 199 | 200 | 201 | 246 | -------------------------------------------------------------------------------- /server/backend/js/scan.js: -------------------------------------------------------------------------------- 1 | // 检测 scanner 是否已经在运行 2 | let isScanning = false; 3 | 4 | // 开始扫码 5 | function startScan() { 6 | if (isScanning) { 7 | // 去掉之前的 scanner 监听事件 8 | Quagga.offDetected(searchISBN); 9 | Quagga.stop(); 10 | } 11 | 12 | // 检测摄像头,并获取摄像头的 ID 值 13 | let cameraArr = []; 14 | 15 | if (navigator.mediaDevices.enumerateDevices) { 16 | navigator.mediaDevices.enumerateDevices().then(function(devices) { 17 | devices.forEach(function(device) { 18 | if (device.kind == 'videoinput') { 19 | cameraArr.push(device.deviceId); 20 | } 21 | }); 22 | 23 | initscan(cameraArr[cameraArr.length - 1]); 24 | }).catch(function(err) { 25 | alert(err.name + ": " + err.message); 26 | }); 27 | } else { 28 | alert('你的浏览器太旧了,赶快换成最新的 Google 浏览器吧 ~'); 29 | return; 30 | } 31 | } 32 | 33 | 34 | /* 35 | * 因为服务器返回的数据是全局的, 36 | * 所以必须将 JSONP 回调函数定义为全局函数 37 | */ 38 | let responseHandler; 39 | 40 | var loading = document.querySelector('.loading'); 41 | 42 | 43 | // 扫条形码 44 | function initscan(cameraID) { 45 | Quagga.init({ 46 | inputStream: { 47 | name: "Live", 48 | type: "LiveStream", 49 | constraints: { 50 | width: 1280, 51 | height: 720, 52 | facingMode: "environment", 53 | deviceId: cameraID 54 | }, 55 | target: document.querySelector('#scanner') 56 | }, 57 | frequency: 5, 58 | decoder: { 59 | readers: ["ean_reader"] 60 | } 61 | }, function(err) { 62 | if (err) { 63 | alert(err); 64 | return; 65 | } 66 | console.log("Initialization finished. Ready to start"); 67 | Quagga.start(); 68 | isScanning = true; 69 | Quagga.onDetected(searchISBN); 70 | }); 71 | } 72 | 73 | 74 | // 利用 JSONP 从豆瓣跨域获取指定 ISBN 码的书籍信息 75 | function searchISBN(data) { 76 | let code = data.codeResult.code; 77 | let codeHolder = document.getElementById('code-holder'); 78 | 79 | codeHolder.innerHTML = 'ISBN 码:' + code; 80 | Quagga.stop(); 81 | 82 | let bookInfo = document.getElementById('book-info'); 83 | let loadMessage = bookInfo.firstElementChild; 84 | loadMessage.classList.add('show-message'); 85 | 86 | loading.classList.add('show-loading'); 87 | 88 | // 通过 JSONP 跨域取得数据,并进行处理 89 | let url = 'https://api.douban.com/v2/book/isbn/:' + code; 90 | getJSONP(url).then(function(result) { 91 | loading.classList.remove('show-loading'); 92 | 93 | useData(result); 94 | }).catch(function() { 95 | loading.classList.remove('show-loading'); 96 | bookInfo.innerHTML = '

很抱歉,在豆瓣 API 中没有找到相关书籍,你可以选择重试或手动录入书籍~

'; 97 | }); 98 | } 99 | 100 | // 录入或录出书籍 101 | function useData(json) { 102 | let title = json.title; 103 | let author = json.author.join('/'); 104 | let imageSrc = json.images.large; 105 | let tags = ''; 106 | let bookInfo = document.getElementById('book-info'); 107 | 108 | json.tags.forEach(function(item) { 109 | tags += item.name + '|'; 110 | }); 111 | tags = tags.slice(0, -1); 112 | 113 | /* 114 | * 因为豆瓣返回的某些书籍的数据有时会不完整 115 | * 所以在这里进行一些简单的检测和处理 116 | */ 117 | if (imageSrc) { 118 | imagSrc = json.image; 119 | } 120 | if (!title || !author || !imageSrc || imageSrc.match(/book-default/g)) { 121 | bookInfo.innerHTML = '

很抱歉,在豆瓣 API 中没有获取到书籍的完整信息,请手动录入书籍~

'; 122 | return; 123 | } 124 | 125 | // 若书籍信息完整,则直接显示于页面上 126 | bookInfo.innerHTML = ` 127 |

书籍信息

128 |
书名:${title}
129 |
作者:${author}
130 |
131 |
132 | 133 | 152 |
153 |
`; 154 | 155 | let dataObj = { 156 | title: title, 157 | author: author, 158 | imageSrc: imageSrc, 159 | tags: tags 160 | }; 161 | 162 | // 提交数据到后台 163 | let submitIn = document.getElementById('submit-in'); 164 | let submitOut = document.getElementById('submit-out'); 165 | 166 | submitIn.onclick = function() { 167 | loading.classList.add('show-loading'); 168 | submitIn.classList.add('submit-close'); 169 | 170 | sendData('in-scan', dataObj).then(function(result) { 171 | alert(result.trim()); 172 | loading.classList.remove('show-loading'); 173 | submitIn.classList.remove('submit-close'); 174 | }).catch(function() { 175 | loading.classList.remove('show-loading'); 176 | submitIn.classList.remove('submit-close'); 177 | alert('录入数据失败~'); 178 | }); 179 | }; 180 | 181 | submitOut.onclick = function() { 182 | loading.classList.add('show-loading'); 183 | submitOut.classList.add('submit-close'); 184 | 185 | sendData('out-scan', dataObj).then(function(result) { 186 | alert(result.trim()); 187 | loading.classList.remove('show-loading'); 188 | submitOut.classList.remove('submit-close'); 189 | }).catch(function() { 190 | loading.classList.remove('show-loading'); 191 | submitOut.classList.remove('submit-close'); 192 | alert('录出数据失败~'); 193 | }); 194 | }; 195 | 196 | // 利用 Ajax 向服务器提交数据 197 | function sendData(flag, dataObj) { 198 | return new Promise(function(resolve, reject) { 199 | let url = "./operate.php"; 200 | let request = new XMLHttpRequest(); 201 | 202 | request.onload = function() { 203 | resolve(this.responseText); 204 | }; 205 | 206 | request.onerror = reject; 207 | 208 | let bookData = new FormData(); 209 | 210 | let number = document.getElementById('add-number').value; 211 | 212 | if (!number.match(/^[1-9][0-9]*$/)) { 213 | alert('请输入有效的数量 ~'); 214 | 215 | let submitIn = document.getElementById('submit-in'); 216 | let submitOut = document.getElementById('submit-out'); 217 | loading.classList.remove('show-loading'); 218 | submitIn.classList.remove('submit-close'); 219 | submitOut.classList.remove('submit-close'); 220 | return; 221 | } 222 | 223 | let title = dataObj.title; 224 | let author = dataObj.author; 225 | let imageSrc = dataObj.imageSrc; 226 | let kind = document.getElementById('select-kind').value; 227 | let tags = dataObj.tags; 228 | 229 | bookData.append('flag', flag.trim()); 230 | bookData.append('kind', kind.trim()); 231 | bookData.append('title', title.trim()); 232 | bookData.append('author', author.trim()); 233 | bookData.append('imageSrc', imageSrc.trim()); 234 | bookData.append('number', number); 235 | bookData.append('tags', tags.trim()); 236 | 237 | request.open("POST", url, true); // 初始化一个请求 238 | request.send(bookData); // 发送请求 239 | }); 240 | } 241 | 242 | } 243 | 244 | 245 | 246 | // JSONP 247 | function getJSONP(url) { 248 | return new Promise(function(resolve, reject) { 249 | if (url.indexOf('?') === -1) { 250 | url += '?callback=responseHandler'; 251 | } else { 252 | url += '&callback=responseHandler'; 253 | } 254 | 255 | // 使用 JSONP 实现跨域请求 256 | let script = document.createElement('script'); 257 | script.setAttribute('src', url); 258 | document.body.appendChild(script); 259 | 260 | script.onerror = reject; 261 | // 处理回调函数 262 | responseHandler = function(json) { 263 | try { 264 | resolve(json); 265 | } finally { 266 | // 函数调用之后,移除对应的标签 267 | script.parentNode.removeChild(script); 268 | } 269 | }; 270 | }); 271 | } -------------------------------------------------------------------------------- /src/components/content/content.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 292 | 293 | -------------------------------------------------------------------------------- /src/components/menu/menu.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 166 | 167 | -------------------------------------------------------------------------------- /src/components/menu/book-list.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 245 | 246 | 425 | 426 | -------------------------------------------------------------------------------- /src/components/login.vue: -------------------------------------------------------------------------------- 1 | 241 | 242 | 666 | 667 | --------------------------------------------------------------------------------