init
This commit is contained in:
3
.browserslistrc
Normal file
3
.browserslistrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
||||||
17
.eslintrc.js
Normal file
17
.eslintrc.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
'extends': [
|
||||||
|
'plugin:vue/essential',
|
||||||
|
'eslint:recommended'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
parser: 'babel-eslint'
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
29
README.md
Normal file
29
README.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# lions
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run your tests
|
||||||
|
```
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||||
5
babel.config.js
Normal file
5
babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
||||||
11975
package-lock.json
generated
Normal file
11975
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "lions-agent",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.19.2",
|
||||||
|
"core-js": "^3.6.4",
|
||||||
|
"vue": "^2.6.11",
|
||||||
|
"vue-router": "^3.1.6",
|
||||||
|
"vuelidate": "^0.7.5",
|
||||||
|
"vuetify": "^2.2.26",
|
||||||
|
"vuex": "^3.1.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@mdi/font": "^5.1.45",
|
||||||
|
"@vue/cli-plugin-babel": "^4.3.0",
|
||||||
|
"@vue/cli-plugin-eslint": "^4.3.0",
|
||||||
|
"@vue/cli-service": "^4.3.0",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"eslint": "^6.7.2",
|
||||||
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"material-design-icons-iconfont": "^5.0.1",
|
||||||
|
"vue-template-compiler": "^2.6.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
17
public/index.html
Normal file
17
public/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, initial-scale=1.0, user-scalable=no">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
23
src/App.vue
Normal file
23
src/App.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<v-app>
|
||||||
|
<router-view></router-view>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'app',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #fdfdfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hasFooter {
|
||||||
|
padding-bottom: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
56
src/api/axios.js
Executable file
56
src/api/axios.js
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import store from "@/store"
|
||||||
|
import router from "@/router";
|
||||||
|
// axios 配置
|
||||||
|
axios.defaults.timeout = 3000;
|
||||||
|
axios.defaults.baseURL = 'http://lions-vote.yuzhankeji.cn/api/';
|
||||||
|
|
||||||
|
const axiosConf = (config) => {
|
||||||
|
// config.headers.Authorization = store.state.accessToken;
|
||||||
|
config.headers.Authorization = localStorage.getItem('accessToken')
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
axios.interceptors.request.use(axiosConf, err => {
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
axios.interceptors.response.use(async (response) => {
|
||||||
|
let data = {};
|
||||||
|
let code = Number(response.data.status_code);
|
||||||
|
if (code === 401 || code == 0) {
|
||||||
|
if (response.data.status == 'EXCEPTION') {
|
||||||
|
return Promise.reject(response.data)
|
||||||
|
}
|
||||||
|
if (response.headers.authorization) {
|
||||||
|
await store.commit('login', response.headers.authorization);
|
||||||
|
|
||||||
|
response.config.headers.Authorization = response.headers.authorization;
|
||||||
|
const result = await axios.request(axiosConf(response.config));
|
||||||
|
if (result) {
|
||||||
|
data = result;
|
||||||
|
}
|
||||||
|
} else if (router.currentRoute.name != 'AuthLogin') {
|
||||||
|
setTimeout(() => {
|
||||||
|
router.replace({name: 'AuthLogin', query: {redirect: router.currentRoute.fullPath}});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
} else if (code === 200) {
|
||||||
|
data = response.data.data;
|
||||||
|
} else if (code === 403) {
|
||||||
|
await router.replace({path: '/403', query: {redirect: router.currentRoute.fullPath}});
|
||||||
|
} else if (code === 410) {
|
||||||
|
setTimeout(() => {
|
||||||
|
router.replace({name: 'AuthLogin', query: {redirect: router.currentRoute.fullPath}});
|
||||||
|
}, 1200);
|
||||||
|
} else if (code == 404) {
|
||||||
|
return Promise.reject(response.data);
|
||||||
|
} else {
|
||||||
|
return Promise.reject(response.data);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}, (error) => {
|
||||||
|
return Promise.reject(error.response.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default axios;
|
||||||
7
src/api/index.js
Executable file
7
src/api/index.js
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
import auth from './interfaces/auth'
|
||||||
|
import index from './interfaces/index'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
index,
|
||||||
|
auth
|
||||||
|
}
|
||||||
13
src/api/interfaces/auth.js
Executable file
13
src/api/interfaces/auth.js
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
import axios from '../axios'
|
||||||
|
|
||||||
|
const license = () => axios.get('auth/license')
|
||||||
|
|
||||||
|
const code = (data) => axios.post('auth/code/login2', data)
|
||||||
|
|
||||||
|
const loginByCode = (data) => axios.post('auth/loginByCode', data)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
license,
|
||||||
|
code,
|
||||||
|
loginByCode
|
||||||
|
}
|
||||||
7
src/api/interfaces/index.js
Normal file
7
src/api/interfaces/index.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import axios from '../axios'
|
||||||
|
|
||||||
|
const index = () => axios.get('agent')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
index
|
||||||
|
}
|
||||||
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
27
src/main.js
Normal file
27
src/main.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import VueRouter from 'vue-router'
|
||||||
|
import router from './router'
|
||||||
|
import store from './store'
|
||||||
|
import vuetify from '@/plugins/vuetify'
|
||||||
|
import api from './api'
|
||||||
|
import toast from './plugins/toast'
|
||||||
|
|
||||||
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
Vue.prototype.$api = api
|
||||||
|
Vue.prototype.toast = toast;
|
||||||
|
Vue.prototype.jump = (name, params, query) => {
|
||||||
|
router.push({
|
||||||
|
name: name,
|
||||||
|
params: params,
|
||||||
|
query: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
new Vue({
|
||||||
|
vuetify,
|
||||||
|
router,
|
||||||
|
store,
|
||||||
|
render: h => h(App)
|
||||||
|
}).$mount('#app')
|
||||||
174
src/pages/auth/login.vue
Normal file
174
src/pages/auth/login.vue
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<template>
|
||||||
|
<v-container class="login">
|
||||||
|
<div class="body-2 grey--text">
|
||||||
|
中国狮子联会哈尔滨代表处
|
||||||
|
</div>
|
||||||
|
<div class="display-1 pt-3 pb-10">
|
||||||
|
选举监督系统
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-form
|
||||||
|
@submit="formSubmit"
|
||||||
|
v-model="valid"
|
||||||
|
>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="12">
|
||||||
|
<v-text-field
|
||||||
|
:counter="11"
|
||||||
|
:rules="usernameRules"
|
||||||
|
clearable
|
||||||
|
label="手机号"
|
||||||
|
prefix="+86"
|
||||||
|
required
|
||||||
|
type="tel"
|
||||||
|
v-model="username"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
class="caption grey--text"
|
||||||
|
cols="12"
|
||||||
|
md="12"
|
||||||
|
>
|
||||||
|
点击"下一步"即表示已阅读并同意并自愿遵守
|
||||||
|
<span
|
||||||
|
@click="ishow(1)"
|
||||||
|
class="blue--text text--darken-4"
|
||||||
|
>《服务使用协议》</span>以及
|
||||||
|
<span
|
||||||
|
@click="ishow(2)"
|
||||||
|
class="blue--text text--darken-4"
|
||||||
|
>《隐私声明》</span>
|
||||||
|
</v-col>
|
||||||
|
<v-col>
|
||||||
|
<v-btn
|
||||||
|
:disabled="!valid"
|
||||||
|
block
|
||||||
|
color="#1f3c84"
|
||||||
|
dark
|
||||||
|
large
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
下一步
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
|
||||||
|
<v-dialog
|
||||||
|
v-model="dialog"
|
||||||
|
width="500"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title
|
||||||
|
class="grey lighten-2"
|
||||||
|
>
|
||||||
|
{{dia.title}}
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text class="pt-4" v-html="dia.content">
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<div class="flex-grow-1"></div>
|
||||||
|
<v-btn
|
||||||
|
@click="dialog = false"
|
||||||
|
color="#1f3c84"
|
||||||
|
text
|
||||||
|
>
|
||||||
|
已阅读
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "login",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
username: '',
|
||||||
|
usernameRules: [
|
||||||
|
v => !!v || '手机号必须填写',
|
||||||
|
v => (v && v.length == 11) || '手机号码必须是11位',
|
||||||
|
v => /^1[3-9][0-9]{9}$/.test(v) || '手机号码格式不正确'
|
||||||
|
],
|
||||||
|
dialog: false,
|
||||||
|
dia: {
|
||||||
|
title: '',
|
||||||
|
content: ''
|
||||||
|
},
|
||||||
|
agreement: '',
|
||||||
|
privacy: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$api.auth.license().then(result => {
|
||||||
|
this.agreement = result.agreement;
|
||||||
|
this.privacy = result.privacy
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.$api.auth.code({
|
||||||
|
username: this.username
|
||||||
|
}).then(() => {
|
||||||
|
router.push({
|
||||||
|
name: 'AuthVerify',
|
||||||
|
params: {
|
||||||
|
username: this.username
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
this.toast(err.message, 'warning')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
ishow(n) {
|
||||||
|
this.dialog = !this.dialog;
|
||||||
|
if (n == 1) {
|
||||||
|
this.dia = {
|
||||||
|
title: '用户协议',
|
||||||
|
content: this.agreement
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.dia = {
|
||||||
|
title: '隐私声明',
|
||||||
|
content: this.privacy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
wechatLogin() {
|
||||||
|
let uri = window.location.origin + '/auth/wechat';
|
||||||
|
this.$api.auth.wechat_url({
|
||||||
|
uri: uri,
|
||||||
|
scope: 'snsapi_base'
|
||||||
|
}).then(res => {
|
||||||
|
window.location.href = res.url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login {
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-image: url("../../assets/logo.png");
|
||||||
|
background-size: 200px;
|
||||||
|
background-position: center;
|
||||||
|
|
||||||
|
padding: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption span {
|
||||||
|
color: #1f3c84;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
186
src/pages/auth/verify.vue
Normal file
186
src/pages/auth/verify.vue
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
<template>
|
||||||
|
<v-content>
|
||||||
|
<v-container>
|
||||||
|
<input
|
||||||
|
:disabled="disable"
|
||||||
|
@input="getVal"
|
||||||
|
autofocus="autofocus"
|
||||||
|
class="hidden-input"
|
||||||
|
type="number"
|
||||||
|
v-model="verify"
|
||||||
|
>
|
||||||
|
<div class="code-box">
|
||||||
|
<div class="title">
|
||||||
|
输入短信验证码
|
||||||
|
</div>
|
||||||
|
<div class="body-2">
|
||||||
|
验证码已发送至{{username}},请在下方输入框内输入4位数字验证码
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-space-around">
|
||||||
|
<template v-for="(item, index) in ranges">
|
||||||
|
<div :class="{active: codeIndex === item}" :key="index" class="item">
|
||||||
|
<div v-if="codeIndex == item">
|
||||||
|
<span class="dot-line"></span>
|
||||||
|
</div>
|
||||||
|
{{ codeArr[index] ? codeArr[index] : ''}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-container>
|
||||||
|
</v-content>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import router from "@/router";
|
||||||
|
import {mapActions} from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
maxlength: 4,
|
||||||
|
codeIndex: 1,
|
||||||
|
codeArr: [],
|
||||||
|
ranges: [1, 2, 3, 4],
|
||||||
|
disable: false,
|
||||||
|
username: '',
|
||||||
|
verify: '',
|
||||||
|
next_path: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeRouteEnter(to, from, next) {
|
||||||
|
if (from.name != 'AuthLogin' || to.params.username == undefined) {
|
||||||
|
router.push({
|
||||||
|
name: 'AuthLogin'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.username = this.$route.params.username;
|
||||||
|
this.parent_id = this.$route.params.parent_id;
|
||||||
|
this.next_path = this.$route.params.next_path
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['doLogin']),
|
||||||
|
getVal(e) {
|
||||||
|
let value = e.target.value;
|
||||||
|
let arr = value.split('');
|
||||||
|
this.codeIndex = arr.length + 1;
|
||||||
|
this.codeArr = arr;
|
||||||
|
if (this.codeIndex > Number(this.maxlength)) {
|
||||||
|
this.disable = true;
|
||||||
|
this.sendVerifyToServer()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sendVerifyToServer() {
|
||||||
|
this.$api.auth.loginByCode({
|
||||||
|
username: this.username,
|
||||||
|
parent_id: this.parent_id,
|
||||||
|
verify: this.verify
|
||||||
|
}).then(result => {
|
||||||
|
this.doLogin(result.access_token);
|
||||||
|
this.toast('登录成功');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.next_path) {
|
||||||
|
router.push({
|
||||||
|
path: this.next_path
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
router.push({
|
||||||
|
name: 'Index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}).catch(err => {
|
||||||
|
this.toast(err.message, 'warning');
|
||||||
|
this.disable = false;
|
||||||
|
this.codeIndex = 1;
|
||||||
|
this.codeArr = [];
|
||||||
|
this.verify = ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@-webkit-keyframes twinkling {
|
||||||
|
0% {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes twinkling {
|
||||||
|
0% {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-moz-keyframes twinkling {
|
||||||
|
0% {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-content {
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-image: url("../../assets/logo.png");
|
||||||
|
background-size: 200px;
|
||||||
|
background-position: bottom right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-flex {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-input {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 300%;
|
||||||
|
color: #fff;
|
||||||
|
height: 100%;
|
||||||
|
text-align: left;
|
||||||
|
z-index: 9;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
border-bottom: 5px solid #BDBDBD;
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 50px;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-line {
|
||||||
|
display: block;
|
||||||
|
height: 20px;
|
||||||
|
width: 2px;
|
||||||
|
background: #BDBDBD;
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-left: 24px;
|
||||||
|
-webkit-animation: twinkling 1s infinite;
|
||||||
|
-moz-animation: twinkling 1s infinite;
|
||||||
|
animation: twinkling 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item.active {
|
||||||
|
border-bottom-color: #1f3c84;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
135
src/pages/index/index.vue
Normal file
135
src/pages/index/index.vue
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="pa-4 mbody"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
fixed
|
||||||
|
dark
|
||||||
|
fab
|
||||||
|
right
|
||||||
|
color="#1f3c84"
|
||||||
|
@click="reload"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-refresh</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-list
|
||||||
|
three-line
|
||||||
|
>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>当前投票轮次</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ now_vote }}</v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>应签到{{ desserts.length }}人,实际签到{{ desserts.filter((i) => i.sign == 1).length }}人,已投票{{ desserts.filter((i) => i.vote).length }}人。</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
<v-simple-table>
|
||||||
|
<template v-slot:default>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">姓名</th>
|
||||||
|
<th class="text-left">手机号</th>
|
||||||
|
<th class="text-left">签到</th>
|
||||||
|
<th class="text-left">投票</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in desserts" :key="item.name">
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td><a :href="`tel:` + item.mobile">{{ item.mobile }}</a></td>
|
||||||
|
<td>
|
||||||
|
<v-icon
|
||||||
|
v-if="item.sign"
|
||||||
|
color="green"
|
||||||
|
>
|
||||||
|
mdi-check
|
||||||
|
</v-icon>
|
||||||
|
<v-icon
|
||||||
|
v-else
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
mdi-close
|
||||||
|
</v-icon>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<v-icon
|
||||||
|
v-if="item.vote"
|
||||||
|
color="green"
|
||||||
|
>
|
||||||
|
mdi-check
|
||||||
|
</v-icon>
|
||||||
|
<v-icon
|
||||||
|
v-else
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
mdi-close
|
||||||
|
</v-icon>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
|
</v-simple-table>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
block
|
||||||
|
@click="logout"
|
||||||
|
>
|
||||||
|
注销登录
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapActions} from 'vuex'
|
||||||
|
import router from "@/router";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "index",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
user: [],
|
||||||
|
desserts: [],
|
||||||
|
now_vote: '当前无投票'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$api.index.index().then(result => {
|
||||||
|
this.user = result.user
|
||||||
|
this.now_vote = result.now_vote
|
||||||
|
this.desserts = result.desserts
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['doLogout']),
|
||||||
|
reload() {
|
||||||
|
this.$api.index.index().then(result => {
|
||||||
|
this.user = result.user
|
||||||
|
this.now_vote = result.now_vote
|
||||||
|
this.desserts = result.desserts
|
||||||
|
});
|
||||||
|
},
|
||||||
|
logout: function () {
|
||||||
|
this.toast('退出成功');
|
||||||
|
this.doLogout();
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push({
|
||||||
|
name: 'AuthLogin'
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cns {
|
||||||
|
background-image: url("../../assets/logo.png");
|
||||||
|
background-size: 100%;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card__text p {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
13
src/pages/layouts/common.vue
Executable file
13
src/pages/layouts/common.vue
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<router-view></router-view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "common.layout"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
34
src/plugins/toast/index.js
Executable file
34
src/plugins/toast/index.js
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
import Toast from "./toast"
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
let currentToast;
|
||||||
|
|
||||||
|
const index = (message, color) => {
|
||||||
|
if (currentToast) {
|
||||||
|
currentToast.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
currentToast = createToast({
|
||||||
|
Vue,
|
||||||
|
message,
|
||||||
|
color: color || 'success',
|
||||||
|
onClose: () => {
|
||||||
|
currentToast = null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function createToast({Vue, message, color, onClose}) {
|
||||||
|
const Constructor = Vue.extend(Toast)
|
||||||
|
|
||||||
|
let toast = new Constructor()
|
||||||
|
toast.message = message
|
||||||
|
toast.color = color
|
||||||
|
toast.timeout = 1000
|
||||||
|
toast.$mount();
|
||||||
|
toast.$on('close', onClose)
|
||||||
|
document.body.appendChild(toast.$el)
|
||||||
|
return toast
|
||||||
|
}
|
||||||
|
|
||||||
|
export default index
|
||||||
38
src/plugins/toast/toast.vue
Executable file
38
src/plugins/toast/toast.vue
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<v-snackbar
|
||||||
|
:color="color"
|
||||||
|
:timeout="timeout"
|
||||||
|
:top="true"
|
||||||
|
v-model="show"
|
||||||
|
>
|
||||||
|
{{ message }}
|
||||||
|
</v-snackbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "toast.vue",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
timeout: 1000,
|
||||||
|
color: 'success',
|
||||||
|
show: true,
|
||||||
|
message: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
console.log(this.color)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.$el.remove();
|
||||||
|
this.$emit("close");
|
||||||
|
this.$destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
8
src/plugins/utils.js
Normal file
8
src/plugins/utils.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
const isWechat = () => {
|
||||||
|
let ua = navigator.userAgent.toLowerCase();
|
||||||
|
return (/micromessenger/.test(ua)) ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
isWechat
|
||||||
|
}
|
||||||
13
src/plugins/vuetify.js
Executable file
13
src/plugins/vuetify.js
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Vuetify from 'vuetify'
|
||||||
|
import 'vuetify/dist/vuetify.min.css'
|
||||||
|
import '@mdi/font/css/materialdesignicons.css'
|
||||||
|
|
||||||
|
Vue.use(Vuetify)
|
||||||
|
|
||||||
|
export default new Vuetify({
|
||||||
|
icons: {
|
||||||
|
iconfont: 'mdi'
|
||||||
|
},
|
||||||
|
theme: {}
|
||||||
|
})
|
||||||
79
src/router/index.js
Normal file
79
src/router/index.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import VueRouter from "vue-router"
|
||||||
|
import store from "@/store"
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Index',
|
||||||
|
component: () => import(/* webpackChunkName: "index" */ '@/pages/index/index'),
|
||||||
|
meta: {
|
||||||
|
title: '首页',
|
||||||
|
keepAlive: true,
|
||||||
|
requireAuth: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
path: '/auth',
|
||||||
|
component: () => import(/* webpackChunkName: "auth" */ '@/pages/layouts/common'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'login',
|
||||||
|
name: 'AuthLogin',
|
||||||
|
component: () => import(/* webpackChunkName: "auth" */ '@/pages/auth/login'),
|
||||||
|
meta: {
|
||||||
|
title: '登录'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
path: 'verify',
|
||||||
|
name: 'AuthVerify',
|
||||||
|
component: () => import(/* webpackChunkName: "auth" */ '@/pages/auth/verify'),
|
||||||
|
meta: {
|
||||||
|
title: '验证手机号'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const router = new VueRouter({
|
||||||
|
mode: 'history',
|
||||||
|
routes
|
||||||
|
});
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
let hasLogin = store.state.hasLogin;
|
||||||
|
console.log('ROUTE' + to.name + '||' + hasLogin)
|
||||||
|
if (to.name === 'AuthLogin') {
|
||||||
|
// 如果已经登录,直接跳转到首页去
|
||||||
|
if (hasLogin) {
|
||||||
|
next({
|
||||||
|
name: 'Index'
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let tk = localStorage.getItem('accessToken')
|
||||||
|
|
||||||
|
if (tk) {
|
||||||
|
store.commit('login', tk);
|
||||||
|
next({
|
||||||
|
name: 'Index'
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (to.meta.requireAuth === true) {
|
||||||
|
if (hasLogin === false) {
|
||||||
|
next({
|
||||||
|
name: 'AuthLogin',
|
||||||
|
params: {
|
||||||
|
next: to.path
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
store.state.showFooter = to.meta.showFooter || false;
|
||||||
|
|
||||||
|
next()
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router
|
||||||
49
src/store/index.js
Normal file
49
src/store/index.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
|
||||||
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
const store = new Vuex.Store({
|
||||||
|
state: {
|
||||||
|
hasLogin: false,
|
||||||
|
accessToken: '',
|
||||||
|
userInfo: null,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
login(state, token) {
|
||||||
|
state.hasLogin = true;
|
||||||
|
state.accessToken = token;
|
||||||
|
localStorage.setItem('accessToken', token)
|
||||||
|
},
|
||||||
|
logout(state) {
|
||||||
|
state.hasLogin = false;
|
||||||
|
state.accessToken = "";
|
||||||
|
state.userInfo = null;
|
||||||
|
localStorage.removeItem('accessToken')
|
||||||
|
localStorage.removeItem('userInfo')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {},
|
||||||
|
actions: {
|
||||||
|
/**
|
||||||
|
* 登录并获取用户基础信息
|
||||||
|
* @param dispatch
|
||||||
|
* @param commit
|
||||||
|
* @param token
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async doLogin({commit}, token) {
|
||||||
|
commit('login', token)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 登出方法
|
||||||
|
* @param commit
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async doLogout({commit}) {
|
||||||
|
commit('logout')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default store
|
||||||
Reference in New Issue
Block a user