安装 vue-i18n 插件
1
npm install --save vue-i18n@'8.24.5'
新建自定义语言配置

新建 scr/lang/ 目录,并添加对应js文件,我这里只支持了中文和英文

src/lang/en.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
route: {
dashboard: 'Dashboard'
},
wifiLoginPage: {
smsTitle: 'SMS Login',
querySmsCode: 'Get Verify code',
GuestTitle: 'Guest Login',
login: 'Login',
return: 'Return',
ScanCode: 'Staff Login',
mobileLogin: 'MobilePhone Login (OA approval First)',
loginSuccess: 'Login Success',
offline: 'offline',
dingTalkLoginTitle: 'DingTalk Login',
loadingLogin: 'Logging in',
authenticating: 'Authenticating...'
}
}
src/lang/zh.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
route: {
dashboard: '首页'
},
wifiLoginPage: {
smsTitle: '短信登录',
querySmsCode: '获取验证码',
GuestTitle: '访客登录',
login: '登录',
return: '返回主界面',
ScanCode: '员工钉钉扫码',
mobileLogin: '手机验证码登录(OA提前审批)',
loginSuccess: '成功登录',
offline: '下线',
dingTalkLoginTitle: '钉钉登录',
loadingLogin: '正在登录',
authenticating: '正在验证身份'
}
}
src/lang/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import Vue from 'vue' // 引入Vue
import VueI18n from 'vue-i18n' // 引入国际化的插件包
import Cookies from 'js-cookie' // 引入 Cookies 保存当前默认语言选项
import elementEnLocale from 'element-ui/lib/locale/lang/en' // element-ui 英文包
import elementZhLocale from 'element-ui/lib/locale/lang/zh-CN'// element-ui 中文包
// 自定义的中英文配置
import enLocale from './en'
import zhLocale from './zh'
Vue.use(VueI18n) // 全局注册国际化包
// 创建国际化插件的实例
const i18n = new VueI18n({
// 指定语言类型 zh表示中文 en表示英文 set locale 设置默认初始化的语言 i18n
locale: Cookies.get('language') || 'zh',
// 将将elementUI语言包 和自定义语言包 加入到插件语言数据里 set locale messages
messages: {
// 英文环境下的语言数据
en: {
...enLocale,
...elementEnLocale
},
// 中文环境下的语言数据
zh: {
...zhLocale,
...elementZhLocale
}
}
})
export default i18n

src/main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import Vue from 'vue'
import ElementUI from 'element-ui'
import Cookies from 'js-cookie'
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
import 'element-ui/lib/theme-chalk/index.css'
import '@/styles/index.scss' // global css
import App from './App'
import '@/icons' // icon
import '@/permission' // permission control
import i18n from '@/lang/index'

Vue.use(require('vue-moment'))
Vue.config.productionTip = false

// 这里需要配置
Vue.use(ElementUI, {
// set element-ui default size
size: Cookies.get('size') || 'medium',
// 配置elementUI 语言转换关系
i18n: (key, value) => i18n.t(key, value)
})
new Vue({
el: '#app',
router,
store,
i18n, // 这里加上
render: h => h(App)
})
src/store/getters.js
1
language: state => state.app.language, //添加这一行
src/store/modules/app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import Cookies from 'js-cookie'

const state = {
// ...其他配置项省略...
// set locale 设置默认初始化的语言 i18n
language: Cookies.get('language') || 'zh'
}

const mutations = {
// ...其他配置项省略...
SET_LANGUAGE: (state, language) => {
state.language = language
Cookies.set('language', language)
}
}

const actions = {
// ...其他配置项省略...
setLanguage({ commit }, language) {
commit('SET_LANGUAGE', language)
}
}

export default {
namespaced: true,
state,
mutations,
actions
}

注意: app.js 的属性中中配置了命名空间 namespaced: true ,其他组件在通过 this.$store.dispatch() 方法调用时,要在 action 方法名之前加上 前缀路径 app/ ,不然会报错 unknown action type:XXX(未知的操作类型:)。

src/components/LangSelect/index.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<el-dropdown trigger="click" class="international" @command="handleSetLanguage">
<div>
<svg-icon class-name="international-icon" icon-class="language" />
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="zh" :disabled="language==='zh'">中文</el-dropdown-item>
<el-dropdown-item command="en" :disabled="language==='en'">English</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>

<script>
export default {
name: 'LangSelect',
computed: {
language() {
return this.$store.getters.language
}
},
methods: {
handleSetLanguage(lang) {
this.$i18n.locale = lang
this.$store.dispatch('app/setLanguage', lang)
this.$message.success('switch language success')
// 重新刷新页面更改语言
location.reload()
}
}
}
</script>

