├── .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 | 
60 | 
61 | 
62 | 
63 | 
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 |
2 |
3 |
4 |
5 |
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 |
2 |
5 |
13 |
14 |
15 |
16 | 加载失败
17 |
18 |
19 |
姓名:{{img.username}}
20 |
学号:{{img.studentID}}
21 |
时间:{{new Date(img.time).toLocaleDateString()}}
22 |
23 |
24 |
25 |
26 |
46 |
47 |
66 |
--------------------------------------------------------------------------------
/src/components/business/download/files.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
20 |
21 |
22 |
23 |
24 |
57 |
58 |
61 |
--------------------------------------------------------------------------------
/src/components/business/download/preview.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
17 |
25 |
26 |
27 |
28 |
29 |
139 |
140 |
150 |
--------------------------------------------------------------------------------
/src/components/business/task/addTask.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
15 |
25 |
33 |
38 |
46 |
47 |
48 |
49 |
54 |
62 |
68 |
69 |
74 |
75 |
76 | 发布任务
84 |
85 |
86 |
87 |
204 |
205 |
208 |
--------------------------------------------------------------------------------
/src/components/business/task/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
14 |
--------------------------------------------------------------------------------
/src/components/business/task/taskDetail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
17 |
18 |
19 |
20 | 开始时间:{{timeFormat(curTask.startTime)}}
21 |
26 |
27 | 点击查看完成情况
32 |
37 |
38 |
42 | 未完成
45 |
46 |
47 |
48 |
53 | 已提交
57 |
58 |
59 |
60 |
61 |
62 |
67 |
68 |
74 |
75 |
76 |
77 |
78 |
79 |
219 |
220 |
229 |
--------------------------------------------------------------------------------
/src/components/business/task/taskItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
47 |
48 |
74 |
--------------------------------------------------------------------------------
/src/components/business/task/taskList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 |
17 |
26 |
27 |
28 |
29 |
30 |
65 |
66 |
72 |
--------------------------------------------------------------------------------
/src/components/business/todoList/todoList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
16 |
17 |
23 |
24 |
25 |
26 |
31 |
35 |
36 |
37 |
38 |
39 |
93 |
94 |
97 |
--------------------------------------------------------------------------------
/src/components/business/todoList/upload.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
21 | 上传文件
22 |
23 |
24 |
25 |
26 |
27 |
99 |
100 |
108 |
--------------------------------------------------------------------------------
/src/components/contact/groupEdit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 | 保存
13 | 删除
15 |
16 |
17 |
18 |
84 |
85 |
88 |
--------------------------------------------------------------------------------
/src/components/contact/groupItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
47 |
48 |
52 |
--------------------------------------------------------------------------------
/src/components/contact/groupList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
25 |
26 |
27 |
28 |
29 |
121 |
122 |
125 |
--------------------------------------------------------------------------------
/src/components/contact/groupUserList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
13 |
18 |
19 |
20 |
21 |
28 |
29 |
30 |
31 |
32 |
131 |
132 |
135 |
--------------------------------------------------------------------------------
/src/components/contact/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
14 |
--------------------------------------------------------------------------------
/src/components/contact/userEdit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
17 |
18 | 保存
20 | 删除
22 |
23 |
24 |
25 |
95 |
96 |
99 |
--------------------------------------------------------------------------------
/src/components/contact/userList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
34 |
35 |
38 |
--------------------------------------------------------------------------------
/src/components/home/explore.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
16 |
--------------------------------------------------------------------------------
/src/components/home/home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 | 加载失败
15 |
16 |
17 |
18 |
19 |
20 |
26 |
27 |
28 |
29 | 加载失败
30 |
31 |
32 |
33 | {{username}} / {{university.name}}
34 |
35 |
36 |
41 |
47 |
48 |
56 |
57 |
58 |
59 | 加载失败
60 |
61 |
62 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
80 |
87 |
94 |
95 |
96 |
103 |
110 |
111 |
112 |
113 |
118 |
124 |
125 |
132 |
133 |
134 |
135 | 加载失败
136 |
137 |
138 |
139 |
140 |
141 |
142 |
221 |
222 |
270 |
--------------------------------------------------------------------------------
/src/components/home/info.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
21 |
26 |
32 |
33 |
34 |
35 |
41 |
46 |
47 |
48 | 确认修改
53 |
54 |
55 |
56 |
57 |
58 |
退出登录
64 |
65 |
66 |
67 |
222 |
223 |
226 |
--------------------------------------------------------------------------------
/src/components/login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
31 |
32 |
35 |
--------------------------------------------------------------------------------
/src/components/login/login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
16 | 登陆
19 |
20 |
21 |
22 |
76 |
77 |
80 |
--------------------------------------------------------------------------------
/src/components/login/phone.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
25 |
32 | {{btnText}}
33 |
34 |
35 |
41 | {{submitText}}
42 |
43 |
44 |
45 |
46 |
139 |
140 |
143 |
--------------------------------------------------------------------------------
/src/components/login/register.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
15 |
16 |
22 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
35 |
36 |
37 |
38 |
39 |
146 |
147 |
149 |
--------------------------------------------------------------------------------
/src/components/login/schoolPicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
16 |
23 |
25 |
26 |
27 |
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 |
2 |
3 |
4 |
5 | 探索
6 | 主页
7 |
8 |
9 |
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 |
--------------------------------------------------------------------------------