This commit is contained in:
2022-05-04 15:41:02 +08:00
commit c76a1850a1
766 changed files with 201246 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

17
VUE-WEB/lions-web/.eslintrc.js vendored Normal file
View 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
VUE-WEB/lions-web/.gitignore vendored Normal file
View 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?

View 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
VUE-WEB/lions-web/babel.config.js vendored Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

11980
VUE-WEB/lions-web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
{
"name": "lions",
"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",
"vue-wechat-title": "^2.0.5",
"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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View 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>推举系统</title>
</head>
<body>
<noscript>
欢迎使用中国狮子联会哈尔滨代表处推举系统
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -0,0 +1,40 @@
<template>
<v-app>
<router-view
v-wechat-title="$route.meta.title"
></router-view>
</v-app>
</template>
<script>
export default {
name: 'app',
}
</script>
<style scoped>
#app {
width: 100%;
height: 100%;
background: #fdfdfd;
}
.hasFooter {
padding-bottom: 50px;
}
/*
* 水平居中
*/
.pack-center {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-box-pack: center;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: -1;
}
</style>

56
VUE-WEB/lions-web/src/api/axios.js vendored Normal 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://api.lions-vote.cnskl.com/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;

11
VUE-WEB/lions-web/src/api/index.js vendored Normal file
View File

@@ -0,0 +1,11 @@
import article from './interfaces/article'
import auth from './interfaces/auth'
import index from './interfaces/index'
import vote from './interfaces/vote'
export default {
article,
index,
auth,
vote
}

View File

@@ -0,0 +1,11 @@
import axios from '../axios'
const index = () => axios.get('articles')
const show = (id) => axios.get('articles/' + id)
const audit = (id) => axios.post('articles/' + id)
export default {
index,
show,
audit
}

View File

@@ -0,0 +1,13 @@
import axios from '../axios'
const license = () => axios.get('auth/license')
const code = (data) => axios.post('auth/code/login', data)
const loginByCode = (data) => axios.post('auth/loginByCode', data)
export default {
license,
code,
loginByCode
}

View File

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

View File