<style scoped>
::v-deep .international-icon {
font-size: 20px;
cursor: pointer;
vertical-align: -5px!important;
}
</style>

组件中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
<template>
<div id="poster">
<div>
<img src="@/assets/login_images/qingmu_logo.png" alt="" class="logo">
<lang-select class="lang" />
</div>
<div v-if="loginType!=='ding' && loginType !=='loading'" class="login-container">
<el-form ref="postForm" :model="postForm" class="login-form">
<div v-show="loginType==='sms'">
<h3 class="login_title">{{ $t('wifiLoginPage.smsTitle') }}</h3>

<el-form-item prop="mobile" required>
<phoneNumber v-model="inputMobile" size="lg" default-country-code="CN" @update="getInputMobile" />
</el-form-item>
<el-form-item prop="smscode">
<el-input
ref="smscode"
v-model.number="postForm.smscode"
prefix-icon="el-icon-message"
placeholder="验证码"
auto-complete="on"
name="smscode"
type="text"
>
<el-button slot="suffix" type="text" :disabled="!show" @click="onSendCode">
<span v-show="show">{{ $t('wifiLoginPage.querySmsCode') }}</span>
<span v-show="!show">{{ count }} s</span>
</el-button>
</el-input>
</el-form-item>
<el-form-item style="width: 100%">
<el-button type="primary" style="width: 100%;border: none" :disabled="clicked" @click="onSubmit">{{ $t('wifiLoginPage.login') }}</el-button>
</el-form-item>
<el-form-item style="width: 100%">
<el-button type="info" style="width: 100%;border: none" :disabled="clicked" @click="reset">{{ $t('wifiLoginPage.return') }}</el-button>
</el-form-item>
</div>
<div v-show="!loginType && !validLogin">
<h3 class="login_title">{{ $t('wifiLoginPage.GuestTitle') }}</h3>
<el-form-item style="width: 100%">
<el-button type="success" style="width: 100%;background: #41cb6d;border: none; font-size: 20px;" :disabled="clicked" @click="ddLogin">{{ $t('wifiLoginPage.ScanCode') }}</el-button>
</el-form-item>
<el-form-item style="width: 100%">
<el-link :underline="false" :disabled="clicked" style="width: 100%;border: none;font-size: 12px " @click="smsLogin">{{ $t('wifiLoginPage.mobileLogin') }}</el-link>
</el-form-item>
</div>
<div v-if="validLogin">
<h3 class="login_title">{{ $t('wifiLoginPage.loginSuccess') }}</h3>
<el-form-item prop="mobile">
<el-input v-model="inputMobile" prefix-icon="el-icon-user-solid" disabled type="text" />
</el-form-item>
<el-form-item style="width: 100%">
<el-button type="danger" style="width: 100%;border: none" @click="offline">{{ $t('wifiLoginPage.offline') }}</el-button>
</el-form-item>
</div>
</el-form>
</div>
<div v-show="loginType==='ding'" class="login-container">
<h3 class="login_title">{{ $t('wifiLoginPage.dingTalkLoginTitle') }}</h3>
<div id="login_container" />
<el-button type="info" style="width: 100%;border: none" :disabled="clicked" @click="reset">{{ $t('wifiLoginPage.return') }}</el-button>
</div>
<div v-if="loginType==='loading'" class="login-container">
<h3 class="login_title">{{ $t('wifiLoginPage.loadingLogin') }}</h3>
<span class="login_title">{{ $t('wifiLoginPage.authenticating') }}</span>
</div>
</div>
</template>

<script>
import LangSelect from '@/components/LangSelect'
import VuePhoneNumberInput from 'vue-phone-number-input'
import 'vue-phone-number-input/dist/vue-phone-number-input.css'
import { guest_wifi, guest_wifi_offline, send_guest_code } from '@/api/wifi'

