This commit is contained in:
2022-05-09 16:33:35 +08:00
commit ec89076dff
27 changed files with 21594 additions and 0 deletions

23
src/App.vue Normal file
View 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
View 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
View 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
View 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
}

View File

@@ -0,0 +1,7 @@
import axios from '../axios'
const index = () => axios.get('agent')
export default {
index
}

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

27
src/main.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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