├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── images ├── download.png ├── home.png ├── info.jpg ├── school.jpg └── upload.png ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── api │ ├── api.js │ ├── axios.js │ └── upyun.js ├── assets │ ├── logo.png │ └── reset.css ├── components │ ├── business │ │ ├── download │ │ │ ├── card.vue │ │ │ ├── files.vue │ │ │ └── preview.vue │ │ ├── task │ │ │ ├── addTask.vue │ │ │ ├── index.vue │ │ │ ├── taskDetail.vue │ │ │ ├── taskItem.vue │ │ │ └── taskList.vue │ │ └── todoList │ │ │ ├── todoList.vue │ │ │ └── upload.vue │ ├── contact │ │ ├── groupEdit.vue │ │ ├── groupItem.vue │ │ ├── groupList.vue │ │ ├── groupUserList.vue │ │ ├── index.vue │ │ ├── userEdit.vue │ │ └── userList.vue │ ├── home │ │ ├── explore.vue │ │ ├── home.vue │ │ └── info.vue │ └── login │ │ ├── index.vue │ │ ├── login.vue │ │ ├── phone.vue │ │ ├── register.vue │ │ └── schoolPicker.vue ├── main.js ├── plugins │ └── vant.js ├── router │ ├── contact.js │ ├── download.js │ ├── home.js │ ├── index.js │ ├── task.js │ └── todolist.js ├── store │ ├── actions.js │ ├── getters.js │ ├── index.js │ ├── mutation-types.js │ ├── mutations.js │ └── state.js ├── utils │ └── storage.js └── views │ └── index.vue └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 13 | }, 14 | parserOptions: { 15 | parser: 'babel-eslint' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FileCollection文件收集系统 2 | > **大家都是程序员,希望大佬不要搞我的后台接口(跪下)。另外如果觉得还行的话,可以给个小星星鼓励下么,感谢** 3 | ## 一、简介 4 | ### 1. 思路 5 | 鉴于班长,团支书等班干部在收集材料(例如**青年大学习截图**)时,经常需要在群里@**全体成员**, 6 | 我决定开发这样一个轻量级的文件收集系统,帮助班干部完成任务,减轻他们的压力。 7 | 最开始只是和大家闲聊谈到,但后来觉得挺有意思的,可以做,于是还真的就一发不可收拾了。 8 | 熬夜写(累崩)QAQ,不过兴趣是最好的老师,在这个过程中学会了很多新知识。 9 | 10 | ### 2. 前端状况 11 | #### ①主要工具: 12 | 1. 前端主要使用Vue框架,Vuex状态管理,Vue-Router路由管理,辅以lodash库和ES6新特性进行组件化开发,使用webpack打包项目。 13 | 2. UI方面使用Vue移动端组件库Vant,采用stylus预处理器编译CSS代码,除组件原生样式外暂时未做太多布局,重点开发业务逻辑。 14 | 3. 使用vue-persistedstate进行本地状态持久化存储,编写storage.js进行localstorage加密处理,防止刷新丢失数据并保证数据安全。 15 | 4. 使用axios,qs包封装请求,处理请求字符串(**q**uery**s**tring) 16 | 5. 使用js-cookie包管理cookie,读取登陆状态等 17 | 18 | #### ②已完成功能点如下: 19 | 1. 常规的登陆,注册,修改资料等 20 | 2. 登陆状态管理,用户信息存储。 21 | 3. 发布任务功能 22 | 4. 待提交任务清单,也就是每个人需要完成的任务 23 | 24 | #### ③待完成大功能点有: 25 | 1. 界面进一步美化,代码模块组织 26 | 27 | ### 3. 后端状况 28 | #### ①主要工具: 29 | 1. 主要使用nodejs完成,使用Express框架搭建服务器,使用ES6,CommandJS标准进行模块化开发。 30 | 2. 基于HTTPS协议,但开发阶段暂时使用HTTP协议 31 | 3. 使用mongoose包连接并操作MongoDB数据库,存储用户数据,包括会话,用户基本信息,任务信息 32 | 4. 使用q模块封装数据库操作(DAO),返回promise对象 33 | 5. 使用body-parser解析post请求体 34 | 6. 使用jsonwebtoken颁发令牌,访问项目需要携带令牌,进行身份验证 35 | 7. 文件上传使用**又拍云**对象存储服务与CDN服务 36 | 8. 使用xlsx包读取表格中的大学信息,并生成json文件用于提供大学信息 37 | 9. ~~使用express-session,cookie-parser,配合connect-mongo持久化用户会话信息,借助会话限制个别接口的访问~~ 38 | 39 | ### 二、说明 40 | 第一次正儿八经的开发一个系统,前后端都要负责的情况下,真的要考虑很多问题。在开发的过程中,很多以前不熟悉, 41 | 但又很常用的模块,例如promise,async,await,cookie等基本已经熟练掌握。在逐渐理解设计原则与设计模式的过程中, 42 | 项目经历了一次又一次的重构,但每一次重构,都是一次进步,我也很享受这个过程,毕竟独自开发,遇到问题需要不同的翻书, 43 | 这本身就是一个学习的过程。 44 | 45 | 另外,后端代码涉及数据库账号密码,还有https的证书等暂时不方便公开,但是后端接口是部署好了的,项目文件夹src下的api.js里面 46 | **将localhost替换为域名biubiubius.com即可**,不要瞎搞奥,验证码要钱的咧。 47 | 48 | ### 三、起步 49 | 50 | ``` bash 51 | # 安装依赖包 52 | npm install 53 | 54 | # 将项目部署到 0.0.0.0:5000 55 | npm run serve 56 | ``` 57 | 58 | ### 四、相关截图 59 | ![home](images/home.png) 60 | ![download](images/download.png) 61 | ![upload](images/upload.png) 62 | ![school](images/school.jpg) 63 | ![info](images/info.jpg) 64 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ], 5 | plugins: [ 6 | 'lodash', 7 | ['import', { 8 | libraryName: 'vant', 9 | libraryDirectory: 'es', 10 | style: true 11 | }, 'vant'], 12 | '@babel/plugin-syntax-dynamic-import' 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /images/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emosheeep/fileUpload/bdef5d91e9c7243b906a1bfc0bde0366cc0b505a/images/download.png -------------------------------------------------------------------------------- /images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emosheeep/fileUpload/bdef5d91e9c7243b906a1bfc0bde0366cc0b505a/images/home.png -------------------------------------------------------------------------------- /images/info.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emosheeep/fileUpload/bdef5d91e9c7243b906a1bfc0bde0366cc0b505a/images/info.jpg -------------------------------------------------------------------------------- /images/school.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emosheeep/fileUpload/bdef5d91e9c7243b906a1bfc0bde0366cc0b505a/images/school.jpg -------------------------------------------------------------------------------- /images/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emosheeep/fileUpload/bdef5d91e9c7243b906a1bfc0bde0366cc0b505a/images/upload.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fileupload", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build --report", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "animate.css": "^3.7.2", 12 | "axios": "^0.19.1", 13 | "core-js": "^3.4.4", 14 | "js-base64": "^2.5.1", 15 | "js-cookie": "^2.2.1", 16 | "lodash": "^4.17.15", 17 | "moment": "^2.24.0", 18 | "qs": "latest", 19 | "upyun": "^3.3.11", 20 | "vue": "^2.6.10", 21 | "vue-router": "^3.1.3", 22 | "vuex": "^3.1.2", 23 | "vuex-persistedstate": "^2.7.0" 24 | }, 25 | "devDependencies": { 26 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 27 | "@vue/cli-plugin-babel": "^4.1.0", 28 | "@vue/cli-plugin-eslint": "^4.1.0", 29 | "@vue/cli-plugin-router": "^4.1.0", 30 | "@vue/cli-plugin-vuex": "^4.1.0", 31 | "@vue/cli-service": "^4.1.0", 32 | "@vue/eslint-config-standard": "^4.0.0", 33 | "babel-eslint": "^10.0.3", 34 | "babel-plugin-import": "^1.13.0", 35 | "babel-plugin-lodash": "^3.3.4", 36 | "compression-webpack-plugin": "^3.1.0", 37 | "eslint": "^5.16.0", 38 | "eslint-plugin-vue": "^5.0.0", 39 | "lodash-webpack-plugin": "^0.11.5", 40 | "moment-locales-webpack-plugin": "^1.1.2", 41 | "stylus": "^0.54.7", 42 | "stylus-loader": "^3.0.2", 43 | "vant": "^2.4.2", 44 | "vue-template-compiler": "^2.6.10", 45 | "webpack-bundle-analyzer": "^3.6.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emosheeep/fileUpload/bdef5d91e9c7243b906a1bfc0bde0366cc0b505a/public/favicon.ico -------------------------------------------------------------------------------- /public/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 | 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/api/api.js: -------------------------------------------------------------------------------- 1 | import http from './axios' 2 | 3 | const AUTH = '/auth' 4 | const UNIVERSITY = '/university' 5 | const LOGIN = '/login' 6 | const REGISTER = '/register' 7 | const UPDATEPHONE = '/update/phone' 8 | const UPDATEINFO = '/update/info' 9 | const UPDATEAVATAR = '/update/avatar' 10 | const UPDATEBACKGROUND = '/update/background' 11 | const CONTACT = '/update/contact' 12 | const ADDTASK = '/task/add' 13 | const REMOVETASK = '/task/remove' 14 | const UPDATETASK = '/task/update' 15 | const FINDTASK = '/task/find' 16 | const TODOLIST = '/todoList' 17 | 18 | // 发送验证码 19 | export const sendAuthCode = data => http(AUTH, data, 'POST') 20 | 21 | // 获取大学信息 22 | export const getUniversity = () => http(UNIVERSITY) 23 | 24 | // 登陆 25 | export const login = data => http(LOGIN, data, 'POST') 26 | 27 | // 注册 28 | export const register = data => http(REGISTER, data, 'POST') 29 | 30 | // 修改信息 31 | export const updatePhone = data => http(UPDATEPHONE, data, 'POST') 32 | export const updateInfo = data => http(UPDATEINFO, data, 'POST') 33 | export const updateAvatar = data => http(UPDATEAVATAR, data, 'POST') 34 | export const updateBackground = data => http(UPDATEBACKGROUND, data, 'POST') 35 | 36 | // 联系人信息接口 37 | export const updateContact = data => http(CONTACT, data, 'POST') 38 | 39 | // 用户的任务信息编辑 40 | export const addTask = data => http(ADDTASK, data, 'POST') 41 | export const removeTask = data => http(REMOVETASK, data, 'POST') 42 | export const updateTask = data => http(UPDATETASK, data, 'POST') 43 | export const findTask = data => http(FINDTASK, data, 'POST') 44 | 45 | // 查询待提交清单 46 | export const todoList = data => http(TODOLIST, data, 'POST') 47 | -------------------------------------------------------------------------------- /src/api/axios.js: -------------------------------------------------------------------------------- 1 | /* 2 | ajax请求函数模块 3 | 返回promise对象,返回数据response.data 4 | */ 5 | import axios from 'axios' 6 | import Qs from 'qs' 7 | import store from '../store' 8 | 9 | // 本地接口 10 | axios.defaults.baseURL = 'http://localhost:3001/api' 11 | // 网络接口 12 | // axios.defaults.baseURL = 'http://www.biubiubius.com:3001/api' 13 | 14 | export default function (url, data = {}, type = 'GET', 15 | headers = { authorization: store.state.token }) { 16 | return new Promise((resolve, reject) => { 17 | // 保存由axios返回的promise对象 18 | let promise 19 | if (type === 'GET') { 20 | promise = axios.get(url, { 21 | params: data, 22 | headers 23 | }) 24 | } else { 25 | // 发送post请求 序列化为表单数据 26 | promise = axios.post(url, Qs.stringify(data), { headers }) 27 | } 28 | 29 | promise.then(res => { 30 | // 成功调用resolve,返回数据部分 31 | resolve(res.data) 32 | }).catch(err => { 33 | if (err) { 34 | reject(err.response) 35 | } else { 36 | console.log(err) 37 | reject(err) 38 | } 39 | }) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/api/upyun.js: -------------------------------------------------------------------------------- 1 | import upyun from 'upyun' 2 | import http from './axios' 3 | import { Base64 } from 'js-base64' 4 | 5 | function getHeaderSign (bucket, method, path) { 6 | return http('/sign/upyun', { 7 | bucket: bucket.bucketName, 8 | method, 9 | path 10 | }) 11 | } 12 | 13 | let bucket = new upyun.Bucket('image-fileupload') 14 | 15 | // 又拍云压缩 16 | const UPYUN_COMPRESS = 'http://p0.api.upyun.com/pretreatment/' 17 | 18 | // 通用 19 | export const client = new upyun.Client(bucket, getHeaderSign) 20 | // 又拍云cdn缓存刷新 21 | export const refreshCDN = url => http('/sign/refresh', { url }, 'POST') 22 | // 压缩云端文件 23 | export const compress = async (tasks) => { 24 | return new Promise((resolve, reject) => { 25 | http('/sign/compress', {}, 'POST').then(headers => { 26 | let data = { 27 | notify_url: 'https://hooks.upyun.com/OVYZW1X4_', 28 | tasks: Base64.encode(JSON.stringify(tasks)), 29 | service: 'image-fileupload', 30 | app_name: 'compress' 31 | } 32 | resolve(http(UPYUN_COMPRESS, data, 'POST', headers)) 33 | }).catch(e => reject(e)) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emosheeep/fileUpload/bdef5d91e9c7243b906a1bfc0bde0366cc0b505a/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/reset.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | /* -----------H-ui前端框架----------------------- 3 | H-ui.reset.css v1.2 4 | 重定义浏览器默认样式 5 | H-ui.reser CSS file for H-ui 6 | Copyright H-ui Inc. 7 | http://www.H-ui.net 8 | date:2014.10.09 9 | Created & Modified by guojunhui. 10 | ----------------------------------------------*/ 11 | /*1 重定义浏览器默认样式 12 | Name: style_reset 13 | Level: Global 14 | Explain: 重定义浏览器默认样式 15 | Last Modify: jackying 16 | */ 17 | *{word-wrap:break-word} 18 | html,body,h1,h2,h3,h4,h5,h6,hr,p,iframe,dl,dt,dd,ul,ol,li,pre,form,button,input,textarea,th,td,fieldset{margin:0;padding:0} 19 | ul,ol,dl{list-style-type:none} 20 | html,body{*position:static} 21 | html{font-family: sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%} 22 | address,caption,cite,code,dfn,em,th,var{font-style:normal;font-weight:normal} 23 | input,button,textarea,select,optgroup,option{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit} 24 | input,button{overflow: visible;vertical-align:middle;outline:none} 25 | body,th,td,button,input,select,textarea{font-family:"Microsoft Yahei","Hiragino Sans GB","Helvetica Neue",Helvetica,tahoma,arial,Verdana,sans-serif,"WenQuanYi Micro Hei","\5B8B\4F53";font-size:12px;color: #333;-webkit-font-smoothing:antialiased;-moz-font-smoothing:antialiased} 26 | body{line-height:1.6} 27 | h1,h2,h3,h4,h5,h6{font-size:100%} 28 | a,area{outline:none;blr:expression(this.onFocus=this.blur())} 29 | a{text-decoration:none;cursor: pointer} 30 | a:hover{text-decoration:underline;outline:none} 31 | a.ie6:hover{zoom:1} 32 | a:focus{outline:none} 33 | a:hover,a:active{outline:none}:focus{outline:none} 34 | sub,sup{vertical-align:baseline} 35 | /*img*/ 36 | img{border:0;vertical-align:middle} 37 | a img,img{-ms-interpolation-mode:bicubic} 38 | .img-responsive{max-width: 100%;height: auto} 39 | /*IE下a:hover 背景闪烁*/ 40 | html{overflow:-moz-scrollbars-vertical;zoom:expression(function(ele){ele.style.zoom = "1";document.execCommand("BackgroundImageCache",false,true)}(this))} 41 | 42 | /*HTML5 reset*/ 43 | header,footer,section,aside,details,menu,article,section,nav,address,hgroup,figure,figcaption,legend{display:block;margin:0;padding:0}time{display:inline} 44 | audio,canvas,video{display:inline-block;*display:inline;*zoom:1} 45 | audio:not([controls]){display:none} 46 | legend{width:100%;margin-bottom:20px;font-size:21px;line-height:40px;border:0;border-bottom:1px solid #e5e5e5} 47 | legend small{font-size:15px;color:#999} 48 | svg:not(:root) {overflow: hidden} 49 | fieldset {border-width:0;padding: 0.35em 0.625em 0.75em;margin: 0 2px;border: 1px solid #c0c0c0} 50 | input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button {height: auto} 51 | input[type="search"] {-webkit-appearance: textfield; /* 1 */-moz-box-sizing: content-box;-webkit-box-sizing: content-box; /* 2 */box-sizing: content-box} 52 | input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration {-webkit-appearance: none} 53 | /* 54 | Name: style_clearfix 55 | Example: class="clearfix|cl" 56 | Explain: Clearfix(简写cl)避免因子元素浮动而导致的父元素高度缺失能问题 57 | */ 58 | .cl:after,.clearfix:after{content:".";display:block;height:0;clear:both;visibility:hidden}.cl,.clearfix{zoom:1} -------------------------------------------------------------------------------- /src/components/business/download/card.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 46 | 47 | 66 | -------------------------------------------------------------------------------- /src/components/business/download/files.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 57 | 58 | 61 | -------------------------------------------------------------------------------- /src/components/business/download/preview.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 139 | 140 | 150 | -------------------------------------------------------------------------------- /src/components/business/task/addTask.vue: -------------------------------------------------------------------------------- 1 | 86 | 87 | 204 | 205 | 208 | -------------------------------------------------------------------------------- /src/components/business/task/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/components/business/task/taskDetail.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 219 | 220 | 229 | -------------------------------------------------------------------------------- /src/components/business/task/taskItem.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 47 | 48 | 74 | -------------------------------------------------------------------------------- /src/components/business/task/taskList.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 65 | 66 | 72 | -------------------------------------------------------------------------------- /src/components/business/todoList/todoList.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 93 | 94 | 97 | -------------------------------------------------------------------------------- /src/components/business/todoList/upload.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 99 | 100 | 108 | -------------------------------------------------------------------------------- /src/components/contact/groupEdit.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 84 | 85 | 88 | -------------------------------------------------------------------------------- /src/components/contact/groupItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 47 | 48 | 52 | -------------------------------------------------------------------------------- /src/components/contact/groupList.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 121 | 122 | 125 | -------------------------------------------------------------------------------- /src/components/contact/groupUserList.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 131 | 132 | 135 | -------------------------------------------------------------------------------- /src/components/contact/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/components/contact/userEdit.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 95 | 96 | 99 | -------------------------------------------------------------------------------- /src/components/contact/userList.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 34 | 35 | 38 | -------------------------------------------------------------------------------- /src/components/home/explore.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/components/home/home.vue: -------------------------------------------------------------------------------- 1 | 141 | 142 | 221 | 222 | 270 | -------------------------------------------------------------------------------- /src/components/home/info.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 222 | 223 | 226 | -------------------------------------------------------------------------------- /src/components/login/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 31 | 32 | 35 | -------------------------------------------------------------------------------- /src/components/login/login.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 76 | 77 | 80 | -------------------------------------------------------------------------------- /src/components/login/phone.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 139 | 140 | 143 | -------------------------------------------------------------------------------- /src/components/login/register.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 146 | 147 | 149 | -------------------------------------------------------------------------------- /src/components/login/schoolPicker.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 111 | 112 | 115 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import './plugins/vant.js' 6 | import Cookies from 'js-cookie' 7 | 8 | Vue.config.productionTip = false 9 | // 全局绑定cookie函数 10 | Vue.prototype.$cookie = Cookies 11 | 12 | new Vue({ 13 | router, 14 | store, 15 | render: h => h(App) 16 | }).$mount('#app') 17 | 18 | // animateCSS 19 | Vue.prototype.$animationCSS = function (element, animationName, callback) { 20 | const node = typeof element === 'object' ? element : document.querySelector(element) 21 | node.classList.add('animated', animationName) 22 | node.addEventListener('animationend', function handler () { 23 | node.classList.remove('animated', animationName) 24 | node.removeEventListener('animationend', handler) 25 | 26 | if (typeof callback === 'function') callback() 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /src/plugins/vant.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { 3 | NavBar, Cell, Button, 4 | Toast, Dialog, Icon, Lazyload, 5 | Image, Loading, CellGroup, Popup, Divider, 6 | ActionSheet, Panel, CountDown, Stepper, 7 | Tab, Tabs, Tag, Sticky, Field, PullRefresh, 8 | Checkbox, Row, NoticeBar, SwipeCell, 9 | List, Search 10 | } from 'vant' 11 | 12 | Vue.use(Lazyload) 13 | // 生产模式下使用外部CDN 14 | if (process.env.NODE_ENV !== 'production') { 15 | Vue 16 | .use(Search) 17 | .use(List) 18 | .use(SwipeCell) 19 | .use(NoticeBar) 20 | .use(Row) 21 | .use(Checkbox) 22 | .use(PullRefresh) 23 | .use(Field) 24 | .use(Sticky) 25 | .use(ActionSheet) 26 | .use(Panel) 27 | .use(CountDown) 28 | .use(Stepper) 29 | .use(Tab) 30 | .use(Tabs) 31 | .use(Tag) 32 | .use(NavBar) 33 | .use(Toast) 34 | .use(Dialog) 35 | .use(Cell) 36 | .use(Button) 37 | .use(Icon) 38 | .use(Image) 39 | .use(Loading) 40 | .use(CellGroup) 41 | .use(Popup) 42 | .use(Divider) 43 | } 44 | -------------------------------------------------------------------------------- /src/router/contact.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/contact', 4 | name: 'contact', 5 | component: () => import(/* webpackChunkName: "contact" */ '../components/contact'), 6 | beforeEnter: (to, from, next) => { // 跳转 7 | if (to.name === 'contact') { 8 | next({ name: 'groups', replace: true }) 9 | } else { 10 | next() 11 | } 12 | }, 13 | children: [ 14 | { 15 | path: 'groups', 16 | name: 'groups', 17 | component: () => import(/* webpackChunkName: "contact" */ '../components/contact/groupList.vue') 18 | }, 19 | { 20 | path: 'members', 21 | name: 'members', 22 | component: () => import(/* webpackChunkName: "contact" */ '../components/contact/groupUserList.vue'), 23 | props: route => ({ title: route.query.title }) 24 | } 25 | ] 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /src/router/download.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/files', 4 | name: 'files', 5 | component: () => import(/* webpackChunkName: "download" */ '../components/business/download/files.vue') 6 | }, 7 | { 8 | path: '/preview', 9 | name: 'preview', 10 | component: () => import(/* webpackChunkName: "download" */ '../components/business/download/preview.vue'), 11 | props: route => ({ title: route.query.title }) 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /src/router/home.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/info', 4 | name: 'info', 5 | component: () => import(/* webpackChunkName: "home" */ '../components/home/info.vue') 6 | }, 7 | { 8 | path: '/', 9 | component: () => import(/* webpackChunkName: "home" */ '../views/index.vue'), 10 | beforeEnter: (to, from, next) => { // 跳转 11 | if (to.fullPath === '/') { 12 | next({ name: 'home', replace: true }) 13 | } else { 14 | next() 15 | } 16 | }, 17 | children: [ 18 | { 19 | path: 'explore', 20 | name: 'explore', 21 | component: () => import(/* webpackChunkName: "home" */ '../components/home/explore.vue') 22 | }, 23 | { 24 | path: 'home', 25 | name: 'home', 26 | component: () => import(/* webpackChunkName: "home" */ '../components/home/home.vue') 27 | } 28 | ] 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import store from '../store' 4 | import home from './home' 5 | import task from './task' 6 | import contact from './contact' 7 | import download from './download' 8 | import todolist from './todolist' 9 | 10 | Vue.use(VueRouter) 11 | 12 | const routes = [ 13 | ...home, 14 | ...task, 15 | ...contact, 16 | ...download, 17 | ...todolist, 18 | { 19 | path: '/login', 20 | name: 'login', 21 | component: () => import(/* webpackChunkName: "login" */ '../components/login') 22 | } 23 | ] 24 | 25 | const router = new VueRouter({ 26 | mode: 'history', 27 | base: process.env.BASE_URL, 28 | routes 29 | }) 30 | 31 | // 全局路由拦截(主要针对登陆状态) 32 | router.beforeEach((to, from, next) => { 33 | // 判断是否过期 34 | if (Date.now() <= store.state.expires) { 35 | next() 36 | } else { 37 | if (to.name === 'login') { 38 | next() 39 | } else { 40 | next({ name: 'login', replace: true }) 41 | console.log('请先登录') 42 | } 43 | } 44 | }) 45 | 46 | export default router 47 | -------------------------------------------------------------------------------- /src/router/task.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/task', 4 | name: 'task', 5 | component: () => import(/* webpackChunkName: "task" */ '../components/business/task'), 6 | beforeEnter: (to, from, next) => { // 跳转 7 | if (to.name === 'task') { 8 | next({ name: 'list', replace: true }) 9 | } else { 10 | next() 11 | } 12 | }, 13 | children: [ 14 | { 15 | path: 'list', 16 | name: 'list', 17 | component: () => import(/* webpackChunkName: "task" */ '../components/business/task/taskList.vue') 18 | }, 19 | { 20 | path: 'addTask', 21 | name: 'addTask', 22 | component: () => import(/* webpackChunkName: "task" */ '../components/business/task/addTask.vue') 23 | }, 24 | { 25 | path: 'detail', 26 | name: 'detail', 27 | component: () => import(/* webpackChunkName: "task" */ '../components/business/task/taskDetail.vue'), 28 | props: route => ({ title: route.query.title }) 29 | } 30 | ] 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /src/router/todolist.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/todoList', 4 | name: 'todoList', 5 | component: () => import(/* webpackChunkName: "todoList" */ '../components/business/todoList/todoList.vue') 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /src/store/actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 通过mutations间接更新state,这里的方法可以是异步的 3 | */ 4 | import { 5 | findTask, updateContact, 6 | updateAvatar, updateBackground 7 | } from '../api/api.js' 8 | import type from './mutation-types' 9 | 10 | export default { 11 | // 读取待提交清单信息 12 | [type.SET_TASK] ({ commit, state }, callback) { 13 | return new Promise((resolve, reject) => { 14 | findTask({ 15 | creator: state.phone 16 | }).then(res => { 17 | commit(type.SET_TASK, res.data) 18 | resolve(res.data) 19 | }).catch(e => reject(e)) 20 | }) 21 | }, 22 | // 异步更新联系人信息 23 | [type.SET_CONTACT] ({ commit, state }, contact) { 24 | return new Promise((resolve, reject) => { 25 | updateContact({ 26 | condition: { phone: state.phone }, 27 | data: { contact } 28 | }).then(result => { 29 | if (result.status) { 30 | // 后台数据整体替换 31 | commit(type.SET_CONTACT, contact) 32 | resolve(contact) 33 | } else { 34 | // eslint-disable-next-line prefer-promise-reject-errors 35 | reject(result) 36 | } 37 | }).catch(e => reject(e)) 38 | }) 39 | }, 40 | // 更新头像 41 | [type.SET_AVATAR] ({ commit, state }, filename) { 42 | return new Promise((resolve, reject) => { 43 | updateAvatar({ 44 | phone: state.phone, 45 | avatar: filename 46 | }).then(res => { 47 | commit(type.SET_AVATAR, filename) 48 | resolve(res) 49 | }).catch(e => reject(e)) 50 | }) 51 | }, 52 | // 更新背景图片 53 | [type.SET_BACKGROUND] ({ commit, state }, filename) { 54 | return new Promise((resolve, reject) => { 55 | updateBackground({ 56 | phone: state.phone, 57 | background: filename 58 | }).then(res => { 59 | commit(type.SET_BACKGROUND, filename) 60 | resolve(res) 61 | }).catch(e => reject(e)) 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * state的计算属性 3 | */ 4 | export default { 5 | } 6 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | vuex最核心的管理对象store, 导出store对象 3 | */ 4 | import Vue from 'vue' 5 | import Vuex from 'vuex' 6 | import state from './state' 7 | import mutations from './mutations' 8 | import actions from './actions' 9 | import getters from './getters' 10 | import createPersistedState from 'vuex-persistedstate' 11 | import createLogger from 'vuex/dist/logger' 12 | import Storage from '../utils/storage' 13 | 14 | Vue.use(Vuex) 15 | const debug = process.env.NODE_ENV !== 'production' 16 | const persistedState = createPersistedState({ 17 | key: 'loginUser', 18 | storage: new Storage() 19 | }) 20 | export default new Vuex.Store({ 21 | state, 22 | actions, 23 | mutations, 24 | getters, 25 | strict: debug, 26 | plugins: debug ? [createLogger(), persistedState] : [persistedState] // 调试插件,控制台打印具体信息 27 | }) 28 | -------------------------------------------------------------------------------- /src/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 定义一些常量 3 | */ 4 | let methods = { 5 | UPDATE_USER: 'UPDATE_USER', // 更新用户信息 6 | CLEAR_USER: 'CLEAR_USER', // 清除用户信息 7 | SET_CONTACT: 'SET_CONTACT', // 设置联系人分组 8 | ADD_TASK: 'ADD_TASK', // 新增任务 9 | SET_TASK: 'SET_TASK', // 新增任务 10 | DELETE_TASK: 'DELETE_TASK', // 删除任务 11 | UPDATE_TASK: 'UPDATE_TASK', // 更新任务 12 | FIND_TASK: 'FIND_TASK', // 查询任务 13 | SET_TODOLIST: 'SET_TODOLIST', // 设置待提交清单 14 | SET_TOKEN: 'SET_TOKEN', // 设置令牌 15 | SET_USER_BY_PHONE: 'SET_USER_BY_PHONE', // 服务器端更新数据后,本地也需要更新 16 | SET_AVATAR: 'SET_AVATAR', // 修改头像 17 | SET_BACKGROUND: 'SET_BACKGROUND' 18 | } 19 | Object.freeze(methods) 20 | export default methods 21 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * mutations直接修改数据对象state,注意这里的方法必须是同步方法 3 | */ 4 | import type from './mutation-types' 5 | 6 | export default { 7 | // 设置用户信息 8 | [type.UPDATE_USER] (state, data = {}) { 9 | for (let key in state) { 10 | if (data[key]) { 11 | state[key] = data[key] 12 | } 13 | } 14 | }, 15 | // 清除用户信息 16 | [type.CLEAR_USER] (state) { 17 | state.expires = '' 18 | state.token = '' 19 | state.domain = '' 20 | state.username = '' 21 | state.studentID = '' 22 | state.phone = '' 23 | state.avatar = '' // 头像路径 24 | state.background = '' // 背景图片路径 25 | state.university = { 26 | name: '', 27 | id: '' 28 | } 29 | state.contact = [] 30 | state.task = [] 31 | state.todoList = [] 32 | console.log('用户信息已清除') 33 | }, 34 | // 设置contact 35 | [type.SET_CONTACT] (state, contact) { 36 | state.contact = contact 37 | }, 38 | // 新增task 39 | [type.ADD_TASK] (state, task) { 40 | state.task.push(task) 41 | }, 42 | // 替换task 43 | [type.SET_TASK] (state, task) { 44 | state.task = task 45 | }, 46 | // 删除task 47 | [type.DELETE_TASK] (state, task) { 48 | // TODO:这里无端报错 49 | state.task = state.task.filter(item => item.id !== task.id) 50 | }, 51 | // 更新task 52 | [type.UPDATE_TASK] (state, task) { 53 | state.task = state.task.map(item => { 54 | return task.id === item.id ? task : item 55 | }) 56 | }, 57 | // 设置todoList 58 | [type.SET_TODOLIST] (state, todoList) { 59 | state.todoList = todoList 60 | }, 61 | // 服务器端更新手机号码之后,本地同步更新任务和待提交清单 62 | [type.SET_USER_BY_PHONE] (state, phone) { 63 | // 更新每个任务的创建人信息 64 | state.task = state.task.map(item => { 65 | item.creator = phone 66 | return item 67 | }) 68 | // 同理 69 | state.todoList = state.task.map(item => { 70 | item.creator = phone 71 | return item 72 | }) 73 | }, 74 | [type.SET_AVATAR] (state, filename) { 75 | state.avatar = filename 76 | }, 77 | [type.SET_BACKGROUND] (state, filename) { 78 | state.background = filename 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/store/state.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 状态对象 3 | */ 4 | export default { 5 | expires: '', // 过期时间为七天后 6 | token: '', // 认证信息 7 | domain: '', // CDN域名 8 | username: '', 9 | studentID: '', 10 | phone: '', 11 | avatar: '', // 头像路径 12 | background: '', // 背景图片路径 13 | university: { 14 | name: '', 15 | id: '' 16 | }, 17 | // 联系人小组信息 18 | contact: [], 19 | // 发布的任务 20 | task: [], 21 | // 待提交清代 22 | todoList: [] 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/storage.js: -------------------------------------------------------------------------------- 1 | // 带过期时间的持久化加密本地存储 2 | import { Base64 } from 'js-base64' 3 | import moment from 'moment' 4 | 5 | class Storage { 6 | constructor () { // 参数为最长过期时间 7 | this.storage = window.localStorage 8 | } 9 | setItem (key, value) { 10 | // 这里的value已经是JSON格式 11 | this.storage.setItem(key, Base64.encode(value)) 12 | } 13 | getItem (key) { 14 | const origin = Base64.decode(this.storage.getItem(key)) 15 | let { expires } = JSON.parse(origin) 16 | expires = moment(expires) 17 | console.log('到期时间:', expires.format('YYYY-MM-DD HH:mm:ss')) 18 | if (moment().isSameOrBefore(expires)) { 19 | return origin // 这里要返回JSON格式字符串 20 | } else { 21 | // 时间过期就清空 22 | this.removeItem(key) 23 | return '' 24 | } 25 | } 26 | removeItem (key) { 27 | this.storage.removeItem(key) 28 | } 29 | } 30 | 31 | export default Storage 32 | -------------------------------------------------------------------------------- /src/views/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | 30 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const LodashPlugin = require('lodash-webpack-plugin') 2 | const MomentLocalesPlugin = require('moment-locales-webpack-plugin') 3 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 4 | const WebpackBundleAnalyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 5 | 6 | module.exports = { 7 | publicPath: './', 8 | chainWebpack: config => { 9 | config 10 | .plugin('lodash').use(LodashPlugin).end() 11 | .plugin('moment').use(MomentLocalesPlugin).end() 12 | config 13 | .devServer.disableHostCheck(true).end() 14 | config.optimization.splitChunks({ 15 | cacheGroups: { 16 | common: { 17 | name: 'common', 18 | chunks: 'all', 19 | minSize: 20, 20 | minChunks: 2 21 | } 22 | } 23 | }) 24 | // 构建分析 25 | if (process.env.NODE_ENV === 'production') { 26 | config.externals({ 27 | vue: 'Vue', 28 | moment: 'moment', 29 | 'vue-router': 'VueRouter' 30 | }) 31 | config.plugin('html').tap(options => { 32 | options[0].minify.removeAttributeQuotes = false 33 | return options 34 | }).end() 35 | config.plugin('compress').use(CompressionWebpackPlugin, [{ 36 | test: /\.js$|\.html$|\.css$/, 37 | threshold: 0, // 超过10kb就压缩 38 | deleteOriginalAssets: false 39 | }]) 40 | if (process.env.npm_config_report) { 41 | config.plugin('analyzer').use(WebpackBundleAnalyzer).end() 42 | config.plugins.delete('prefetch') 43 | } 44 | } 45 | } 46 | } 47 | --------------------------------------------------------------------------------