export default {
components: {
// eslint-disable-next-line vue/no-unused-components
LangSelect,
phoneNumber: VuePhoneNumberInput
},
data() {
return {
postForm: {
smscode: undefined,
mobile: undefined
},
inputMobile: undefined,
clicked: false,
show: true,
timer: 0,
count: 0,
error_msg: undefined,
ok_msg: undefined,
loginType: undefined,
validLogin: false,
redirect: undefined,
appid: process.env.VUE_APP_ID,
portalUrl: `${process.env.VUE_APP_WEB_URL}/#/guest_wifi`,
dingCodeConfig: {
id: 'login_container',
style: 'border:none;background-color:rgba(0,0,0,0); margin:0 auto; padding: 0 40px 0 0;',
width: '300',
height: '320'
}

}
},
computed: {
getRedirectUrl() {
return encodeURIComponent(this.redirectUrl)
},
getAuthUrl() {
return `https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=${this.appid}&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=${this.getRedirectUrl}`
},
getGoto() {
return encodeURIComponent(this.getAuthUrl)
},
getDingCodeConfig() {
return { ...this.dingCodeConfig, goto: this.getGoto }
}
},
created() {
this.initDingJs()
},
mounted() {
this.userip = this.$route.query.userip
this.nasip = this.$route.query.nasip
this.usermac = this.$route.query.usermac
this.redirectUrl = `${this.portalUrl}?userip=${this.userip}&usermac=${this.usermac}&nasip=${this.nasip}`
if (this.nasip) {
if (this.nasip.indexOf('?code') !== -1) {
this.authorize_wifi()
}
}
},

methods: {
sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time))
},
getInputMobile(val) {
if (val) {
this.postForm.mobile = val['formattedNumber']
// this.inputMobile = val['formattedNumber']
}
},
onSendCode() {
this.$refs.postForm.validate(valid => {
if (valid) {
send_guest_code(this.postForm).then((response) => {
const { code, msg } = response
if (code === 0) {
this.$notify({
title: '成功',
message: msg,
type: 'success',
duration: 5000
})
if (!this.timer) {
this.count = 300
this.show = false
this.timer = setInterval(() => {
if (this.count > 0 && this.count <= 300) {
this.count--
} else {
this.show = true
clearInterval(this.timer)
this.timer = null
}
}, 1000)
}
} else {
this.$notify({
title: '失败',
message: msg,
type: 'error',
duration: 5000
})
}
})
}
})
},
onSubmit() {
this.$refs.postForm.validate(valid => {
if (valid) {
if (!this.postForm.smscode) {
this.$message.warning('请输入验证码...')
return
}
const [nasip] = this.$route.query.nasip.split('?code=')
const data = Object.assign({}, this.postForm, {
'userip': this.$route.query.userip,
'usermac': this.$route.query.usermac,
'nasip': nasip
})
this.clicked = true
this.loginType = 'loading'
guest_wifi(data).then((response) => {
const { code, msg } = response
this.clicked = false
this.inputMobile = response.data
if (code === 0) {
this.sleep(5000).then(() => {
this.loginType = undefined
this.validLogin = true
this.$notify({
title: '登录成功',
message: msg,
type: 'success',
duration: 5000
})
})
} else {
this.loginType = undefined
this.validLogin = false
this.$notify({
title: '失败',
message: msg,
type: 'error',
duration: 5000
})
}
})
}
})
},
offline() {
this.$refs.postForm.validate(valid => {
if (valid) {
const [nasip] = this.$route.query.nasip.split('?code=')
// const nasip = this.queryNasip()
const data = {
'userip': this.$route.query.userip,
'nasip': nasip,
'usermac': this.$route.query.usermac
}
this.clicked = true
guest_wifi_offline(data).then((response) => {
const { code, msg } = response
this.clicked = false
if (code === 0) {
this.validLogin = false
this.postForm.mobile = undefined
} else {
this.validLogin = true
this.$notify({
title: '断开网络失败',
message: msg,
type: 'error',
duration: 5000
})
}
})
}
})
},