@@ -0,0 +1,12 @@
import axios from '../axios'
const index = () => axios.get('vote')
const show = (id) => axios.get('vote/' + id)
const submit = (id, data) => axios.post('vote/' + id, {data: data})
export default {
index,
show,
submit
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

28
VUE-WEB/lions-web/src/main.js vendored Normal file
View File

@@ -0,0 +1,28 @@
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.use(require('vue-wechat-title'))
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')

View File

@@ -0,0 +1,177 @@
<template>
<v-container class="login">
<div class="login-title">登录</div>
<div class="login-subhead grey--text">
心连心服务队-推举系统
</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&#45;&#45;text"-->
<!-- cols="12"-->
<!-- md="12"-->
<!-- >-->
<!-- 点击"下一步"即表示已阅读并同意并自愿遵守-->
<!-- <span-->
<!-- @click="ishow(1)"-->
<!-- class="blue&#45;&#45;text text&#45;&#45;darken-4"-->
<!-- >服务使用协议</span>以及-->
<!-- <span-->
<!-- @click="ishow(2)"-->
<!-- class="blue&#45;&#45;text text&#45;&#45;darken-4"-->
<!-- >隐私声明</span>-->
<!-- </v-col>-->
<v-col>
<v-btn
:disabled="!valid"
block
style="color: #ffffff"
color="#1f3c84"
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: 20vh 60px;
}
.login-title{
position: relative;
font-size: 28px;
font-weight: bold;
}
.login-subhead{
padding-bottom: 5vh;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,190 @@
<template>
<v-container class="verify">
<input
:disabled="disable"
@input="getVal"
autofocus="autofocus"
class="hidden-input"
type="number"
v-model="verify"
>
<div class="login-title">验证码</div>
<div class="login-subhead grey--text">
验证码已发送至{{username}}请在下方输入框内输入4位数字验证码
</div>
<div class="code-box">
<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>
</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;
}
}
.verify{
position: relative;
background-repeat: no-repeat;
background-image: url("../../assets/logo.png");
background-size: 200px;
background-position: bottom right;
width: 100vw;
height: 100vh;
padding: 20vh 60px;
box-sizing: border-box;
}
.login-title{
position: relative;
font-size: 28px;
font-weight: bold;
}
.login-subhead{
padding-bottom: 5vh;
font-size: 14px;
}
.hidden-input {
position: absolute;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
color: #fff;
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>

View File

@@ -0,0 +1,65 @@
<template>
<div
class="pt-4 pl-4 pr-4"
>
<v-card
class="mb-4"
tile
>
<v-card-title>
文件审阅
</v-card-title>
<v-divider></v-divider>
<v-list three-line subheader>
<v-list-item
v-for="(item, i) in files"
:key="i"
@click="jump('FileShow', {id: item.id})"
>
<v-list-item-content>
<v-list-item-title>{{ item.title }}</v-list-item-title>
<v-list-item-subtitle>{{ item.desc }}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action v-if="item.is">
<v-icon
color="red"
>
mdi-check
</v-icon>
</v-list-item-action>
</v-list-item>
</v-list>
</v-card>
<v-btn
absolute
dark
fab
right
color="#1f3c84"
style="bottom: 16px"
@click="jump('Index')"
>
<v-icon>mdi-home</v-icon>
</v-btn>
</div>
</template>
<script>
export default {
name: "index",
data() {
return {
files: [],
}
},
mounted() {
this.$api.article.index().then(result => {
this.files = result.files
});
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,51 @@
<template>
<div
class="pa-4"
v-if="loaded"
>
<h2>{{ detail.title }}</h2>
<v-subheader class="text-right">{{ detail.desc }}</v-subheader>
<div v-html="detail.content"></div>
<v-btn
color="primary"
block
@click="auditPost"
:disabled="audited"
>
审阅完毕
</v-btn>
</div>
</template>
<script>
export default {
name: "index",
data() {
return {
id: 0,
loaded: false,
detail: [],
audited: false
}
},
mounted() {
this.id = this.$route.params.id;
this.$api.article.show(this.id).then(result => {
this.detail = result.article
this.audited = result.audited
this.loaded = true
})
},
methods: {
auditPost: function () {
this.$api.article.audit(this.id).then(() => {
this.$router.back()
})
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,189 @@
<template>
<div class="mian">
<div class="footer">
<div class="footer-title">尊敬的 {{ user.name }} 狮友</div>
<div class="footer-subtitle">欢迎您参加中国狮子联会哈尔滨心连心服务队2020-2021年度队长副队长秘书和队长团队成员推举会议</div>
<div v-if="dested">
<v-btn
block
>
投票已结束您的投票记录已销毁
</v-btn>
</div>
<div v-else>
<div v-if="vote">
<v-btn
block
x-large
class="footer-btn"
@click="jump('VoteIndex')"
>
开始投票
</v-btn>
</div>
<div v-else>
<v-btn
block
x-large
class="footer-btn"
@click="reload"
>
等待开始
</v-btn>
</div>
</div>
</div>
<v-dialog
v-model="dialog"
width="500"
persistent
>
<v-card
class="cns"
tile
>
<v-card-title>
<div class="flex-grow-1"></div>
签署承诺书及签到环节
<div class="flex-grow-1"></div>
</v-card-title>
<v-card-text v-html="html">
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<div class="flex-grow-1"></div>
<v-btn
@click="signPost"
color="primary"
text
>
我同意并签到
</v-btn>
<div class="flex-grow-1"></div>
</v-card-actions>
</v-card>
</v-dialog>
<!-- <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: [],
dialog: false,
vote: false,
dested: false,
html: ''
}
},
mounted() {
this.$api.index.index().then(result => {
this.user = result.user
this.html = result.license
this.dialog = !result.sign
this.vote = result.vote
this.dested = result.dested
});
},
methods: {
...mapActions(['doLogout']),
reload() {
this.$api.index.index().then(result => {
this.user = result.user
this.html = result.license
this.dialog = !result.sign
this.vote = result.vote
this.dested = result.dested
});
},
signPost: function () {
this.$api.index.sign().then(() => {
this.dialog = false
})
},
logout: function () {
this.toast('退出成功');
this.doLogout();
setTimeout(() => {
router.push({
name: 'AuthLogin'
})
}, 1000)
}
}
}
</script>
<style scoped>
.mian {
background-color: #1f3c84;
height: 100vh;
width: 100vw;
background-image: url("../../assets/back_2.png");
background-position: top center;
background-size: cover;
}
.footer {
position: absolute;
top: 40vh;
width: 100vw;
padding: 30px;
box-sizing: border-box;
color: white;
}
.footer-title,
.footer-subtitle {
text-align: center;
text-shadow: 0 2px 2px rgba(0, 0, 0, .5);
}
.footer-title {
font-weight: bold;
padding-bottom: 10px;
font-size: 20px;
}
.footer-subtitle {
margin-bottom: 20px;
font-size: 14px;
}
.footer-btn {
width: 100%;
height: 45px;
background: white;
color: #1f3c84;
font-size: 18px;
font-weight: bold;
text-align: center;
border-radius: 6px;
}
.cns {
background-image: url("../../assets/logo.png");
background-size: 100%;
background-position: center;
}
.v-card__text p {
margin-bottom: 0px;
}
</style>

View File

@@ -0,0 +1,13 @@
<template>
<router-view></router-view>
</template>
<script>
export default {
name: "common.layout"
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,331 @@
<template>
<div v-if="!loaded">
<v-dialog
v-model="lock"
persistent
width="300"
>
<v-card
color="primary"
dark
>
<v-card-text>
<p class="pt-4">数据加载中请稍后</p>
<v-progress-linear
indeterminate
color="white"
class="mb-0"
></v-progress-linear>
</v-card-text>
</v-card>
</v-dialog>
</div>
<v-container class="mian" v-else>
<!-- 已投票提示信息 -->
<div class="mian-shoeToast" v-if="lock">
<div class="mian-shoeToast-icon">
<v-icon color="#1f3c84" size="58">{{ message.icon }}</v-icon>
</div>
<div class="mian-shoeToast-title">{{ message.msg }}</div>
<div class="mian-shoeToast-content">{{ message.tips }}</div>
<div class="mian-shoeToast-btn" @click="jump('Index')">返回主页</div>
</div>
<!-- 投票列表 -->
<div v-else>
<v-dialog
v-model="loading"
persistent
width="300"
>
<v-card
color="primary"
dark
>
<v-card-text>
<p class="pt-4">数据提交中请稍后</p>
<v-progress-linear
indeterminate
color="white"
class="mb-0"
></v-progress-linear>
</v-card-text>
</v-card>
</v-dialog>
<div class="list">
<div class="vote-title">
中国狮子联会哈尔滨心连心服务队
</div>
<div style="color: #ffffff">
2020-2021年度
</div>
<div class="vote-title">{{ info.title }}</div>
<div class="list-card" v-for="(item, index) in list" :key="index">
<v-img
:src="item.cover"
width="110"
height="160"
position="top center"
style="position: absolute"
>
</v-img>
<div class="list-card-content">
<div class="list-card-content-name">{{ item.name }}</div>
<div class="list-card-content-item"><label>参选职务:</label>{{ item.desc }}</div>
<!-- <div class="list-card-content-item"><label>服务队:</label>{{ item.desc2 }}</div>-->
<!-- :disabled="tomax && !(item.id in selected)"-->
<v-checkbox
v-model="selected"
:value="item.id"
class="radioGroup"
:disabled="tomax && !inArray(item.id)"
>
<template v-slot:label>
<div class="list-radio-text">赞成</div>
</template>
</v-checkbox>
</div>
</div>
</div>
<v-dialog v-model="sure" persistent>
<v-card>
<v-card-title>确认提交</v-card-title>
<!-- <v-card-text>-->
<!-- 确认您选择无误立即提交么-->
<!-- </v-card-text>-->
<v-card-actions>
<v-btn color="primary" text @click="sure=false">我再想想</v-btn>
<v-spacer></v-spacer>
<v-btn color="primary" @click="submitForm">确认提交</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<div class="list-footer">
<div class="list-footer-text">最多可选{{ info.max }}您已经选择{{ selected.length }}</div>
<v-btn
class="list-footer-btn"
:disabled="disable"
@click="sure = true"
>
提交推举票
</v-btn>
</div>
</div>
</v-container>
</template>
<script>
export default {
name: "index",
data() {
return {
loaded: false,
id: 0,
info: [],
list: [],
submitData: [],
lock: true,
selected: [],
disable: true,
loading: false,
message: '',
tomax: false,
sure: false
}
},
watch: {
selected: function () {
if (this.selected.length >= this.info.max) {
this.tomax = true
} else {
this.tomax = false
}
if (this.selected.length > 0 && this.selected.length <= this.info.max) {
this.disable = false;
} else {
this.disable = true;
}
}
},
mounted() {
this.id = this.$route.params.id;
this.$api.vote.show(this.id).then(result => {
this.lock = result.lock
this.info = result.info
this.list = result.list
this.loaded = true
})
},
methods: {
inArray: function (id) {
for (let i in this.selected) {
if (this.selected[i] == id) {
return true
}
}
return false
},
submitForm: function (e) {
e.preventDefault()
this.loading = true
this.$api.vote.submit(this.id, this.selected).then((res) => {
this.message = res
this.loading = false
this.lock = true
})
}
}
}
</script>
<style scoped>
.vote-title {
font-weight: bold;
font-size: 20px;
color: white;
padding-top: 20px;
text-shadow: 0 2px 2px rgba(0, 0, 0, .5);
}
.mian {
min-height: 100vh;
width: 100vw;
background-color: #1f3c84;
background-image: url("../../assets/back_1.png");
background-position: top center;
background-size: 100%;
padding: 0;
}
/* 提示信息 */
.mian-shoeToast {
margin-top: 10vh;
margin-left: 30px;
height: 80vh;
width: calc(100vw - 60px);
background: white;
border-radius: 6px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-box-pack: center;
padding: 30px 20px;
box-sizing: border-box;
}
.mian-shoeToast-icon {
text-align: center;
margin-bottom: 30px;
}
.mian-shoeToast-title {
text-align: center;
font-size: 20px;
font-weight: bold;
color: #1f3c84;
padding-bottom: 20px;
}
.mian-shoeToast-content {
font-size: 14px;
}
.mian-shoeToast-btn {
height: 45px;
background: #1f3c84;
color: white;
line-height: 45px;
font-size: 16px;
font-weight: bold;
text-align: center;
border-radius: 6px;
margin-top: 30px;
}
/* 列表 */
.list-header b {
color: #1f3c84;
margin-left: 5px;
}
.list-footer {
background: #1f3c84;
position: fixed;
bottom: 0;
left: 0;
padding: 15px;
width: 100%;
box-sizing: border-box;
}
.list-footer-text {
text-align: center;
color: white;
line-height: 20px;
padding-bottom: 15px;
font-size: 14px;
}
.list-footer-btn {
font-size: 1rem;
height: 45px !important;
line-height: 45px;
width: 100%;
background: white;
color: #1f3c84;
border-radius: 6px;
font-weight: bold;
}
.list {
padding: 0px 15px 110px;
}
.list-card {
background: white;
margin-top: 15px;
border-radius: 6px;
box-sizing: border-box;
padding: 10px;
position: relative;
min-height: 180px;
}
.list-card-content {
margin-left: 130px;
height: 160px;
position: relative;
}
.radioGroup {
position: absolute;
left: 0;
bottom: -20px;
}
.list-card-content-name {
font-size: 22px;
font-weight: bold;
color: #1f3c84;
}
.list-card-content-item {
padding-top: 5px;
font-size: 14px;
position: relative;
padding-left: 80px;
font-weight: bold;
}
.list-card-content-item label {
position: absolute;
left: 0;
top: 5px;
color: gray;
}
.list-radio-text {
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,315 @@
<template>
<div v-if="!loaded">
<v-dialog
v-model="lock"
persistent
width="300"
>
<v-card
color="primary"
dark
>
<v-card-text>
<p class="pt-4">数据加载中请稍后</p>
<v-progress-linear
indeterminate
color="white"
class="mb-0"
></v-progress-linear>
</v-card-text>
</v-card>
</v-dialog>
</div>
<v-container class="mian" v-else>
<!-- 已投票提示信息 -->
<div class="mian-shoeToast" v-if="lock">
<div class="mian-shoeToast-icon">
<v-icon color="#1f3c84" size="58">{{ message.icon }}</v-icon>
</div>
<div class="mian-shoeToast-title">{{ message.msg }}</div>
<div class="mian-shoeToast-content">{{ message.tips }}</div>
<div class="mian-shoeToast-btn" @click="jump('Index')">返回主页</div>
</div>
<!-- 投票列表 -->
<div v-else>
<v-dialog
v-model="loading"
persistent
width="300"
>
<v-card
color="primary"
dark
>
<v-card-text>
<p class="pt-4">数据提交中请稍后</p>
<v-progress-linear
indeterminate
color="white"
class="mb-0"
></v-progress-linear>
</v-card-text>
</v-card>
</v-dialog>
<div class="list">
<div class="vote-title">
中国狮子联会哈尔滨心连心服务队
</div>
<div style="color: #ffffff">
2020-2021年度
</div>
<div class="vote-title">{{ info.title }}</div>
<div class="list-card" v-for="(item, index) in list" :key="index">
<v-img
:src="item.cover"
width="110"
height="160"
position="top center"
style="position: absolute"
>
</v-img>
<div class="list-card-content">
<div class="list-card-content-name">{{ item.name }}</div>
<div class="list-card-content-item"><label>参选职务:</label>{{ item.desc }}</div>
<!-- <div class="list-card-content-item"><label> :</label>{{ item.desc2 }}</div>-->
<v-radio-group
class="radioGroup"
row
v-model="item.radio"
>
<v-radio
color="#1f3c84"
value="0"
>
<template v-slot:label>
<div class="list-radio-text">不赞成</div>
</template>
</v-radio>
<v-radio
color="#1f3c84"
value="1"
>
<template v-slot:label>
<div class="list-radio-text">赞成</div>
</template>
</v-radio>
</v-radio-group>
</div>
</div>
</div>
<v-dialog v-model="sure" persistent>
<v-card>
<v-card-title>确认提交</v-card-title>
<!-- <v-card-text>-->
<!-- 确认您选择无误立即提交么-->
<!-- </v-card-text>-->
<v-card-actions>
<v-btn color="primary" text @click="sure=false">我再想想</v-btn>
<v-spacer></v-spacer>
<v-btn color="primary" @click="submitForm">确认提交</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<div class="list-footer">
<button
class="list-footer-btn"
:disabled="lock"
@click="sure = true"
>
提交推举票
</button>
</div>
</div>
</v-container>
</template>
<script>
export default {
name: "index",
data() {
return {
loaded: false,
id: 0,
info: [],
list: [],
submitData: [],
lock: true,
loading: false,
message: '',
sure: false
}
},
mounted() {
this.id = this.$route.params.id;
this.$api.vote.show(this.id).then(result => {
this.lock = result.lock
this.info = result.info
this.list = result.list
this.loaded = true
})
},
methods: {
submitForm: function (e) {
this.loading = true
e.preventDefault()
this.submitData = []
this.list.forEach((item) => {
this.submitData.push({
id: item.id,
result: item.radio
})
});
this.$api.vote.submit(this.id, this.submitData).then((res) => {
this.message = res
this.loading = false
this.lock = true
})
}
}
}
</script>
<style scoped>
.vote-title {
font-weight: bold;
font-size: 20px;
color: white;
padding-top: 20px;
text-shadow: 0 2px 2px rgba(0, 0, 0, .5);
}
.mian {
min-height: 100vh;
width: 100vw;
background-color: #1f3c84;
background-image: url("../../assets/back_1.png");
background-position: top center;
background-size: 100%;
padding: 0;
}
/* 提示信息 */
.mian-shoeToast {
margin-top: 10vh;
margin-left: 30px;
height: 80vh;
width: calc(100vw - 60px);
background: white;
border-radius: 6px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-box-pack: center;
padding: 30px 20px;
box-sizing: border-box;
}
.mian-shoeToast-icon {
text-align: center;
margin-bottom: 30px;
}
.mian-shoeToast-title {
text-align: center;
font-size: 20px;
font-weight: bold;
color: #1f3c84;
padding-bottom: 20px;
}
.mian-shoeToast-content {
font-size: 14px;
}
.mian-shoeToast-btn {
height: 45px;
background: #1f3c84;
color: white;
line-height: 45px;
font-size: 16px;
font-weight: bold;
text-align: center;
border-radius: 6px;
margin-top: 30px;
}
/* 列表 */
.list-header b {
color: #1f3c84;
margin-left: 5px;
}
.list-footer {
background: #1f3c84;
position: fixed;
bottom: 0;
left: 0;
padding: 15px;
width: 100%;
box-sizing: border-box;
}
.list-footer-btn {
font-size: 1rem;
height: 45px;
line-height: 45px;
width: 100%;
background: white;
color: #1f3c84;
border-radius: 6px;
font-weight: bold;
}
.list {
padding: 0px 15px 75px;
}
.list-card {
background: white;
margin-top: 15px;
border-radius: 6px;
box-sizing: border-box;
padding: 10px;
position: relative;
min-height: 180px;
}
.list-card-content {
margin-left: 130px;
height: 160px;
position: relative;
}
.radioGroup {
position: absolute;
left: 0;
bottom: -20px;
}
.list-card-content-name {
font-size: 22px;
font-weight: bold;
color: #1f3c84;
}
.list-card-content-item {
padding-top: 5px;
font-size: 14px;
position: relative;
padding-left: 80px;
font-weight: bold;
}
.list-card-content-item label {
position: absolute;
left: 0;
top: 5px;
color: gray;
}
.list-radio-text {
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,179 @@
<template>
<div>
<v-container class="mian" v-if="loaded">
<div v-if="!lock">
<div class="vote-title">
中国狮子联会哈尔滨心连心服务队
</div>
<div style="color: #ffffff">
2020-2021年度
</div>
<div class="vote-title">{{ vote.title }}</div>
<div class="vote-block">
<div class="vote-block-title">
{{ vote.type == 'diff' ? '差额推举' : '等额推举' }}规则说明
</div>
<div class="vote-block-html" v-html="vote.rules"></div>
</div>
<div class="vote-block-btn">
<span @click="jump('Vote_' + vote.type, {id: vote.id})">继续</span>
</div>
</div>
<div class="mian-shoeToast" v-else>
<div class="mian-shoeToast-icon">
<v-icon color="#1f3c84" size="58">mdi-checkbox-marked-circle</v-icon>
</div>
<div class="mian-shoeToast-title">投票成功</div>
<div class="mian-shoeToast-content">您已提交本轮次选票为保密起见已隐藏您的投票结果其他人也无法查询</div>
<div class="mian-shoeToast-btn" @click="jump('Index')">返回主页</div>
</div>
</v-container>
<div v-else>
<v-dialog
v-model="loading"
persistent
width="300"
>
<v-card
color="primary"
dark
>
<v-card-text>
<p class="pt-4">数据加载中请稍后</p>
<v-progress-linear
indeterminate
color="white"
class="mb-0"
></v-progress-linear>
</v-card-text>
</v-card>
</v-dialog>
</div>
</div>
</template>
<script>
export default {
name: "index",
data() {
return {
vote: [],
lock: true,
loaded: false,
loading: true
}
},
mounted() {
this.$api.vote.index().then(result => {
this.vote = result.vote
this.lock = result.lock
this.loaded = true
});
},
}
</script>
<style scoped>
.mian {
min-height: 100vh;
width: 100vw;
background-color: #1f3c84;
background-image: url("../../assets/back_1.png");
background-position: top center;
background-size: 100%;
padding: 0 30px 130px 30px;
}
/* 提示信息 */
.mian-shoeToast {
margin-top: 10vh;
height: 80vh;
width: calc(100vw - 60px);
background: white;
border-radius: 6px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-box-pack: center;
padding: 30px 20px;
box-sizing: border-box;
}
.mian-shoeToast-icon {
text-align: center;
margin-bottom: 30px;
}
.mian-shoeToast-title {
text-align: center;
font-size: 20px;
font-weight: bold;
color: #1f3c84;
padding-bottom: 20px;
}
.mian-shoeToast-content {
font-size: 14px;
}
.mian-shoeToast-btn {
height: 45px;
background: #1f3c84;
color: white;
line-height: 45px;
font-size: 16px;
font-weight: bold;
text-align: center;
border-radius: 6px;
margin-top: 30px;
}
.vote-block {
background: white;
padding: 10px;
box-sizing: border-box;
border-radius: 6px;
}
.vote-block-title {
line-height: 40px;
padding-bottom: 10px;
font-size: 18px;
color: #1f3c84;
font-weight: bold;
text-align: center;
}
.vote-title {
font-weight: bold;
font-size: 20px;
padding-bottom: 10px;
color: white;
padding-top: 20px;
text-shadow: 0 2px 2px rgba(0, 0, 0, .5);
}
.vote-block-btn {
background: #1f3c84;
position: fixed;
left: 0;
bottom: 0;
padding: 30px;
width: 100%;
box-sizing: border-box;
}
.vote-block-btn span {
width: 100%;
height: 45px;
line-height: 45px;
background: white;
color: #1f3c84;
font-size: 1rem;
font-weight: bold;
text-align: center;
border-radius: 6px;
display: block;
}
</style>

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

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>

View File

@@ -0,0 +1,8 @@
const isWechat = () => {
let ua = navigator.userAgent.toLowerCase();
return (/micromessenger/.test(ua)) ? true : false;
}
export default {
isWechat
}

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: {}
})

123
VUE-WEB/lions-web/src/router/index.js vendored Normal file
View File

@@ -0,0 +1,123 @@
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: '/vote',
name: 'VoteIndex',
component: () => import(/* webpackChunkName: "index" */ '@/pages/vote/index'),
meta: {
title: '推举规则',
keepAlive: true,
requireAuth: true
}
}, {
path: '/vote_equal/:id',
name: 'Vote_equal',
component: () => import(/* webpackChunkName: "index" */ '@/pages/vote/equal'),
meta: {
title: '等额推举',
keepAlive: true,
requireAuth: true
}
}, {
path: '/vote_diff/:id',
name: 'Vote_diff',
component: () => import(/* webpackChunkName: "index" */ '@/pages/vote/diff'),
meta: {
title: '差额推举',
keepAlive: true,
requireAuth: true
}
}, {
path: '/files',
name: 'FileIndex',
component: () => import(/* webpackChunkName: "index" */ '@/pages/files/index'),
meta: {
title: '文件审阅',
keepAlive: true,
requireAuth: true
}
}, {
path: '/files/:id',
name: 'FileShow',
component: () => import(/* webpackChunkName: "index" */ '@/pages/files/show'),
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;
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
VUE-WEB/lions-web/src/store/index.js vendored 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