调整登录页面,rsa登录修改为固定公钥,新增邮箱登录页面
@ -143,7 +143,7 @@
|
||||
<h5>Loading...</h5>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
BIN
public/logo.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
public/verifyimg/10-979x547.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
public/verifyimg/11-979x547.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
public/verifyimg/12-979x547.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
public/verifyimg/13-979x547.jpg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
public/verifyimg/14-979x547.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
public/verifyimg/16-979x547.jpg
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
public/verifyimg/17-979x547.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
public/verifyimg/18-979x547.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
public/verifyimg/19-979x547.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
public/verifyimg/20-979x547.jpg
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
public/verifyimg/28-979x547.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
public/verifyimg/29-979x547.jpg
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
public/verifyimg/33-979x547.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
public/verifyimg/37-979x547.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
@ -7,9 +7,9 @@
|
||||
import useUserStore from './store/modules/user'
|
||||
import useAppStore from './store/modules/app'
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
import zh from 'element-plus/lib/locale/lang/zh-cn' // 中文语言
|
||||
import en from 'element-plus/lib/locale/lang/en' // 英文语言
|
||||
import tw from 'element-plus/lib/locale/lang/zh-tw' //繁体
|
||||
import zh from 'element-plus/es/locale/lang/zh-cn' // 中文语言
|
||||
import en from 'element-plus/es/locale/lang/en' // 英文语言
|
||||
import tw from 'element-plus/es/locale/lang/zh-tw' //繁体
|
||||
import defaultSettings from '@/settings'
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
|
||||
@ -96,16 +96,16 @@ div:focus {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link-type,
|
||||
.link-type:focus {
|
||||
color: var(--el-color-primary);
|
||||
cursor: pointer;
|
||||
// .link-type,
|
||||
// .link-type:focus {
|
||||
// color: var(--el-color-primary);
|
||||
// cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
// color: rgb(32, 160, 255);
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
// &:hover {
|
||||
// // color: rgb(32, 160, 255);
|
||||
// opacity: 0.3;
|
||||
// }
|
||||
// }
|
||||
|
||||
/** 基础通用 **/
|
||||
.pt5 {
|
||||
|
||||
@ -10,18 +10,18 @@
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0px auto 30px auto;
|
||||
text-align: center;
|
||||
// color: #fff;
|
||||
}
|
||||
// .title {
|
||||
// margin: 0px auto 30px auto;
|
||||
// text-align: center;
|
||||
// // color: #fff;
|
||||
// }
|
||||
|
||||
.login-form {
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
// background-color: hsla(0, 0%, 100%, 0.3);
|
||||
width: var(--base-login-width);
|
||||
padding: 25px 15px 5px 15px;
|
||||
width: calc(100% - 30px - 70px);
|
||||
margin: 0 auto;
|
||||
padding: 15px;
|
||||
position: relative;
|
||||
|
||||
.input-icon {
|
||||
@ -31,19 +31,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.login-tip {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
// .login-tip {
|
||||
// font-size: 13px;
|
||||
// text-align: center;
|
||||
// color: #bfbfbf;
|
||||
// }
|
||||
|
||||
.login-code {
|
||||
width: 33%;
|
||||
width: 34%;
|
||||
height: 40px;
|
||||
float: right;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
// width: 100%;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@ -68,6 +68,12 @@
|
||||
}
|
||||
.langSet {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 10px;
|
||||
left: 20px;
|
||||
top: 13px;
|
||||
}
|
||||
|
||||
.link-wrapper {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
506
src/components/DragVerifyImgChip/index.vue
Normal file
@ -0,0 +1,506 @@
|
||||
<template>
|
||||
<div class="drag-verify-container">
|
||||
<div :style="dragVerifyImgStyle">
|
||||
<img ref="checkImg" crossOrigin="anonymous" :src="imgsrc" @load="checkimgLoaded" style="width: 100%" alt="" />
|
||||
<canvas ref="maincanvas" class="main-canvas"></canvas>
|
||||
<canvas ref="movecanvas" :class="{ goFirst: isOk, goKeep: isKeep }" class="move-canvas"></canvas>
|
||||
<div class="refresh" v-if="showRefresh && !isPassing">
|
||||
<el-icon @click="refreshimg">
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="tips success" v-if="showTips && isPassing">{{ successPassTimeTip }}</div>
|
||||
<div class="tips danger" v-if="showTips && !isPassing && showErrorTip">{{ failTip }}</div>
|
||||
</div>
|
||||
<div
|
||||
ref="dragVerify"
|
||||
class="drag_verify"
|
||||
:style="dragVerifyStyle"
|
||||
@mousemove="dragMoving"
|
||||
@mouseup="dragFinish"
|
||||
@mouseleave="dragFinish"
|
||||
@touchmove="dragMoving"
|
||||
@touchend="dragFinish">
|
||||
<div class="dv_progress_bar" :class="{ goFirst2: isOk }" ref="progressBar" :style="progressBarStyle">
|
||||
{{ successMessage }}
|
||||
</div>
|
||||
<div class="dv_text" :style="textStyle" ref="message">
|
||||
{{ message }}
|
||||
</div>
|
||||
<div
|
||||
class="dv_handler dv_handler_bg"
|
||||
:class="{ goFirst: isOk }"
|
||||
@mousedown="dragStart"
|
||||
@touchstart="dragStart"
|
||||
ref="handler"
|
||||
:style="handlerStyle">
|
||||
<el-icon>
|
||||
<DArrowRight />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'dragVerifyImgChip',
|
||||
props: {
|
||||
isPassing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 250
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 40
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: 'swiping to the right side'
|
||||
},
|
||||
successText: {
|
||||
type: String,
|
||||
default: 'success'
|
||||
},
|
||||
background: {
|
||||
type: String,
|
||||
default: '#eee'
|
||||
},
|
||||
progressBarBg: {
|
||||
type: String,
|
||||
default: '#76c61d'
|
||||
},
|
||||
completedBg: {
|
||||
type: String,
|
||||
default: '#76c61d'
|
||||
},
|
||||
circle: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
radius: {
|
||||
type: String,
|
||||
default: '4px'
|
||||
},
|
||||
handlerIcon: {
|
||||
type: String
|
||||
},
|
||||
successIcon: {
|
||||
type: String
|
||||
},
|
||||
handlerBg: {
|
||||
type: String,
|
||||
default: '#fff'
|
||||
},
|
||||
textSize: {
|
||||
type: String,
|
||||
default: '14px'
|
||||
},
|
||||
textColor: {
|
||||
type: String,
|
||||
default: '#333'
|
||||
},
|
||||
imgsrc: {
|
||||
type: String
|
||||
},
|
||||
barWidth: {
|
||||
type: Number,
|
||||
default: 40
|
||||
},
|
||||
barRadius: {
|
||||
type: Number,
|
||||
default: 8
|
||||
},
|
||||
showRefresh: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
refreshIcon: {
|
||||
type: String
|
||||
},
|
||||
showTips: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
successTip: {
|
||||
type: String,
|
||||
default: '验证通过,超过80%用户'
|
||||
},
|
||||
failTip: {
|
||||
type: String,
|
||||
default: '验证未通过,拖动滑块将悬浮图像正确合并'
|
||||
},
|
||||
diffWidth: {
|
||||
type: Number,
|
||||
default: 20
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
const dragEl = this.$refs.dragVerify
|
||||
dragEl.style.setProperty('--textColor', this.textColor)
|
||||
dragEl.style.setProperty('--width', Math.floor(this.width / 2) + 'px')
|
||||
dragEl.style.setProperty('--pwidth', -Math.floor(this.width / 2) + 'px')
|
||||
},
|
||||
computed: {
|
||||
handlerStyle: function () {
|
||||
return {
|
||||
width: this.height + 'px',
|
||||
height: this.height + 'px',
|
||||
background: this.handlerBg
|
||||
}
|
||||
},
|
||||
message: function () {
|
||||
return this.isPassing ? '' : this.text
|
||||
},
|
||||
successMessage: function () {
|
||||
return this.isPassing ? this.successText : ''
|
||||
},
|
||||
dragVerifyStyle: function () {
|
||||
return {
|
||||
width: this.width + 'px',
|
||||
height: this.height + 'px',
|
||||
lineHeight: this.height + 'px',
|
||||
background: this.background,
|
||||
borderRadius: this.circle ? this.height / 2 + 'px' : this.radius
|
||||
}
|
||||
},
|
||||
dragVerifyImgStyle: function () {
|
||||
return {
|
||||
width: this.width + 'px',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}
|
||||
},
|
||||
progressBarStyle: function () {
|
||||
return {
|
||||
background: this.progressBarBg,
|
||||
height: this.height + 'px',
|
||||
borderRadius: this.circle ? this.height / 2 + 'px 0 0 ' + this.height / 2 + 'px' : this.radius
|
||||
}
|
||||
},
|
||||
textStyle: function () {
|
||||
return {
|
||||
height: this.height + 'px',
|
||||
width: this.width + 'px',
|
||||
fontSize: this.textSize
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isMoving: false,
|
||||
x: 0,
|
||||
isOk: false,
|
||||
isKeep: false,
|
||||
clipBarx: 0,
|
||||
showErrorTip: false,
|
||||
passTime: 0,
|
||||
startTime: 0,
|
||||
successPassTimeTip: this.successTip
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
draw: function (ctx, x, y, operation) {
|
||||
var l = this.barWidth
|
||||
var r = this.barRadius
|
||||
const PI = Math.PI
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, y)
|
||||
ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI)
|
||||
ctx.lineTo(x + l, y)
|
||||
ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI)
|
||||
ctx.lineTo(x + l, y + l)
|
||||
ctx.lineTo(x, y + l)
|
||||
ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true)
|
||||
ctx.lineTo(x, y)
|
||||
ctx.lineWidth = 2
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'
|
||||
ctx.stroke()
|
||||
ctx[operation]()
|
||||
ctx.globalCompositeOperation = 'destination-over'
|
||||
},
|
||||
checkimgLoaded: function () {
|
||||
// 生成图片缺失位置
|
||||
var barWidth = this.barWidth
|
||||
var imgHeight = this.$refs.checkImg.height
|
||||
var imgWidth = this.$refs.checkImg.width
|
||||
var halfWidth = Math.floor(this.width / 2)
|
||||
var refreshHeigth = 25
|
||||
var tipHeight = 20
|
||||
var x = halfWidth + Math.ceil(Math.random() * (halfWidth - barWidth - this.barRadius - 5))
|
||||
var y = refreshHeigth + Math.floor(Math.random() * (imgHeight - barWidth - refreshHeigth - tipHeight))
|
||||
this.$refs.maincanvas.setAttribute('width', imgWidth)
|
||||
this.$refs.maincanvas.setAttribute('height', imgHeight)
|
||||
this.$refs.maincanvas.style.display = 'block'
|
||||
var canvasCtx = this.$refs.maincanvas.getContext('2d')
|
||||
this.draw(canvasCtx, x, y, 'fill')
|
||||
this.clipBarx = x
|
||||
|
||||
var moveCanvas = this.$refs.movecanvas
|
||||
moveCanvas.setAttribute('width', imgWidth)
|
||||
moveCanvas.setAttribute('height', imgHeight)
|
||||
this.$refs.movecanvas.style.display = 'block'
|
||||
const L = barWidth + this.barRadius * 2 + 3 //实际宽度
|
||||
var moveCtx = this.$refs.movecanvas.getContext('2d')
|
||||
moveCtx.clearRect(0, 0, imgWidth, imgHeight)
|
||||
this.draw(moveCtx, x, y, 'clip')
|
||||
moveCtx.drawImage(this.$refs.checkImg, 0, 0, imgWidth, imgHeight)
|
||||
var y = y - this.barRadius * 2 - 1
|
||||
const ImageData = moveCtx.getImageData(x, y, L, L)
|
||||
moveCanvas.setAttribute('width', L)
|
||||
moveCanvas.setAttribute('height', imgHeight)
|
||||
moveCtx.putImageData(ImageData, 0, y)
|
||||
},
|
||||
dragStart: function (e) {
|
||||
// 开始计时
|
||||
this.startTime = new Date().getTime()
|
||||
if (!this.isPassing) {
|
||||
this.isMoving = true
|
||||
this.x = e.pageX || e.touches[0].pageX
|
||||
}
|
||||
this.showBar = true
|
||||
this.showErrorTip = false
|
||||
this.$emit('handlerMove')
|
||||
},
|
||||
dragMoving: function (e) {
|
||||
if (this.isMoving && !this.isPassing) {
|
||||
var _x = (e.pageX || e.touches[0].pageX) - this.x
|
||||
var handler = this.$refs.handler
|
||||
handler.style.left = _x + 'px'
|
||||
this.$refs.progressBar.style.width = _x + this.height / 2 + 'px'
|
||||
this.$refs.movecanvas.style.left = _x + 'px'
|
||||
}
|
||||
},
|
||||
dragFinish: function (e) {
|
||||
if (this.isMoving && !this.isPassing) {
|
||||
var _x = (e.pageX || e.changedTouches[0].pageX) - this.x
|
||||
if (Math.abs(_x - this.clipBarx) > this.diffWidth) {
|
||||
this.isOk = true
|
||||
var that = this
|
||||
setTimeout(function () {
|
||||
that.$refs.handler.style.left = '0'
|
||||
that.$refs.progressBar.style.width = '0'
|
||||
that.$refs.movecanvas.style.left = '0'
|
||||
that.isOk = false
|
||||
}, 500)
|
||||
// 重新计时
|
||||
this.passTime = 0
|
||||
this.$emit('passfail')
|
||||
this.showErrorTip = true
|
||||
} else {
|
||||
// 提示完成秒数
|
||||
const endTime = new Date().getTime()
|
||||
this.passTime = (endTime - this.startTime) / 1000
|
||||
this.successPassTimeTip = '验证通过,用时' + this.passTime + 's'
|
||||
this.passVerify()
|
||||
}
|
||||
this.isMoving = false
|
||||
}
|
||||
},
|
||||
passVerify: function () {
|
||||
this.$emit('update:isPassing', true)
|
||||
this.isMoving = false
|
||||
var handler = this.$refs.handler
|
||||
handler.children[0].className = this.successIcon
|
||||
this.$refs.progressBar.style.background = this.completedBg
|
||||
this.$refs.message.style['-webkit-text-fill-color'] = 'unset'
|
||||
this.$refs.message.style.animation = 'slidetounlock2 3s infinite'
|
||||
this.$refs.progressBar.style.color = '#fff'
|
||||
this.$refs.progressBar.style.fontSize = this.textSize
|
||||
this.isKeep = true
|
||||
setTimeout(() => {
|
||||
this.$refs.movecanvas.style.left = this.clipBarx + 'px'
|
||||
setTimeout(() => {
|
||||
this.isKeep = false
|
||||
this.$refs.maincanvas.style.display = 'none'
|
||||
this.$refs.movecanvas.style.display = 'none'
|
||||
}, 200)
|
||||
}, 100)
|
||||
this.$emit('passcallback')
|
||||
},
|
||||
reset: function () {
|
||||
this.reImg()
|
||||
this.checkimgLoaded()
|
||||
},
|
||||
reImg: function () {
|
||||
this.$emit('update:isPassing', false)
|
||||
const oriData = this.$options.data()
|
||||
for (const key in oriData) {
|
||||
if (Object.prototype.hasOwnProperty.call(oriData, key)) {
|
||||
this[key] = oriData[key]
|
||||
}
|
||||
}
|
||||
var handler = this.$refs.handler
|
||||
var message = this.$refs.message
|
||||
handler.style.left = '0'
|
||||
this.$refs.progressBar.style.width = '0'
|
||||
handler.children[0].className = this.handlerIcon
|
||||
message.style['-webkit-text-fill-color'] = 'transparent'
|
||||
message.style.animation = 'slidetounlock 3s infinite'
|
||||
message.style.color = this.background
|
||||
this.$refs.movecanvas.style.left = '0px'
|
||||
},
|
||||
refreshimg: function () {
|
||||
this.$emit('refresh')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
imgsrc: {
|
||||
immediate: false,
|
||||
handler: function () {
|
||||
this.reImg()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.drag_verify {
|
||||
position: relative;
|
||||
background-color: #e8e8e8;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.drag_verify .dv_handler {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.drag_verify .dv_handler i {
|
||||
color: #666;
|
||||
padding-left: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.drag_verify .dv_handler .el-icon-circle-check {
|
||||
color: #6c6;
|
||||
margin-top: 9px;
|
||||
}
|
||||
|
||||
.drag_verify .dv_progress_bar {
|
||||
position: absolute;
|
||||
height: 34px;
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.drag_verify .dv_text {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
color: transparent;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-o-user-select: none;
|
||||
-ms-user-select: none;
|
||||
background: -webkit-gradient(
|
||||
linear,
|
||||
left top,
|
||||
right top,
|
||||
color-stop(0, var(--textColor)),
|
||||
color-stop(0.4, var(--textColor)),
|
||||
color-stop(0.5, #fff),
|
||||
color-stop(0.6, var(--textColor)),
|
||||
color-stop(1, var(--textColor))
|
||||
);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
-webkit-text-size-adjust: none;
|
||||
animation: slidetounlock 3s infinite;
|
||||
}
|
||||
|
||||
.drag_verify .dv_text * {
|
||||
-webkit-text-fill-color: var(--textColor);
|
||||
}
|
||||
|
||||
.goFirst {
|
||||
left: 0px !important;
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.goKeep {
|
||||
transition: left 0.2s;
|
||||
}
|
||||
|
||||
.goFirst2 {
|
||||
width: 0px !important;
|
||||
transition: width 0.5s;
|
||||
}
|
||||
|
||||
.drag-verify-container {
|
||||
position: relative;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.refresh {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.tips {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.tips.success {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
color: green;
|
||||
}
|
||||
|
||||
.tips.danger {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.main-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.move-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
@-webkit-keyframes slidetounlock {
|
||||
0% {
|
||||
background-position: var(--pwidth) 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: var(--width) 0;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes slidetounlock2 {
|
||||
0% {
|
||||
background-position: var(--pwidth) 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: var(--pwidth) 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
45
src/components/LoginFormTitle/index.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="title-container">
|
||||
<img class="logo" src="/favicon.ico" />
|
||||
<div class="title">
|
||||
<span class="white">企业仓库</span>
|
||||
<span class="blue"> 管理系统</span>
|
||||
</div>
|
||||
<el-tag class="tag" type="info" effect="plain" disable-transitions>BETA</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<script setup name="LoginFormTitle" lang="ts"></script>
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
.title-container {
|
||||
height: 90px;
|
||||
vertical-align: bottom;
|
||||
|
||||
.logo {
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
width: 320px;
|
||||
line-height: 50px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.title .white {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.title .blue {
|
||||
color: rgba(64, 158, 255, 1);
|
||||
font-size: 42px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -4,12 +4,14 @@
|
||||
"password": "密码",
|
||||
"captcha": "验证码",
|
||||
"btnLogin": "登录",
|
||||
"btnLoginLoading": "登录中...",
|
||||
"rememberMe": "记住密码",
|
||||
"loginSuccess": "登录成功",
|
||||
"loginTimeOut": "登录状态已过期,请重新登录",
|
||||
"reLogin": "重新登录",
|
||||
"invalidSession": "无效的会话,或者会话已过期,请重新登录。",
|
||||
"otherLoginWay": "其他登录方式",
|
||||
"register": "注册"
|
||||
"register": "注册",
|
||||
"toLoginByEmail": "邮箱登录"
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import ElementPlus from 'element-plus'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import '@/assets/styles/index.scss' // global css
|
||||
|
||||
import App from './App'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import directive from './directive' // directive
|
||||
// 注册指令
|
||||
@ -28,17 +28,17 @@ import { getDicts } from '@/api/system/dict/data'
|
||||
import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, download } from '@/utils/ruoyi'
|
||||
|
||||
// 分页组件
|
||||
import Pagination from '@/components/Pagination'
|
||||
import Pagination from '@/components/Pagination/index.vue'
|
||||
// 自定义表格工具组件
|
||||
import RightToolbar from '@/components/RightToolbar'
|
||||
import RightToolbar from '@/components/RightToolbar/index.vue'
|
||||
// 文件上传组件
|
||||
import FileUpload from '@/components/FileUpload'
|
||||
import FileUpload from '@/components/FileUpload/index.vue'
|
||||
// 图片上传组件
|
||||
import ImageUpload from '@/components/ImageUpload'
|
||||
import ImageUpload from '@/components/ImageUpload/index.vue'
|
||||
// 图片预览组件
|
||||
import ImagePreview from '@/components/ImagePreview'
|
||||
import ImagePreview from '@/components/ImagePreview/index.vue'
|
||||
// 字典标签组件
|
||||
import DictTag from '@/components/DictTag'
|
||||
import DictTag from '@/components/DictTag/index.vue'
|
||||
// el-date-picker 快捷选项
|
||||
import dateOptions from '@/utils/dateOptions'
|
||||
|
||||
@ -7,9 +7,9 @@ import { isHttp } from '@/utils/validate'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import usePermissionStore from '@/store/modules/permission'
|
||||
NProgress.configure({ showSpinner: false });
|
||||
NProgress.configure({ showSpinner: false })
|
||||
|
||||
const whiteList = ['/login', '/auth-redirect', '/bind', '/register', '/socialLogin'];
|
||||
const whiteList = ['/login', '/loginByEmail', '/auth-redirect', '/bind', '/register', '/socialLogin']
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
NProgress.start()
|
||||
@ -22,22 +22,29 @@ router.beforeEach((to, from, next) => {
|
||||
} else {
|
||||
if (useUserStore().roles.length === 0) {
|
||||
// 判断当前用户是否已拉取完user_info信息
|
||||
useUserStore().getInfo().then(() => {
|
||||
usePermissionStore().generateRoutes().then(accessRoutes => {
|
||||
// 根据roles权限生成可访问的路由表
|
||||
accessRoutes.forEach(route => {
|
||||
if (!isHttp(route.path)) {
|
||||
router.addRoute(route) // 动态添加可访问路由表
|
||||
}
|
||||
})
|
||||
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
|
||||
useUserStore()
|
||||
.getInfo()
|
||||
.then(() => {
|
||||
usePermissionStore()
|
||||
.generateRoutes()
|
||||
.then((accessRoutes) => {
|
||||
// 根据roles权限生成可访问的路由表
|
||||
accessRoutes.forEach((route) => {
|
||||
if (!isHttp(route.path)) {
|
||||
router.addRoute(route) // 动态添加可访问路由表
|
||||
}
|
||||
})
|
||||
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
|
||||
})
|
||||
})
|
||||
}).catch(err => {
|
||||
useUserStore().logOut().then(() => {
|
||||
ElMessage.error(err != undefined ? err : '登录失败')
|
||||
next({ path: '/' })
|
||||
.catch((err) => {
|
||||
useUserStore()
|
||||
.logOut()
|
||||
.then(() => {
|
||||
ElMessage.error(err != undefined ? err : '登录失败')
|
||||
next({ path: '/' })
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
@ -56,4 +63,4 @@ router.beforeEach((to, from, next) => {
|
||||
|
||||
router.afterEach(() => {
|
||||
NProgress.done()
|
||||
})
|
||||
})
|
||||
|
||||
@ -39,6 +39,11 @@ export const constantRoutes = [
|
||||
component: () => import('@/views/login'),
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '/loginByEmail',
|
||||
component: () => import('@/views/loginByEmail'),
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
path: '/sociallogin',
|
||||
component: () => import('@/views/socialLogin'),
|
||||
|
||||
@ -21,27 +21,26 @@ const useUserStore = defineStore('user', {
|
||||
// 登录
|
||||
login(userInfo) {
|
||||
return new Promise((resolve, reject) => {
|
||||
getRsaKey().then((response) => {
|
||||
const publicKey = response.data.publicKey
|
||||
const username = userInfo.username.trim()
|
||||
const password = encryptByPublicKey(userInfo.password, publicKey)
|
||||
const code = userInfo.code
|
||||
const uuid = userInfo.uuid
|
||||
login(username, password, code, uuid)
|
||||
.then((res) => {
|
||||
if (res.code == 200) {
|
||||
setToken(res.data)
|
||||
this.token = res.data
|
||||
resolve() //then处理
|
||||
} else {
|
||||
console.log('login error ', res)
|
||||
reject(res) //catch处理
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
// getRsaKey().then((response) => {
|
||||
const username = userInfo.username.trim()
|
||||
const password = encryptByPublicKey(userInfo.password)
|
||||
const code = userInfo.code
|
||||
const uuid = userInfo.uuid
|
||||
login(username, password, code, uuid)
|
||||
.then((res) => {
|
||||
if (res.code == 200) {
|
||||
setToken(res.data)
|
||||
this.token = res.data
|
||||
resolve() //then处理
|
||||
} else {
|
||||
console.log('login error ', res)
|
||||
reject(res) //catch处理
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
// })
|
||||
})
|
||||
},
|
||||
/**
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
|
||||
import jsrsasign from 'jsrsasign'
|
||||
import { KEYUTIL, KJUR } from 'jsrsasign'
|
||||
// 密钥对生成 http://web.chacuo.net/netrsakeypair
|
||||
|
||||
const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALj0zjON+EVdBsnMcR4Uj+jOYgp5ZipftQZ1utW8KvVioz+RSaotF1JHt59q9SC/mZcWWpbpcEqQ3WyyyCC33msCAwEAAQ=='
|
||||
@ -7,6 +7,17 @@ const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALj0zjON+EVdBsnMcR4Uj+jOYgp5Z
|
||||
const privateKey =
|
||||
'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAuPTOM434RV0GycxxHhSP6M5iCnlmKl+1BnW61bwq9WKjP5FJqi0XUke3n2r1IL+ZlxZalulwSpDdbLLIILfeawIDAQABAkB5PYAtq1KjpWddwPYlkbUEFsWNuCaQgExZ/7KJiN9gGjo/UfUZ3W39Orb8PITIYf1NbasReqgddAcsfJNyoDWBAiEA7K89DyTmbjNSmekdD3rejRDdMzzXYtcbo69ZjHoowMUCIQDIDN8eg6PcWk4kiRcRYcNEfriUJR7Fg07ellSPv821bwIhAJA5TEyxIJUgQwI0cZfgOELfdtrlBR5ek6IPlNKsEa89AiBbMVroexPQWC41A3VLjChKagXUKpO7b98dIqRLnyCz6wIgP3qpvnO4IOxY7f5XarfCVyIHZJAMt/R1f16P5OkKv+A='
|
||||
|
||||
const publicPem = `
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo7yRx+lfHxy/c7lZFtZn
|
||||
A3GBeQ+9nxBuOcSz/I8l0dP5JZsWVE+jMtmoxnFrTgCsnGuDHMNpSlexUc3IVcsp
|
||||
bC4tSWh/r4Gy9KsIw0tIY0pulKsN/VhV//7dajMV/Dg+Rd5bwGGjWrlWOp2EPVYB
|
||||
HI66uZ3PpEQe3FbEdaLkJRTENbywgGcGo1tqUpqY5BDGAu9IiXwZe4hdM2uniFS2
|
||||
RKcYDovCiJE1lA7yyx9HEUd9PhYMhPMLvIIdPWW2Gqxsi3IZgG25hER0+o3zICgq
|
||||
G/2VbodBM65JTLBY+KnY6H3o80b0v2qwOB7TktVtWQR6Lz8Ud+qrlgeAzvZb5Fmp
|
||||
DQIDAQAB
|
||||
-----END PUBLIC KEY-----`
|
||||
|
||||
// 加密
|
||||
export function encrypt(txt) {
|
||||
const encryptor = new JSEncrypt()
|
||||
@ -21,7 +32,9 @@ export function decrypt(txt) {
|
||||
return encryptor.decrypt(txt) // 对数据进行解密
|
||||
}
|
||||
|
||||
export const encryptByPublicKey = (txt, publicKey) => {
|
||||
const pubKey = jsrsasign.KEYUTIL.getKey(publicKey)
|
||||
return jsrsasign.KJUR.crypto.Cipher.encrypt(txt, pubKey)
|
||||
export const encryptByPublicKey = (txt: string, publicKey: string = publicPem) => {
|
||||
const pubKey = KEYUTIL.getKey(publicKey)
|
||||
// const ciphertext = KJUR.crypto.Cipher.encrypt(txt, pubKey, 'RSA')
|
||||
const ciphertext = pubKey.encrypt(txt)
|
||||
return btoa(atob(ciphertext).padStart(128, '\0'))
|
||||
}
|
||||
11
src/utils/useCurrentInstance.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { ComponentCustomProperties, ComponentInternalInstance, getCurrentInstance } from 'vue'
|
||||
interface UseCurrentInstance {
|
||||
globalProperties: ComponentCustomProperties & Record<string, any>
|
||||
}
|
||||
export default function useCurrentInstance(): UseCurrentInstance {
|
||||
const { appContext } = getCurrentInstance() as ComponentInternalInstance
|
||||
const globalProperties = appContext.config.globalProperties
|
||||
return {
|
||||
globalProperties
|
||||
}
|
||||
}
|
||||
@ -154,19 +154,6 @@
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-dialog title="618爆款" v-model="open" :close-on-click-modal="false" :show-close="false">
|
||||
<div class="promo" v-for="item in promoteList2" :key="item.id">
|
||||
<h3>
|
||||
<el-link type="success" target="_black" :href="item.link" v-html="item.content + '☛☛点我进入☚☚'"></el-link>
|
||||
</h3>
|
||||
<span v-html="item.tip"></span>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="goTarget('https://curl.qcloud.com/4yEoRquq')">主会场</el-button>
|
||||
<el-button @click="open = false">没兴趣</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -177,7 +164,6 @@ const version = defaultSettings.version
|
||||
function goTarget(url) {
|
||||
window.open(url, '__blank')
|
||||
}
|
||||
const open = ref(true)
|
||||
const promoteList2 = ref([
|
||||
{
|
||||
id: 1,
|
||||
|
||||
@ -1,56 +1,71 @@
|
||||
<template>
|
||||
<starBackground></starBackground>
|
||||
<starBackground />
|
||||
<div class="login">
|
||||
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
|
||||
<h3 class="title">{{ defaultSettings.title }}</h3>
|
||||
|
||||
<LangSelect title="多语言设置" class="langSet" />
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="loginForm.username" type="text" auto-complete="off" :placeholder="$t('login.account')">
|
||||
<template #prefix>
|
||||
<svg-icon name="user" class="input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="loginForm.password" type="password" auto-complete="off" :placeholder="$t('login.password')" @keyup.enter="handleLogin">
|
||||
<template #prefix>
|
||||
<svg-icon name="password" class="input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code" v-if="captchaOnOff != 'off'">
|
||||
<el-input v-model="loginForm.code" auto-complete="off" :placeholder="$t('login.captcha')" style="width: 63%" @keyup.enter="handleLogin">
|
||||
<template #prefix>
|
||||
<svg-icon name="validCode" class="input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="login-code">
|
||||
<img :src="codeUrl" @click="getCode" class="login-code-img" />
|
||||
<div class="login-wrapper">
|
||||
<LoginFormTitle />
|
||||
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
|
||||
<div class="link-wrapper">
|
||||
<LangSelect title="多语言设置" class="langSet" />
|
||||
<el-text type="primary" style="margin: 0 10px">
|
||||
<router-link :to="'/register'">{{ $t('login.register') }}</router-link>
|
||||
</el-text>
|
||||
<el-text type="primary">
|
||||
<router-link :to="'/loginByEmail'">{{ $t('login.toLoginByEmail') }}</router-link>
|
||||
</el-text>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<el-checkbox v-model="loginForm.rememberMe">{{ $t('login.rememberMe') }}</el-checkbox>
|
||||
<span style="font-size: 12px">
|
||||
<span @click="handleForgetPwd()" class="forget-pwd">忘记密码</span>
|
||||
<router-link class="link-type" :to="'/register'">{{ $t('login.register') }}</router-link>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<el-form-item style="width: 100%">
|
||||
<el-button :loading="loading" size="default" type="primary" style="width: 100%" @click.prevent="handleLogin">
|
||||
<span v-if="!loading">{{ $t('login.btnLogin') }}</span>
|
||||
<span v-else>登 录 中...</span>
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<div class="other-login" v-if="defaultSettings.showOtherLogin">
|
||||
<el-divider>{{ $t('login.otherLoginWay') }}</el-divider>
|
||||
|
||||
<span @click="onAuth('GITHUB')" title="github"><svg-icon name="github" className="login-icon"></svg-icon></span>
|
||||
<span @click="onAuth('GITEE')" title="gitee"><svg-icon name="gitee" className="login-icon"></svg-icon></span>
|
||||
</div>
|
||||
</el-form>
|
||||
<el-divider style="margin: 12px 0" />
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="loginForm.username" type="text" size="large" auto-complete="on" :placeholder="$t('login.account')">
|
||||
<template #prefix>
|
||||
<svg-icon name="user" class="el-input__icon input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
:placeholder="$t('login.password')"
|
||||
@keyup.enter="handleLogin">
|
||||
<template #prefix>
|
||||
<svg-icon name="password" class="el-input__icon input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code" v-if="captchaOnOff != 'off'">
|
||||
<el-input
|
||||
v-model="loginForm.code"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
:placeholder="$t('login.captcha')"
|
||||
style="width: 66%"
|
||||
@keyup.enter="handleLogin">
|
||||
<template #prefix>
|
||||
<svg-icon name="validCode" class="el-input__icon input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="login-code">
|
||||
<img :src="codeUrl" @click="getCode" class="login-code-img" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<div class="link-wrapper" style="text-align: left">
|
||||
<el-checkbox v-model="loginForm.rememberMe">{{ $t('login.rememberMe') }}</el-checkbox>
|
||||
</div>
|
||||
<el-form-item>
|
||||
<el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
|
||||
<span v-if="!loading">{{ $t('login.btnLogin') }}</span>
|
||||
<span v-else>{{ $t('login.btnLoginLoading') }}</span>
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<div class="other-login" v-if="defaultSettings.showOtherLogin">
|
||||
<el-divider>{{ $t('login.otherLoginWay') }}</el-divider>
|
||||
<span @click="onAuth('GITHUB')" title="github"><svg-icon name="github" className="login-icon"></svg-icon></span>
|
||||
<span @click="onAuth('GITEE')" title="gitee"><svg-icon name="gitee" className="login-icon"></svg-icon></span>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 底部 -->
|
||||
<div class="el-login-footer">
|
||||
@ -67,7 +82,7 @@ import defaultSettings from '@/settings'
|
||||
import starBackground from '@/views/components/starBackground.vue'
|
||||
import LangSelect from '@/components/LangSelect/index.vue'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
import LoginFormTitle from '@/components/LoginFormTitle/index.vue'
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@ -167,17 +182,17 @@ getCookie()
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/login.scss';
|
||||
.forget-pwd {
|
||||
color: #ccc;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.login-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
// .forget-pwd {
|
||||
// color: #ccc;
|
||||
// margin-right: 10px;
|
||||
// cursor: pointer;
|
||||
// }
|
||||
// .login-icon {
|
||||
// width: 30px;
|
||||
// height: 30px;
|
||||
// margin-right: 20px;
|
||||
// cursor: pointer;
|
||||
// }
|
||||
.other-login {
|
||||
padding: 0px 10px 5px;
|
||||
}
|
||||
|
||||
344
src/views/loginByEmail.vue
Normal file
@ -0,0 +1,344 @@
|
||||
<template>
|
||||
<starBackground />
|
||||
<div class="login">
|
||||
<div class="login-wrapper">
|
||||
<LoginFormTitle />
|
||||
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
|
||||
<div class="link-wrapper">
|
||||
<LangSelect title="多语言设置" class="langSet" />
|
||||
<!-- <el-text type="primary" style="margin: 0 10px">-->
|
||||
<!-- <router-link :to="'/register'">{{ $t('login.register') }}</router-link>-->
|
||||
<!-- </el-text>-->
|
||||
<el-text type="primary">
|
||||
<router-link :to="'/login'">{{ $t('loginByEmail.toLogin') }}</router-link>
|
||||
</el-text>
|
||||
</div>
|
||||
<el-divider style="margin: 12px 0" />
|
||||
<el-form-item prop="email">
|
||||
<el-input v-model="loginForm.email" type="text" size="large" auto-complete="off" :placeholder="$t('loginByEmail.email')">
|
||||
<template #prefix>
|
||||
<svg-icon name="email" class="el-input__icon input-icon" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code">
|
||||
<!-- style="width: 66%" -->
|
||||
<el-input v-model="loginForm.code" size="large" auto-complete="off" :placeholder="$t('login.captcha')" @keyup.enter="handleLogin">
|
||||
<template #prefix>
|
||||
<svg-icon name="validCode" class="el-input__icon input-icon" />
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button @click="getDragVerify" :loading="checkDragVerify.loading">
|
||||
{{ checkDragVerify.text }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<!-- <el-button class="login-code-email" color="#626aef" size="large" @click="getDragVerify" :loading="checkDragVerify.loading">
|
||||
{{ checkDragVerify.text }}
|
||||
</el-button> -->
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
|
||||
<span v-if="!loading">{{ $t('login.btnLogin') }}</span>
|
||||
<span v-else>{{ $t('login.btnLoginLoading') }}</span>
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="el-login-footer">
|
||||
<div v-html="defaultSettings.copyright"></div>
|
||||
</div>
|
||||
<!-- 滑块验证 -->
|
||||
<el-dialog v-model="dragVerify" destroy-on-close width="440px" top="30vh">
|
||||
<DragVerifyImgChip
|
||||
:imgsrc="verifyImg.src"
|
||||
v-model:isPassing="isPassing"
|
||||
text="请按住滑块拖动"
|
||||
successText="验证通过"
|
||||
:width="400"
|
||||
:showRefresh="true"
|
||||
@refresh="refreshImg"
|
||||
@passcallback="verifyPass"
|
||||
style="border: 1px solid rgb(238, 238, 238)">
|
||||
</DragVerifyImgChip>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="loginByEmail" lang="ts">
|
||||
// import { getMailCode, getEmailVerifyImg, getRsaKey } from '@/api/system/login'
|
||||
import defaultSettings from '@/settings'
|
||||
import starBackground from '@/views/components/starBackground.vue'
|
||||
import LangSelect from '@/components/LangSelect/index.vue'
|
||||
import LoginFormTitle from '@/components/LoginFormTitle/index.vue'
|
||||
import DragVerifyImgChip from '@/components/DragVerifyImgChip/index.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useCurrentInstance from '@/utils/useCurrentInstance'
|
||||
import userUserStore from '@/store/modules/user'
|
||||
import { encryptByPublicKey } from '@/utils/jsencrypt'
|
||||
const { globalProperties } = useCurrentInstance()
|
||||
const userStore = userUserStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
interface checkDragVerify {
|
||||
loading: boolean
|
||||
duration: number
|
||||
timer: any
|
||||
text: string
|
||||
}
|
||||
interface loginForm {
|
||||
email: string
|
||||
code?: string
|
||||
uuid?: string
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const seconds = 30
|
||||
const checkEmail = (_rule, value, cb) => {
|
||||
//验证邮箱的正则表达式
|
||||
const regEmail = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/
|
||||
if (regEmail.test(value)) {
|
||||
//合法的邮箱
|
||||
return cb()
|
||||
}
|
||||
cb(new Error('请输入合法的邮箱'))
|
||||
}
|
||||
const loginRules = {
|
||||
code: [{ required: true, trigger: 'change', message: t('loginByEmail.loginRules.dragVerifyRequired') }],
|
||||
email: [
|
||||
{ required: true, trigger: 'blur', message: t('loginByEmail.loginRules.emailRequired') },
|
||||
{ validator: checkEmail, trigger: 'blur', message: t('loginByEmail.loginRules.emailFormat') }
|
||||
]
|
||||
}
|
||||
|
||||
const loginRef = ref()
|
||||
const loading = ref<boolean>(false)
|
||||
const dragVerify = ref(false)
|
||||
// const verifyImg = ref('https://picsum.photos/979/547' + '/?image=' + Math.round(Math.random() * 20))
|
||||
// const /v.jpg
|
||||
const isPassing = ref(false)
|
||||
const loginForm = ref<loginForm>({
|
||||
email: '',
|
||||
code: '',
|
||||
uuid: ''
|
||||
})
|
||||
const checkDragVerify = reactive<checkDragVerify>({
|
||||
loading: false,
|
||||
duration: seconds,
|
||||
timer: null,
|
||||
text: t('loginByEmail.getDragVerify')
|
||||
})
|
||||
interface verifyImg {
|
||||
src: string
|
||||
array: Array<string>
|
||||
flag: number
|
||||
}
|
||||
const verifyImg: verifyImg = reactive({
|
||||
src: '',
|
||||
array: [],
|
||||
flag: 1
|
||||
})
|
||||
// 先存到数组中,然后刷新取下一个
|
||||
function getVerifyImg() {
|
||||
// getEmailVerifyImg().then((res) => {
|
||||
// verifyImg.array = res.data
|
||||
// if (verifyImg.array.length === 0) {
|
||||
// verifyImg.array.push('/v.jpg')
|
||||
// }
|
||||
// verifyImg.src = verifyImg.array[0]
|
||||
// // resolve(verifyImg)
|
||||
// })
|
||||
verifyImg.array = [
|
||||
'/verifyimg/10-979x547.jpg',
|
||||
'/verifyimg/11-979x547.jpg',
|
||||
'/verifyimg/12-979x547.jpg',
|
||||
'/verifyimg/13-979x547.jpg',
|
||||
'/verifyimg/14-979x547.jpg',
|
||||
'/verifyimg/16-979x547.jpg',
|
||||
'/verifyimg/17-979x547.jpg',
|
||||
'/verifyimg/18-979x547.jpg',
|
||||
'/verifyimg/19-979x547.jpg',
|
||||
'/verifyimg/20-979x547.jpg',
|
||||
'/verifyimg/28-979x547.jpg',
|
||||
'/verifyimg/29-979x547.jpg',
|
||||
'/verifyimg/33-979x547.jpg',
|
||||
'/verifyimg/37-979x547.jpg'
|
||||
]
|
||||
verifyImg.src = verifyImg.array[0]
|
||||
}
|
||||
//#region
|
||||
// const verifyImgArrayFlag = ref(0)
|
||||
// 目前问题,第一次为undefined,第二次为数组
|
||||
// 第一次取数组o下标,第二次刷新再取1,到数组长度时,又重新取0
|
||||
//#endregion
|
||||
/**
|
||||
* @description: 刷新图片
|
||||
*/
|
||||
function refreshImg() {
|
||||
// 标量小于数组长度,直接取
|
||||
// 变量初始化为1,所以第一次取的时候,数组长度为1,所以会重新获取数组
|
||||
if (verifyImg.flag < verifyImg.array.length) {
|
||||
verifyImg.src = verifyImg.array[verifyImg.flag]
|
||||
verifyImg.flag++
|
||||
} else {
|
||||
verifyImg.flag = 0
|
||||
verifyImg.src = verifyImg.array[verifyImg.flag]
|
||||
verifyImg.flag++
|
||||
}
|
||||
// verifyImg.value = 'https://picsum.photos/979/547' + '/?image=' + Math.round(Math.random() * 20)
|
||||
// verifyImgArray.value.shift();
|
||||
}
|
||||
const redirect = ref()
|
||||
redirect.value = route.query.redirect
|
||||
interface roles {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
interface LoginUser {
|
||||
createBy: string
|
||||
createTime: string
|
||||
delFlag: string
|
||||
userId: number
|
||||
userName: string
|
||||
nickName: string
|
||||
phonenumber?: string
|
||||
remark?: string
|
||||
email: string
|
||||
password: string
|
||||
sex: string
|
||||
status: string
|
||||
loginIp: string
|
||||
loginDate: string
|
||||
deptId: number
|
||||
}
|
||||
// const currentLoginUser = ref<LoginUser>()
|
||||
let currentLoginCo: any[] = reactive([])
|
||||
/**
|
||||
* @description: 登录
|
||||
*/
|
||||
function handleLogin() {
|
||||
loginRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
// 调用action的登录方法
|
||||
userStore
|
||||
.loginByEmail(loginForm.value)
|
||||
.then((res) => {
|
||||
if (res !== undefined && res.code === 300) {
|
||||
// 有多个角色,弹出选择角色的弹窗
|
||||
roleSelectDialog.visible = true
|
||||
const currentCoSysTypes = res.data.co.map((item) => item.coSysType)
|
||||
globalProperties.getDicts('co_sys_type').then((response) => {
|
||||
const roles: roles[] = []
|
||||
// 根据当前登录用户关联的企业类型,获取企业类型的字典信息
|
||||
currentCoSysTypes
|
||||
.map((item) => response.data.filter((i) => i.dictValue === item)[0])
|
||||
.forEach((element) => {
|
||||
roles.push({
|
||||
id: element.dictValue,
|
||||
name: element.dictLabel
|
||||
})
|
||||
// currentLoginUser.value = res.data.user
|
||||
})
|
||||
roleSelectDialog.roles = roles
|
||||
})
|
||||
currentLoginCo = reactive(res.data.co)
|
||||
return
|
||||
}
|
||||
globalProperties.$modal.msgSuccess(globalProperties.$t('login.loginSuccess'))
|
||||
router.push({ path: redirect.value || '/' })
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
globalProperties.$modal.msgError(error.msg)
|
||||
loading.value = false
|
||||
// // 重新获取验证码
|
||||
// verifyPass()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
interface roleSelectDialog {
|
||||
visible: boolean
|
||||
roles: roles[]
|
||||
}
|
||||
const roleSelectDialog: roleSelectDialog = reactive({
|
||||
visible: false,
|
||||
roles: []
|
||||
})
|
||||
/**
|
||||
* @description: 企业类型选择弹窗关闭
|
||||
*/
|
||||
function roleSelectDialogClose(): void {
|
||||
loading.value = false
|
||||
loginForm.value.code = ''
|
||||
globalProperties.$modal.msg(t('roleSelectDialog.close'))
|
||||
}
|
||||
/**
|
||||
* @description: 企业类型选择弹窗回调
|
||||
* @param {roles} role 企业类型信息
|
||||
*/
|
||||
function roleSelectDialogCallback(role: roles): void {
|
||||
loading.value = false
|
||||
const { coSysType, coId } = currentLoginCo.find((item) => item.coSysType === role.id)
|
||||
userStore
|
||||
.loginByCoSysType({ coId: coId, cosysType: coSysType }) // , user: currentLoginUser.value
|
||||
.then(() => {
|
||||
router.push({ path: redirect.value || '/' })
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
globalProperties.$modal.msgError(error.msg)
|
||||
loading.value = false
|
||||
// 重新获取验证码
|
||||
// if (captchaOnOff.value) {
|
||||
// getCode()
|
||||
// loginForm.value.code = ''
|
||||
// }
|
||||
})
|
||||
}
|
||||
/**
|
||||
* @description: 获取滑动验证
|
||||
*/
|
||||
function getDragVerify() {
|
||||
loginRef.value.validateField('email', async (valid) => {
|
||||
if (valid) {
|
||||
isPassing.value = false
|
||||
dragVerify.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 验证通过
|
||||
*/
|
||||
function verifyPass() {
|
||||
checkDragVerify.loading = true
|
||||
checkDragVerify.timer && clearInterval(checkDragVerify.timer)
|
||||
checkDragVerify.timer = setInterval(() => {
|
||||
const tmp = checkDragVerify.duration--
|
||||
checkDragVerify.text = `${tmp}` + t('loginByEmail.checkDragVerifyDuration')
|
||||
if (tmp <= 0) {
|
||||
clearInterval(checkDragVerify.timer)
|
||||
checkDragVerify.timer = null
|
||||
checkDragVerify.duration = seconds
|
||||
checkDragVerify.text = t('loginByEmail.getDragVerify')
|
||||
checkDragVerify.loading = false
|
||||
}
|
||||
}, 1000)
|
||||
// getRsaKey().then((response) => {
|
||||
getMailCode({ toUser: encryptByPublicKey(loginForm.value.email) }).then((res) => {
|
||||
// console.log(res)
|
||||
})
|
||||
// })
|
||||
setTimeout(() => {
|
||||
dragVerify.value = false
|
||||
}, 1500)
|
||||
}
|
||||
getVerifyImg()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/login.scss';
|
||||
</style>
|
||||
@ -1,62 +1,88 @@
|
||||
<template>
|
||||
<starBackground></starBackground>
|
||||
<div class="register">
|
||||
<el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" class="register-form">
|
||||
<h3 class="title">{{ title }}</h3>
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="registerForm.username" type="text" auto-complete="off" placeholder="账号">
|
||||
<template #prefix>
|
||||
<svg-icon name="user" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="registerForm.password" type="password" auto-complete="off" placeholder="密码" @keyup.enter="handleRegister">
|
||||
<template #prefix>
|
||||
<svg-icon name="password" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="confirmPassword">
|
||||
<el-input v-model="registerForm.confirmPassword" type="password" auto-complete="off" placeholder="确认密码" @keyup.enter="handleRegister">
|
||||
<template #prefix>
|
||||
<svg-icon name="password" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code" v-if="captchaOnOff">
|
||||
<el-input v-model="registerForm.code" auto-complete="off" placeholder="验证码" style="width: 63%" @keyup.enter="handleRegister">
|
||||
<template #prefix>
|
||||
<svg-icon name="validCode" />
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="register-code ml10">
|
||||
<img :src="codeUrl" @click="getCode" class="register-code-img" />
|
||||
<div class="login">
|
||||
<div class="login-wrapper">
|
||||
<LoginFormTitle />
|
||||
<el-form ref="registerFormRef" :model="registerForm" :rules="registerRules" class="login-form">
|
||||
<div class="link-wrapper">
|
||||
<LangSelect title="多语言设置" class="langSet" />
|
||||
<el-text type="primary">
|
||||
<router-link :to="'/login'">使用已有账户登录</router-link>
|
||||
</el-text>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 100%">
|
||||
<el-button :loading="loading" type="primary" size="large" style="width: 100%" @click.prevent="handleRegister">
|
||||
<span v-if="!loading">注 册</span>
|
||||
<span v-else>注 册 中...</span>
|
||||
</el-button>
|
||||
<div style="float: right">
|
||||
<router-link class="link-type" :to="'/login'">使用已有账户登录</router-link>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-divider style="margin: 12px 0" />
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="registerForm.username" type="text" size="large" auto-complete="off" placeholder="账号">
|
||||
<template #prefix>
|
||||
<svg-icon name="user" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="registerForm.password"
|
||||
type="password"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="密码"
|
||||
@keyup.enter="handleRegister">
|
||||
<template #prefix>
|
||||
<svg-icon name="password" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="registerForm.confirmPassword"
|
||||
type="password"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="确认密码"
|
||||
@keyup.enter="handleRegister">
|
||||
<template #prefix>
|
||||
<svg-icon name="password" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code" v-if="captchaOnOff">
|
||||
<el-input
|
||||
v-model="registerForm.code"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="验证码"
|
||||
style="width: 66%"
|
||||
@keyup.enter="handleRegister">
|
||||
<template #prefix>
|
||||
<svg-icon name="validCode" />
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="login-code ml10">
|
||||
<img :src="codeUrl" @click="getCode" class="login-code-img" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button :loading="loading" type="primary" size="large" style="width: 100%" @click.prevent="handleRegister">
|
||||
<span v-if="!loading">注 册</span>
|
||||
<span v-else>注 册 中...</span>
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<!-- 底部 -->
|
||||
<div class="el-register-footer">
|
||||
<div class="el-login-footer">
|
||||
<div v-html="defaultSettings.copyright"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="register">
|
||||
<script setup name="register" lang="ts">
|
||||
import starBackground from '@/views/components/starBackground.vue'
|
||||
import LangSelect from '@/components/LangSelect/index.vue'
|
||||
import LoginFormTitle from '@/components/LoginFormTitle/index.vue'
|
||||
import { getCodeImg, register } from '@/api/system/login'
|
||||
import defaultSettings from '@/settings'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { proxy } = getCurrentInstance() as any
|
||||
const router = useRouter()
|
||||
const codeUrl = ref('')
|
||||
const registerForm = reactive({
|
||||
@ -70,7 +96,7 @@ const registerForm = reactive({
|
||||
const registerFormRef = ref(null)
|
||||
const loading = ref(false)
|
||||
const captchaOnOff = ref(true)
|
||||
const equalToPassword = (rule, value, callback) => {
|
||||
const equalToPassword = (_rule, value, callback) => {
|
||||
if (registerForm.password !== value) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
} else {
|
||||
@ -102,18 +128,11 @@ const registerRules = reactive({
|
||||
],
|
||||
code: [{ required: true, trigger: 'change', message: '请输入验证码' }]
|
||||
})
|
||||
const copyRight = computed(() => {
|
||||
return defaultSettings.copyright
|
||||
})
|
||||
const title = computed(() => {
|
||||
return defaultSettings.title
|
||||
})
|
||||
|
||||
function getCode() {
|
||||
getCodeImg().then((res) => {
|
||||
codeUrl.value = 'data:image/gif;base64,' + res.data.img
|
||||
registerForm.uuid = res.data.uuid
|
||||
// this.$forceUpdate()
|
||||
})
|
||||
}
|
||||
function handleRegister() {
|
||||
@ -147,63 +166,5 @@ getCode()
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss">
|
||||
.register {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background: radial-gradient(220% 105% at top center, #1b2947 10%, #4b76a7 40%, #81acae 65%, #f7f7b6);
|
||||
}
|
||||
.title {
|
||||
margin: 0px auto 30px auto;
|
||||
text-align: center;
|
||||
// color: #fff;
|
||||
}
|
||||
|
||||
.register-form {
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
width: var(--base-login-width);
|
||||
padding: 25px 15px 5px 15px;
|
||||
.el-input {
|
||||
height: 38px;
|
||||
input {
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
.input-icon {
|
||||
height: 39px;
|
||||
width: 14px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
.register-tip {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
.register-code {
|
||||
width: 33%;
|
||||
height: 38px;
|
||||
float: right;
|
||||
img {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
.el-register-footer {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-family: Arial;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.register-code-img {
|
||||
height: 38px;
|
||||
}
|
||||
@import '@/assets/styles/login.scss';
|
||||
</style>
|
||||
|
||||
62
tsconfig.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// 将每个文件作为单独的模块
|
||||
"isolatedModules": true,
|
||||
// 指定ECMAScript目标版本
|
||||
"target": "esnext",
|
||||
// 指定生成哪个模块系统代码
|
||||
"module": "esnext",
|
||||
// 决定如何处理模块。
|
||||
"moduleResolution": "node",
|
||||
// 决定如何处理模块。
|
||||
// 启用 --strict相当于启用 --noImplicitAny, --noImplicitThis, --alwaysStrict,
|
||||
// --strictNullChecks和 --strictFunctionTypes和--strictPropertyInitialization。
|
||||
"strict": true,
|
||||
"noLib": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
// 允许从没有设置默认导出的模块中默认导入。这并不影响代码的输出,仅为了类型检查。
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strictFunctionTypes": false,
|
||||
// 支持jsx语法
|
||||
"jsx": "preserve",
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
// 生成相应的 .map文件。
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
// 若有未使用的局部变量则抛错。
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": ["dom", "esnext", "DOM.Iterable"],
|
||||
//关闭implicitly has an 'any' type
|
||||
"noImplicitAny": false,
|
||||
// 忽略所有的声明文件( *.d.ts)的类型检查。
|
||||
"skipLibCheck": true,
|
||||
// 要包含的类型声明文件名列表
|
||||
"types": ["vite/client", "element-plus/global"],
|
||||
"removeComments": true,
|
||||
// 解析非相对模块名的基准目录
|
||||
"baseUrl": "./",
|
||||
// 要包含的类型声明文件名列表
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"/@/*": ["src/*"],
|
||||
"#/*": ["src/types/*"],
|
||||
"/#/*": ["src/types/*"]
|
||||
},
|
||||
"outDir": "./"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"vite.config.ts",
|
||||
"auto-imports.d.ts",
|
||||
"components.d.ts",
|
||||
"src/api/article/article.js"
|
||||
],
|
||||
"exclude": ["node_modules", "dist", ".vscode", ".idea"]
|
||||
}
|
||||