initDingJs() {
!(function(window, document) {
function d(a) {
var e; var c = document.createElement('iframe')
var d = 'https://login.dingtalk.com/login/qrcode.htm?goto=' + a.goto
// eslint-disable-next-line no-sequences
d += a.style ? '&style=' + encodeURIComponent(a.style) : '',
d += a.href ? '&href=' + a.href : '',
c.src = d,
c.frameBorder = '0',
c.allowTransparency = 'true',
c.scrolling = 'no',
c.width = a.width ? a.width + 'px' : '365px',
c.height = a.height ? a.height + 'px' : '400px',
e = document.getElementById(a.id),
e.innerHTML = '',
e.appendChild(c)
}

window.DDLogin = d
}(window, document))
},
addDingListener() {
const self = this

const handleLoginTmpCode = function(loginTmpCode) {
window.location.href = self.getAuthUrl + `&loginTmpCode=${loginTmpCode}`
}

const handleMessage = function(event) {
if (event.origin === 'https://login.dingtalk.com') {
handleLoginTmpCode(event.data)
}
}

if (typeof window.addEventListener !== 'undefined') {
window.addEventListener('message', handleMessage, false)
} else if (typeof window.attachEvent !== 'undefined') {
window.attachEvent('onmessage', handleMessage)
}
},
initDingLogin() {
window.DDLogin(this.getDingCodeConfig)
},
queryString() {
const url = window.location.href
const codeIndex = url.indexOf('&state=') // Find the index of 'code=' in the URL
if (codeIndex !== -1) { // Check if 'code=' was found
const codeStart = codeIndex - 32
return url.substring(codeStart, codeIndex !== -1 ? codeIndex : undefined) // Extract the code substring
}
return null
},
queryNasip() {
const url = window.location.href
const codeIndex = url.indexOf('&nasip=') // Find the index of 'code=' in the URL
const codeIndexC = url.indexOf('%3Fcode') // Find the index of 'code=' in the URL
if (codeIndex !== -1) { // Check if 'code=' was found
const codeStart = codeIndex + 7
const codeEnd = codeIndexC
return url.substring(codeStart, codeEnd !== -1 ? codeEnd : undefined) // Extract the code substring
}
return null
},
authorize_wifi() {
this.loginType = 'loading'
const [nasip] = this.$route.query.nasip.split('?code=')
const code = this.queryString()
// const start = window.location.href.indexOf('&state=')
// const end = start - 32
// const code = window.location.href.substring(start, end)
if (code) {
const data = {
'userip': this.$route.query.userip,
'usermac': this.$route.query.usermac,
'nasip': nasip,
'dingcode': code
}
guest_wifi(data).then((response) => {
const { code, msg, data } = response
if (code === 0) {
this.sleep(5000).then(() => {
this.loginType = undefined
this.validLogin = true
this.postForm.mobile = data
})
} else {
this.validLogin = false
this.loginType = undefined
this.$notify({
title: '失败',
message: msg,
type: 'error',
duration: 5000
})
}
}).catch((res) => {
this.validLogin = false
this.loginType = undefined
this.$notify({
title: '失败',
message: '内部错误,请联系系统管理员',
type: 'error',
duration: 5000
})
})
}
},

ddLogin() {
this.loginType = 'ding'
this.addDingListener()
this.initDingLogin()
// this.authorize_wifi()
},
smsLogin() {
this.loginType = 'sms'
},
reset() {
this.loginType = undefined
this.validLogin = false
}
}

}

</script>

<style lang="scss" scoped>
#poster {
margin: 0;
padding: 0;
background: url(~@/assets/login_images/login_cover.0565288.gif) no-repeat center;
height: 100%;
width: 100%;
background-size: cover;
position: fixed;
}

.lang {
margin: 10px;
padding: 10px;
color: #fff;
float: right;
}
.logo {
margin: 10px;
}

.login-container {
border-radius: 15px;
background-clip: padding-box;
margin: 30px auto;
width: 300px;
background: #fff;
padding: 15px 15px 15px 15px;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #ffffff; // 发光效果
}

.login-container .el-input input {
background: transparent;
-webkit-appearance: none;
border-radius: 15px;
padding: 15px 30px 15px 30px !important;
color: #289bcc;
height: 60px;
caret-color: #ffffff !important;
}

.login-container .login-content {
border-radius: 15px;
background-clip: padding-box;
padding: 15px 30px 15px 30px;
text-align: center;
background: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #ffffff;
}

.login_title {
margin: 10px auto 20px auto;
text-align: center;
color: #505458;
}

.alert {
margin: 20px auto;
}

.footer {
color: #fff;
font-size: 12px;
line-height: 2;
opacity: .6;
position: absolute;
left: 0;
bottom: 10px;
width: 100%;
text-align: center;
text-shadow: 0 2px 4px rgba(21,50,101,.6);
}

::v-deep .country-selector__label{
position: absolute;
top: -8px;
cursor: pointer;
left: 11px;
-webkit-transform: translateY(25%);
transform: translateY(25%);
opacity: 0;
-webkit-transition: all .25s cubic-bezier(.645,.045,.355,1);
transition: all .25s cubic-bezier(.645,.045,.355,1);
font-size: 6px;
color: #9a7b25;
}

::v-deep .input-tel__label{
position: absolute;
top: -8px;
cursor: pointer;
left: 13px;
-webkit-transform: translateY(25%);
transform: translateY(25%);
opacity: 0;
-webkit-transition: all .25s cubic-bezier(.645,.045,.355,1);
transition: all .25s cubic-bezier(.645,.045,.355,1);
font-size: 6px;
color: #9a7b25;
}
</style>