|
@@ -16,44 +16,91 @@
|
|
|
|
|
|
|
|
</div>
|
|
</div>
|
|
|
<div class="login_info_message">后台管理系统登录</div>
|
|
<div class="login_info_message">后台管理系统登录</div>
|
|
|
- <div class="login_info_input">
|
|
|
|
|
- <el-form ref="userInfoForm" @submit.prevent="SubmitLogin">
|
|
|
|
|
- <el-form-item prop="userName">
|
|
|
|
|
- <el-input v-model="userName" placeholder="请输入账号" clearable @keyup.enter="SubmitLogin">
|
|
|
|
|
- <template #prefix>
|
|
|
|
|
- <i class="iconfont icon_zhanghao" />
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-input>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- <el-form-item prop="passWord">
|
|
|
|
|
- <el-input v-model="passWord" type="password" show-password placeholder="请输入密码" @keyup.enter="SubmitLogin">
|
|
|
|
|
- <template #prefix>
|
|
|
|
|
- <i class="iconfont icon_mima" />
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-input>
|
|
|
|
|
- <!-- <el-input v-model="passWord" placeholder="请输入密码" :type="!showPass ? 'password' : 'text'">
|
|
|
|
|
- <template #prefix>
|
|
|
|
|
- <i class="iconfont icon_mima" />
|
|
|
|
|
- </template>
|
|
|
|
|
- <template #suffix>
|
|
|
|
|
- <span @click.stop="showPass = !showPass">
|
|
|
|
|
- <i v-if="showPass" class="iconfont icon_xianshimima"></i>
|
|
|
|
|
- <i v-else class="iconfont icon_buxianshimima"></i>
|
|
|
|
|
- </span>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-input> -->
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- </el-form>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="login_info_item">
|
|
|
|
|
- <div class="item_left">
|
|
|
|
|
- <el-checkbox v-model="remeberPassWord">记住密码</el-checkbox>
|
|
|
|
|
|
|
+ <div v-if="tabActive == 1">
|
|
|
|
|
+ <div class="login_info_message module_tab">
|
|
|
|
|
+ <div class="tab_item">
|
|
|
|
|
+ 账号登录
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="login_info_input">
|
|
|
|
|
+ <el-form ref="userInfoForm" @submit.prevent="SubmitLogin">
|
|
|
|
|
+ <el-form-item prop="userName">
|
|
|
|
|
+ <el-input v-model="userName" placeholder="请输入账号" clearable @keyup.enter="SubmitLogin">
|
|
|
|
|
+ <template #prefix>
|
|
|
|
|
+ <i class="iconfont icon_zhanghao" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-input>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item prop="passWord">
|
|
|
|
|
+ <el-input v-model="passWord" type="password" show-password placeholder="请输入密码" @keyup.enter="SubmitLogin">
|
|
|
|
|
+ <template #prefix>
|
|
|
|
|
+ <i class="iconfont icon_mima" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-input>
|
|
|
|
|
+ <!-- <el-input v-model="passWord" placeholder="请输入密码" :type="!showPass ? 'password' : 'text'">
|
|
|
|
|
+ <template #prefix>
|
|
|
|
|
+ <i class="iconfont icon_mima" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #suffix>
|
|
|
|
|
+ <span @click.stop="showPass = !showPass">
|
|
|
|
|
+ <i v-if="showPass" class="iconfont icon_xianshimima"></i>
|
|
|
|
|
+ <i v-else class="iconfont icon_buxianshimima"></i>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-input> -->
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="login_info_item">
|
|
|
|
|
+ <div class="item_left">
|
|
|
|
|
+ <el-checkbox v-model="remeberPassWord">记住密码</el-checkbox>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="item_right">忘记密码</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="login_info_button">
|
|
|
|
|
+ <el-button style="width:100%;" @click="SubmitLogin()" :loading="loadingLogin">登 录</el-button>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="item_right">忘记密码</div>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="login_info_button">
|
|
|
|
|
- <el-button style="width:100%;" @click="SubmitLogin()" :loading="loadingLogin">登 录</el-button>
|
|
|
|
|
|
|
+ <div v-if="tabActive == 6">
|
|
|
|
|
+ <div class="login_info_message module_tab">
|
|
|
|
|
+ <div class="tab_item">
|
|
|
|
|
+ 手机验证
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="login_info_input">
|
|
|
|
|
+ <el-form>
|
|
|
|
|
+ <el-form-item prop="userName">
|
|
|
|
|
+ <el-input v-model="sliderPhone" readonly placeholder="请输入手机号" maxlength="11">
|
|
|
|
|
+ <template #prefix>
|
|
|
|
|
+ <i class="iconfont icon_shoujihao" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-input>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item prop="passWord">
|
|
|
|
|
+ <el-input v-model="sliderCode" clearable placeholder="请输入验证码" maxlength="6"
|
|
|
|
|
+ @input="HandleSliderCodeInput">
|
|
|
|
|
+ <template #prefix>
|
|
|
|
|
+ <i class="iconfont icon_yanzhengma" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #suffix>
|
|
|
|
|
+ <span :class="isCount ? 'code_gray' : 'get_code'" @click="GetBindPhoneCode">{{ codeText }}</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-input>
|
|
|
|
|
+
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="login_info_item">
|
|
|
|
|
+ <div>
|
|
|
|
|
+
|
|
|
|
|
+ 验证码5分钟内有效,请注意查收短信。
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="login_info_button">
|
|
|
|
|
+ <el-button style="width:100%;" @click="SubmitCodeLogin()" :loading="loadingBind">登 录</el-button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
<div class="login_other_type">
|
|
<div class="login_other_type">
|
|
|
<div class="other_login_line">
|
|
<div class="other_login_line">
|
|
|
<span class="line"></span>
|
|
<span class="line"></span>
|
|
@@ -77,165 +124,306 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
|
|
+ <SlideCode v-model="showSlideCode" title="安全验证" :sliderUUID="sliderUUID" :sliderBgUrl="sliderBgUrl"
|
|
|
|
|
+ :sliderPositionY="sliderPositionY" :sliderImgUrl="sliderImgUrl" @close="CloseSlideCode"
|
|
|
|
|
+ @refresh="ObtainSliderImg" />
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
-<script setup>
|
|
|
|
|
-import { ref, onMounted } from 'vue'
|
|
|
|
|
-// import particles from 'particles.js'
|
|
|
|
|
-import { login, getUserInfo } from '@/api/login'
|
|
|
|
|
-import { ElMessage } from 'element-plus' //
|
|
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { ref, onMounted, onBeforeUnmount } from 'vue'
|
|
|
|
|
+import { loginSignIn, getUserInfo, bindDeviceCode, getSlider,loginInByCode } from '@/api/login'
|
|
|
|
|
+import { getStableDeviceFingerprint } from '@/utils/deviceFingerprint'
|
|
|
|
|
+import SlideCode from '@/components/SlideCode.vue' // 滑动验证码组件
|
|
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
import { useRouter } from 'vue-router'
|
|
import { useRouter } from 'vue-router'
|
|
|
|
|
|
|
|
|
|
+/** 菜单项类型 */
|
|
|
|
|
+interface MenuItem {
|
|
|
|
|
+ route: string
|
|
|
|
|
+ [key: string]: unknown
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-const router = useRouter() // 获取路由实例
|
|
|
|
|
-// 响应式数据
|
|
|
|
|
-const userName = ref('')
|
|
|
|
|
-const passWord = ref('')
|
|
|
|
|
-const remeberPassWord = ref(false)
|
|
|
|
|
-const loadingLogin = ref(false)
|
|
|
|
|
-const showPass = ref(false)
|
|
|
|
|
-// 粒子动画初始化
|
|
|
|
|
-const InitParticles = () => {
|
|
|
|
|
- const particlesConfig = {
|
|
|
|
|
- particles: {
|
|
|
|
|
- number: {
|
|
|
|
|
- value: 60,
|
|
|
|
|
- density: {
|
|
|
|
|
- enable: true,
|
|
|
|
|
- value_area: 800
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- color: {
|
|
|
|
|
- value: '#ffffff'
|
|
|
|
|
- },
|
|
|
|
|
- shape: {
|
|
|
|
|
- type: ['circle'],
|
|
|
|
|
- stroke: {
|
|
|
|
|
- width: 0,
|
|
|
|
|
- color: '#000000'
|
|
|
|
|
- },
|
|
|
|
|
- polygon: {
|
|
|
|
|
- nb_sides: 5
|
|
|
|
|
- },
|
|
|
|
|
- image: {
|
|
|
|
|
- src: 'img/github.svg',
|
|
|
|
|
- width: 100,
|
|
|
|
|
- height: 100
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- opacity: {
|
|
|
|
|
- value: 0.5,
|
|
|
|
|
- random: false,
|
|
|
|
|
- anim: {
|
|
|
|
|
- enable: false,
|
|
|
|
|
- speed: 1,
|
|
|
|
|
- opacity_min: 0.1,
|
|
|
|
|
- sync: false
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- size: {
|
|
|
|
|
- value: 3,
|
|
|
|
|
- random: false,
|
|
|
|
|
- anim: {
|
|
|
|
|
- enable: true,
|
|
|
|
|
- speed: 40,
|
|
|
|
|
- size_min: 0.1,
|
|
|
|
|
- sync: false
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- line_linked: {
|
|
|
|
|
- enable: true,
|
|
|
|
|
- distance: 150,
|
|
|
|
|
- color: '#ffffff',
|
|
|
|
|
- opacity: 0.4,
|
|
|
|
|
- width: 1
|
|
|
|
|
- },
|
|
|
|
|
- move: {
|
|
|
|
|
- enable: true,
|
|
|
|
|
- speed: 6,
|
|
|
|
|
- direction: 'none',
|
|
|
|
|
- random: false,
|
|
|
|
|
- straight: false,
|
|
|
|
|
- out_mode: 'out',
|
|
|
|
|
- bounce: false,
|
|
|
|
|
- attract: {
|
|
|
|
|
- enable: false,
|
|
|
|
|
- rotateX: 600,
|
|
|
|
|
- rotateY: 1200
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- interactivity: {
|
|
|
|
|
- detect_on: 'canvas',
|
|
|
|
|
- events: {
|
|
|
|
|
- onhover: {
|
|
|
|
|
- enable: true,
|
|
|
|
|
- mode: 'grab'
|
|
|
|
|
- },
|
|
|
|
|
- onclick: {
|
|
|
|
|
- enable: true,
|
|
|
|
|
- mode: 'push'
|
|
|
|
|
- },
|
|
|
|
|
- resize: true
|
|
|
|
|
- },
|
|
|
|
|
- modes: {
|
|
|
|
|
- grab: {
|
|
|
|
|
- distance: 140,
|
|
|
|
|
- line_linked: {
|
|
|
|
|
- opacity: 1
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- bubble: {
|
|
|
|
|
- distance: 400,
|
|
|
|
|
- size: 40,
|
|
|
|
|
- duration: 2,
|
|
|
|
|
- opacity: 8,
|
|
|
|
|
- speed: 3
|
|
|
|
|
- },
|
|
|
|
|
- repulse: {
|
|
|
|
|
- distance: 200,
|
|
|
|
|
- duration: 0.4
|
|
|
|
|
- },
|
|
|
|
|
- push: {
|
|
|
|
|
- particles_nb: 4
|
|
|
|
|
- },
|
|
|
|
|
- remove: {
|
|
|
|
|
- particles_nb: 2
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- retina_detect: true
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const router = useRouter()
|
|
|
|
|
|
|
|
- particlesJS('particles', particlesConfig, () => {
|
|
|
|
|
- console.log('粒子动画特效加载成功')
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// 响应式数据
|
|
|
|
|
+const userName = ref<string>('')
|
|
|
|
|
+const passWord = ref<string>('')
|
|
|
|
|
+const remeberPassWord = ref<boolean>(false)
|
|
|
|
|
+const loadingLogin = ref<boolean>(false)
|
|
|
|
|
|
|
|
|
|
+const tabActive = ref<number>(1)// 登录类型 1:账号登录 6:手机验证
|
|
|
|
|
+/** 设备指纹(用于登录设备识别) */
|
|
|
|
|
+const deviceFingerprint = ref<string>('')
|
|
|
|
|
+const menuList = ref<MenuItem[]>([])
|
|
|
|
|
+const showSlideCode = ref<boolean>(false)
|
|
|
|
|
+const sliderUUID = ref<string>('')
|
|
|
|
|
+const sliderBgUrl = ref<string>('')
|
|
|
|
|
+const sliderPositionY = ref<number>(0)
|
|
|
|
|
+const sliderImgUrl = ref<string>('')
|
|
|
|
|
+/** 滑动验证状态:0=未开始,1=验证成功,-1=验证失败 */
|
|
|
|
|
+const sliderVerified = ref<number>(0)
|
|
|
|
|
+/** 滑块验证成功后后端返回的手机号 */
|
|
|
|
|
+const sliderPhone = ref<string>('')
|
|
|
|
|
+const sliderCode = ref<string>('')
|
|
|
|
|
+const loadingBind = ref<boolean>(false)
|
|
|
|
|
+const isCount = ref<boolean>(false) // 验证码倒计时状态
|
|
|
|
|
+const codeText = ref<string>('获取验证码') // 验证码倒计时文本
|
|
|
|
|
+// 页面加载时预获取设备指纹 + 恢复记住的密码
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
-// InitParticles()
|
|
|
|
|
|
|
+ // 预加载设备指纹(非阻塞,登录时若未完成会再次等待)
|
|
|
|
|
+ getStableDeviceFingerprint()
|
|
|
|
|
+ .then((fp) => {
|
|
|
|
|
+ deviceFingerprint.value = fp
|
|
|
|
|
+ console.log('📱 当前设备指纹:', fp)
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch((error) => {
|
|
|
|
|
+ console.error('获取设备指纹失败:', error)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 恢复记住的密码
|
|
|
const storedUsername = localStorage.getItem('userName')
|
|
const storedUsername = localStorage.getItem('userName')
|
|
|
const storedPassword = localStorage.getItem('passWord')
|
|
const storedPassword = localStorage.getItem('passWord')
|
|
|
const storedRememberMe = localStorage.getItem('rememberMe')
|
|
const storedRememberMe = localStorage.getItem('rememberMe')
|
|
|
|
|
|
|
|
if (storedRememberMe === 'true') {
|
|
if (storedRememberMe === 'true') {
|
|
|
- userName.value = storedUsername
|
|
|
|
|
- passWord.value = storedPassword
|
|
|
|
|
|
|
+ userName.value = storedUsername || ''
|
|
|
|
|
+ passWord.value = storedPassword || ''
|
|
|
remeberPassWord.value = true
|
|
remeberPassWord.value = true
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-// 清空输入框
|
|
|
|
|
-const ClearInput = () => {
|
|
|
|
|
- userName.value = ''
|
|
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 关闭滑动验证码弹窗(SlideCode @close 事件)
|
|
|
|
|
+ * @param verified 验证状态:1=成功,-1=失败,0=未开始
|
|
|
|
|
+ * @param phoneNumber 验证成功后后端返回的手机号
|
|
|
|
|
+ */
|
|
|
|
|
+const CloseSlideCode = (verified: number, phoneNumber: string): void => {
|
|
|
|
|
+ console.log('关闭滑动验证码弹窗', verified)
|
|
|
|
|
+ console.log('验证状态', verified)
|
|
|
|
|
+ sliderVerified.value = verified
|
|
|
|
|
+ console.log('验证手机号', phoneNumber)
|
|
|
|
|
+ sliderPhone.value = phoneNumber || ''
|
|
|
|
|
+
|
|
|
|
|
+ if (verified === 1) {
|
|
|
|
|
+ // 验证成功
|
|
|
|
|
+ // TODO: 旧项目此处根据 tabActive 区分「账号登录 / 手机号验证 / 绑定手机」并调用 StartCountdown() 启动短信倒计时。
|
|
|
|
|
+ // 当前登录页为纯账号密码登录,暂无多标签与短信验证码 UI,
|
|
|
|
|
+ // 待补充对应流程后,在此处接入验证成功后的登录 / 发短信逻辑。
|
|
|
|
|
+ console.log('验证成功,手机号为', phoneNumber)
|
|
|
|
|
+ tabActive.value = 6 // 切换到手机号验证标签
|
|
|
|
|
+ StartCountdown()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ showSlideCode.value = false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 倒计时定时器句柄 */
|
|
|
|
|
+let countdownTimer: ReturnType<typeof setInterval> | null = null
|
|
|
|
|
+/** 倒计时剩余秒数 */
|
|
|
|
|
+let count = 300
|
|
|
|
|
+
|
|
|
|
|
+/** 启动全局倒计时定时器 */
|
|
|
|
|
+const StartCountdown = (): void => {
|
|
|
|
|
+ // 先清除已存在的定时器
|
|
|
|
|
+ if (countdownTimer) {
|
|
|
|
|
+ clearInterval(countdownTimer)
|
|
|
|
|
+ countdownTimer = null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ isCount.value = true
|
|
|
|
|
+ count = 300
|
|
|
|
|
+ codeText.value = '300 秒'
|
|
|
|
|
+ countdownTimer = setInterval(() => {
|
|
|
|
|
+ count--
|
|
|
|
|
+ codeText.value = `${count} 秒`
|
|
|
|
|
+ if (count <= 0) {
|
|
|
|
|
+ StopCountdown()
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 1000)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 停止全局倒计时定时器 */
|
|
|
|
|
+const StopCountdown = (): void => {
|
|
|
|
|
+ if (countdownTimer) {
|
|
|
|
|
+ clearInterval(countdownTimer)
|
|
|
|
|
+ countdownTimer = null
|
|
|
|
|
+ }
|
|
|
|
|
+ isCount.value = false
|
|
|
|
|
+ count = 300
|
|
|
|
|
+ codeText.value = '重新获取'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 组件卸载时清理倒计时定时器,避免泄漏
|
|
|
|
|
+onBeforeUnmount(() => {
|
|
|
|
|
+ StopCountdown()
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+/** 重新获取滑动验证码(SlideCode @refresh 事件) */
|
|
|
|
|
+const ObtainSliderImg = (): void => {
|
|
|
|
|
+ const params = {
|
|
|
|
|
+ sliderUUID: sliderUUID.value, // 图片唯一凭证
|
|
|
|
|
+ }
|
|
|
|
|
+ getSlider(params).then((res: any) => {
|
|
|
|
|
+ console.log('重新获取滑动验证码结果', res)
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ sliderUUID.value = res.data.sliderUUID
|
|
|
|
|
+ sliderBgUrl.value = res.data.backgroundBase64
|
|
|
|
|
+ sliderImgUrl.value = res.data.blockBase64
|
|
|
|
|
+ sliderPositionY.value = res.data.y // 验证码的 y 坐标
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage.error(res.msg)
|
|
|
|
|
+ }
|
|
|
|
|
+ }).catch((err: unknown) => {
|
|
|
|
|
+ // 非 200 由拦截器统一弹错并 reject,此处仅兜底避免未处理的 rejection
|
|
|
|
|
+ console.error('重新获取滑动验证码异常:', err)
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 验证码输入回调(TODO: 如需输满 6 位自动提交可在此处理) */
|
|
|
|
|
+const HandleSliderCodeInput = (): void => {
|
|
|
|
|
+ // TODO: 预留,验证码输入变化回调
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 获取手机验证码(TODO: 接入发送短信验证码接口并启动倒计时) */
|
|
|
|
|
+const GetBindPhoneCode = (): void => {
|
|
|
|
|
+ // TODO: 调用发送验证码接口,成功后启动倒计时并更新 codeText / isCount
|
|
|
|
|
+ console.log('TODO: 获取手机验证码')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 密码加密(TODO: 内容待补充,接入旧项目加密逻辑)
|
|
|
|
|
+ * @param text 明文密码
|
|
|
|
|
+ * @returns 密文(当前原样返回明文,仅占位)
|
|
|
|
|
+ */
|
|
|
|
|
+const encrypt = (text: string): string => {
|
|
|
|
|
+ // TODO: 补充加密实现
|
|
|
|
|
+ return text
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 保存设备指纹到信任设备(TODO: 内容待补充,可调用 bindDeviceCode) */
|
|
|
|
|
+const SaveUserDeviceFinge = (): void => {
|
|
|
|
|
+ // TODO: 补充信任设备指纹保存逻辑
|
|
|
|
|
+ let params={
|
|
|
|
|
+ username: userName.value,// 用户名
|
|
|
|
|
+ deviceFinger: deviceFingerprint.value,// 设备指纹
|
|
|
|
|
+ };
|
|
|
|
|
+ bindDeviceCode(params).then((res: any) => {
|
|
|
|
|
+ console.log('绑定设备指纹结果', res)
|
|
|
|
|
+ }).catch(
|
|
|
|
|
+ (err: unknown) => {
|
|
|
|
|
+ console.error('绑定设备指纹失败:', err)
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
- const menuList = ref([])
|
|
|
|
|
-// 登录提交
|
|
|
|
|
-const SubmitLogin = () => {
|
|
|
|
|
- if(!userName.value || !passWord.value) {
|
|
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 登录成功后的统一处理(各登录方式成功后统一调用)
|
|
|
|
|
+ * 流程:存 token/菜单/权限 → (可选)信任设备确认 → 获取用户信息 → 跳转主页
|
|
|
|
|
+ * @param data 登录接口返回的数据
|
|
|
|
|
+ * @param askTrustDevice 是否提示加入信任设备(仅手机号验证码登录传 true)
|
|
|
|
|
+ */
|
|
|
|
|
+const HandleSuccessfulLogin = async (data: any, askTrustDevice = false): Promise<void> => {
|
|
|
|
|
+ // 1. 保存 token、菜单、权限
|
|
|
|
|
+ localStorage.setItem('token', data.tokenValue)
|
|
|
|
|
+ menuList.value = data.pcMenuVOS
|
|
|
|
|
+ localStorage.setItem('menuList', JSON.stringify(data.pcMenuVOS))
|
|
|
|
|
+ localStorage.setItem('permissions', JSON.stringify(data.permissions))
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 仅手机号验证码登录成功后,弹窗提示是否加入30天免验证登录信任设备
|
|
|
|
|
+ // 确定则保存设备指纹,取消则跳过;两者都不阻塞后续登录流程
|
|
|
|
|
+ if (askTrustDevice) {
|
|
|
|
|
+ await ElMessageBox.confirm('是否加入30天免验证登录信任设备?', '提示', {
|
|
|
|
|
+ confirmButtonText: '确定',
|
|
|
|
|
+ cancelButtonText: '取消',
|
|
|
|
|
+ type: 'warning',
|
|
|
|
|
+ }).then(() => {
|
|
|
|
|
+ // 确定按钮点击事件
|
|
|
|
|
+ SaveUserDeviceFinge() // 保存设备指纹
|
|
|
|
|
+ }).catch(() => {
|
|
|
|
|
+ // 取消按钮点击事件:不加入信任设备
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 获取用户信息
|
|
|
|
|
+ const userInfoRes: any = await getUserInfo()
|
|
|
|
|
+ console.log('获取用户信息返回', userInfoRes)
|
|
|
|
|
+ if (userInfoRes && userInfoRes.code === 200) {
|
|
|
|
|
+ localStorage.setItem('userInfo', JSON.stringify(userInfoRes.data))
|
|
|
|
|
+ ElMessage.success('登录成功')
|
|
|
|
|
+ // 4. 跳转到主页
|
|
|
|
|
+ const firstRoute = menuList.value[0]?.route
|
|
|
|
|
+ if (firstRoute) {
|
|
|
|
|
+ router.push(firstRoute)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage.error('菜单为空,无法跳转主页')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 验证码登录提交 */
|
|
|
|
|
+const SubmitCodeLogin = (): void => {
|
|
|
|
|
+ if (!isCount.value) {
|
|
|
|
|
+ // 倒计时未开启
|
|
|
|
|
+ if (codeText.value === '获取验证码') {
|
|
|
|
|
+ ElMessage.error('请先获取验证码!')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (showSlideCode.value) {
|
|
|
|
|
+ ElMessage.error('请先验证滑块位置')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sliderCode.value === '') {
|
|
|
|
|
+ ElMessage.error('请输入验证码')
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ const param = {
|
|
|
|
|
+ phoneCode: sliderCode.value.trim(), // 手机验证码
|
|
|
|
|
+ sliderUUID: sliderUUID.value, // 验证码唯一id
|
|
|
|
|
+ deviceType: 'PC',
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ loadingBind.value = true
|
|
|
|
|
+ loginInByCode(param).then((res: any) => {
|
|
|
|
|
+ console.log('手机验证码登录返回结果', res)
|
|
|
|
|
+ loadingBind.value = false
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ // 清空验证码并走登录成功统一处理(验证码登录需提示加入信任设备)
|
|
|
|
|
+ sliderCode.value = ''
|
|
|
|
|
+ HandleSuccessfulLogin(res.data, true).catch((err: unknown) => {
|
|
|
|
|
+ console.error('处理登录成功失败:', err)
|
|
|
|
|
+ })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 登录失败
|
|
|
|
|
+ ElMessage.error(res.msg)
|
|
|
|
|
+ }
|
|
|
|
|
+ }).catch((err: unknown) => {
|
|
|
|
|
+ loadingBind.value = false
|
|
|
|
|
+ console.error('验证码登录请求异常:', err)
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 登录提交
|
|
|
|
|
+ * 流程:获取设备指纹 → 携带指纹登录 → 绑定设备 → 获取用户信息 → 跳转
|
|
|
|
|
+ */
|
|
|
|
|
+const SubmitLogin = async (): Promise<void> => {
|
|
|
|
|
+ if (!userName.value || !passWord.value) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 确保设备指纹已获取(若 onMounted 预加载未完成则在此等待)
|
|
|
|
|
+ if (!deviceFingerprint.value) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ deviceFingerprint.value = await getStableDeviceFingerprint()
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('登录时获取设备指纹失败:', error)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 记住密码处理
|
|
|
if (remeberPassWord.value) {
|
|
if (remeberPassWord.value) {
|
|
|
localStorage.setItem('userName', userName.value)
|
|
localStorage.setItem('userName', userName.value)
|
|
|
localStorage.setItem('passWord', passWord.value)
|
|
localStorage.setItem('passWord', passWord.value)
|
|
@@ -245,40 +433,44 @@ const SubmitLogin = () => {
|
|
|
localStorage.removeItem('passWord')
|
|
localStorage.removeItem('passWord')
|
|
|
localStorage.setItem('rememberMe', 'false')
|
|
localStorage.setItem('rememberMe', 'false')
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
const params = {
|
|
const params = {
|
|
|
- username: userName.value.trim(),
|
|
|
|
|
- password: passWord.value.trim()
|
|
|
|
|
|
|
+ username: userName.value.trim(), // 账号
|
|
|
|
|
+ password: passWord.value.trim(), // 密码
|
|
|
|
|
+ deviceFinger: deviceFingerprint.value, // 设备指纹
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
loadingLogin.value = true
|
|
loadingLogin.value = true
|
|
|
- login(params).then((res) => {
|
|
|
|
|
- loadingLogin.value = false
|
|
|
|
|
- if (res.code === 200) {
|
|
|
|
|
- console.log("登录成功返回",res)
|
|
|
|
|
- localStorage.setItem('token', res.data.tokenValue)
|
|
|
|
|
- menuList.value = res.data.pcMenuVOS
|
|
|
|
|
- localStorage.setItem('menuList', JSON.stringify(res.data.pcMenuVOS))
|
|
|
|
|
- localStorage.setItem('permissions', JSON.stringify(res.data.permissions))
|
|
|
|
|
- // 登录成功后调用获取用户信息接口
|
|
|
|
|
- return getUserInfo(res.data.token)
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 1. 登录(携带设备指纹)
|
|
|
|
|
+ const res: any = await loginSignIn(params)
|
|
|
|
|
+ loadingLogin.value = false
|
|
|
|
|
+
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ console.log('登录成功返回', res)
|
|
|
|
|
+
|
|
|
|
|
+ //先判断是否有token 有token 直接登录成功
|
|
|
|
|
+ if (res.data.tokenValue) {
|
|
|
|
|
+ // 登录成功后的统一处理(账号密码登录不提示信任设备)
|
|
|
|
|
+ await HandleSuccessfulLogin(res.data)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 无 token:需先通过滑块验证,弹出滑动验证码
|
|
|
|
|
+ showSlideCode.value = true
|
|
|
|
|
+ sliderUUID.value = res.data.sliderUUID
|
|
|
|
|
+ sliderBgUrl.value = res.data.backgroundBase64
|
|
|
|
|
+ sliderImgUrl.value = res.data.blockBase64
|
|
|
|
|
+ sliderPositionY.value = res.data.y // 验证码的 y 坐标
|
|
|
}
|
|
}
|
|
|
- }).then((userInfoRes)=>{
|
|
|
|
|
- console.log("获取用户信息返回",userInfoRes)
|
|
|
|
|
- if (userInfoRes && userInfoRes.code === 200) {
|
|
|
|
|
- // 存储用户信息到 localStorage 或 Pinia
|
|
|
|
|
- localStorage.setItem('userInfo', JSON.stringify(userInfoRes.data))
|
|
|
|
|
- ElMessage.success('登录成功');
|
|
|
|
|
- // 跳转到主页
|
|
|
|
|
- router.push(menuList.value[0].route)
|
|
|
|
|
}
|
|
}
|
|
|
- }).catch((err) => {
|
|
|
|
|
- loadingLogin.value = false
|
|
|
|
|
-
|
|
|
|
|
- console.error(err)
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ // 非 200 由 request.ts 拦截器统一弹错并 reject,由下方 catch 处理
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ loadingLogin.value = false
|
|
|
|
|
+ console.error(err)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 去往官网
|
|
|
|
|
-const GoWebsite = () => {
|
|
|
|
|
|
|
+/** 去往官网 */
|
|
|
|
|
+const GoWebsite = (): void => {
|
|
|
window.open('https://www.k12100.com/website')
|
|
window.open('https://www.k12100.com/website')
|
|
|
}
|
|
}
|
|
|
</script>
|
|
</script>
|