refactor: 优化登录页面和用户存储逻辑

- 将API请求路径中的`/api`前缀移除,简化URL
- 使用`encodeURIComponent`和`decodeURIComponent`替代`btoa`和`atob`,提高数据存储安全性
- 重构登录页面,使用Vant组件替换Element UI,优化用户体验
This commit is contained in:
dzq 2025-04-11 16:39:08 +08:00
parent 7a9847bb25
commit 4891d9376c
3 changed files with 101 additions and 134 deletions

View File

@ -12,7 +12,7 @@ import {
/** 获取临时令牌 */ /** 获取临时令牌 */
export function getTokenApi(appName: string) { export function getTokenApi(appName: string) {
return request<ApiResponseData<TokenResponse>>({ return request<ApiResponseData<TokenResponse>>({
url: '/api/wx/login/getToken', url: '/wx/login/getToken',
method: 'get', method: 'get',
params: { appName } params: { appName }
}) })
@ -21,7 +21,7 @@ export function getTokenApi(appName: string) {
/** 获取微信登录二维码 */ /** 获取微信登录二维码 */
export function getWechatQrCodeApi(token: string) { export function getWechatQrCodeApi(token: string) {
return request<ApiResponseData<string>>({ return request<ApiResponseData<string>>({
url: '/api/wx/login/wechat/qrcode', url: '/wx/login/wechat/qrcode',
method: 'get', method: 'get',
params: { token } params: { token }
}) })
@ -30,7 +30,7 @@ export function getWechatQrCodeApi(token: string) {
/** 发送短信验证码 */ /** 发送短信验证码 */
export function sendSmsApi(token: string, tel: string) { export function sendSmsApi(token: string, tel: string) {
return request<ApiResponseData<SmsSendResponse>>({ return request<ApiResponseData<SmsSendResponse>>({
url: '/api/wx/login/sendSms', url: '/wx/login/sendSms',
method: 'post', method: 'post',
params: { token, tel } params: { token, tel }
}) })
@ -39,7 +39,7 @@ export function sendSmsApi(token: string, tel: string) {
/** 验证短信验证码 */ /** 验证短信验证码 */
export function verifySmsApi(params: VerifySmsParams) { export function verifySmsApi(params: VerifySmsParams) {
return request<ApiResponseData<LoginData>>({ return request<ApiResponseData<LoginData>>({
url: '/api/wx/login/verifySms', url: '/wx/login/verifySms',
method: 'post', method: 'post',
params params
}) })
@ -48,7 +48,7 @@ export function verifySmsApi(params: VerifySmsParams) {
/** 用户退出登录 */ /** 用户退出登录 */
export function logoutApi(token: string) { export function logoutApi(token: string) {
return request<ApiResponseData<LogoutResponse>>({ return request<ApiResponseData<LogoutResponse>>({
url: '/api/wx/login/logout', url: '/wx/login/logout',
method: 'post', method: 'post',
params: { token } params: { token }
}) })

View File

@ -1,168 +1,135 @@
<script setup> <template>
import { ref } from 'vue' <div class="login-container">
<van-form @submit="handleSubmit">
<van-field
v-model="form.tel"
name="手机号"
label="手机号"
placeholder="请输入手机号"
:rules="[{ required: true, message: '请填写手机号' }, { pattern: /^1[3-9]\d{9}$/, message: '手机号格式错误' }]"
/>
<van-field
v-model="form.vcode"
center
clearable
name="验证码"
label="验证码"
placeholder="请输入验证码"
:rules="[{ required: true, message: '请填写验证码' }]"
>
<template #button>
<van-button
size="small"
:disabled="countdown > 0"
@click="sendSms"
native-type="button"
>
{{ countdown > 0 ? `${countdown}秒后重试` : '获取验证码' }}
</van-button>
</template>
</van-field>
<div style="margin: 16px;">
<van-button round block type="primary" native-type="submit">
立即登录
</van-button>
</div>
</van-form>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { verifySmsApi, sendSmsApi, getTokenApi } from '@/common/apis/ab98'
import { useAb98UserStore } from '@/pinia/stores/ab98-user'
import { showSuccessToast, showFailToast } from 'vant' import { showSuccessToast, showFailToast } from 'vant'
import { getTokenApi, sendSmsApi, verifySmsApi } from '@/common/apis/ab98'
import { useAb98UserStore } from '@/pinia/stores/ab98-user'
const userStore = useAb98UserStore() const userStore = useAb98UserStore()
const router = useRouter() const router = useRouter()
//
const form = ref({ const form = ref({
tel: '', tel: '',
vcode: '' vcode: ''
}) })
//
const rules = {
tel: [
{ required: true, message: '请输入手机号码', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
],
vcode: [
{ required: true, message: '请输入验证码', trigger: 'blur' }
]
}
//
const countdown = ref(0) const countdown = ref(0)
const canSend = ref(true) const loading = ref(true)
let timer: number | null = null
//
const handleSendSms = async () => {
if (!form.value.tel) {
showFailToast('请先输入手机号码')
return
}
onMounted(async () => {
try { try {
const { data: tokenData } = await getTokenApi('shop-web') const { data } = await getTokenApi('ab98_app')
if (!tokenData?.success) { if (data.token) {
showFailToast('获取token失败') userStore.setToken(data.token)
} else {
showFailToast('令牌获取失败')
}
} catch (err) {
showFailToast('网络异常,请重试')
} finally {
loading.value = false
}
})
const sendSms = async () => {
try {
if (!/^1[3-9]\d{9}$/.test(form.value.tel)) {
showFailToast('手机号格式错误')
return return
} }
userStore.setToken(tokenData.data.token);
const { data } = await sendSmsApi(tokenData.data.token, form.value.tel) const { data } = await sendSmsApi(userStore.token, form.value.tel)
if (data.success) { if (data.success) {
showSuccessToast('验证码已发送')
startCountdown() startCountdown()
showSuccessToast('验证码已发送')
} else { } else {
showFailToast(data.message || '发送失败') showFailToast(data.message || '发送失败')
} }
} catch (error) { } catch (err) {
showFailToast('发送验证码失败') showFailToast('请求异常,请稍后重试')
} }
} }
//
const startCountdown = () => { const startCountdown = () => {
canSend.value = false
countdown.value = 60 countdown.value = 60
const timer = setInterval(() => { timer = window.setInterval(() => {
if (countdown.value <= 0) { if (countdown.value <= 0 && timer) {
clearInterval(timer) window.clearInterval(timer)
canSend.value = true
return return
} }
countdown.value-- countdown.value--
}, 1000) }, 1000)
} }
//
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
const params = { const { data } = await verifySmsApi({
token: userStore.token, token: userStore.token,
tel: form.value.tel, tel: form.value.tel,
vcode: form.value.vcode vcode: form.value.vcode
} })
const { data } = await verifySmsApi(params)
if (data.success) { if (data.success) {
userStore.setTel(form.value.tel)
userStore.setUserInfo(data) userStore.setUserInfo(data)
ElMessage.success('登录成功') userStore.setIsLogin(true)
showSuccessToast('登录成功')
router.push('/') router.push('/')
} else { } else {
ElMessage.error('验证码错误或已过期') showFailToast('验证码错误')
} }
} catch (error) { } catch (err) {
ElMessage.error('登录失败') console.error(err)
showFailToast('登录失败,请稍后重试')
} }
} }
</script> </script>
<template>
<div class="login-container">
<el-card class="login-box">
<h2 class="title">手机验证码登录</h2>
<el-form
:model="form"
:rules="rules"
label-width="80px"
label-position="top"
>
<el-form-item label="手机号码" prop="tel">
<el-input v-model="form.tel" placeholder="请输入手机号码" />
</el-form-item>
<el-form-item label="验证码" prop="vcode">
<div class="vcode-input">
<el-input
v-model="form.vcode"
placeholder="请输入验证码"
style="width: 60%"
/>
<el-button
:disabled="!canSend"
@click="handleSendSms"
style="margin-left: 10px; width: 35%"
>
{{ countdown > 0 ? `${countdown}秒后重试` : '获取验证码' }}
</el-button>
</div>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="handleSubmit"
style="width: 100%"
>
立即登录
</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<style scoped> <style scoped>
.login-container { .login-container {
display: flex; padding: 20px;
justify-content: center; margin-top: 30%;
align-items: center;
min-height: 100vh;
background-color: #f5f7fa;
}
.login-box {
width: 400px;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.title {
text-align: center;
margin-bottom: 30px;
color: #303133;
}
.vcode-input {
display: flex;
align-items: center;
} }
</style> </style>

View File

@ -19,24 +19,24 @@ const STORAGE_KEYS = {
export const useAb98UserStore = defineStore("ab98User", () => { export const useAb98UserStore = defineStore("ab98User", () => {
// 用户面部图像URL // 用户面部图像URL
const storedFace = localStorage.getItem(STORAGE_KEYS.FACE) const storedFace = localStorage.getItem(STORAGE_KEYS.FACE)
const face_img = ref<string>(storedFace ? atob(storedFace) : '') const face_img = ref<string>(storedFace ? decodeURIComponent(storedFace) : '')
// 用户性别(男/女) // 用户性别(男/女)
const storedSex = localStorage.getItem(STORAGE_KEYS.SEX) const storedSex = localStorage.getItem(STORAGE_KEYS.SEX)
const sex = ref<string>(storedSex ? atob(storedSex) : '') const sex = ref<string>(storedSex ? decodeURIComponent(storedSex) : '')
// 用户真实姓名 // 用户真实姓名
const storedName = localStorage.getItem(STORAGE_KEYS.NAME) const storedName = localStorage.getItem(STORAGE_KEYS.NAME)
const name = ref<string>(storedName ? atob(storedName) : '') const name = ref<string>(storedName ? decodeURIComponent(storedName) : '')
// AB98系统用户唯一标识 // AB98系统用户唯一标识
const storedUserId = localStorage.getItem(STORAGE_KEYS.USERID) const storedUserId = localStorage.getItem(STORAGE_KEYS.USERID)
const userid = ref<string>(storedUserId ? atob(storedUserId) : "") const userid = ref<string>(storedUserId ? decodeURIComponent(storedUserId) : "")
// 是否已完成注册流程 // 是否已完成注册流程
const registered = ref<boolean>(JSON.parse(localStorage.getItem(STORAGE_KEYS.REGISTERED) || "false")) const registered = ref<boolean>(JSON.parse(localStorage.getItem(STORAGE_KEYS.REGISTERED) || "false"))
// 用户绑定手机号 // 用户绑定手机号
const storedTel = localStorage.getItem(STORAGE_KEYS.TEL) const storedTel = localStorage.getItem(STORAGE_KEYS.TEL)
const tel = ref<string>(storedTel ? atob(storedTel) : "") const tel = ref<string>(storedTel ? decodeURIComponent(storedTel) : "")
// 用户认证令牌 // 用户认证令牌
const storedToken = localStorage.getItem(STORAGE_KEYS.TOKEN) const storedToken = localStorage.getItem(STORAGE_KEYS.TOKEN)
const token = ref<string>(storedToken ? atob(storedToken) : "") const token = ref<string>(storedToken ? decodeURIComponent(storedToken) : "")
// 用户登录状态 // 用户登录状态
const isLogin = ref<boolean>(false); const isLogin = ref<boolean>(false);
isLogin.value = tel.value ? true : false; isLogin.value = tel.value ? true : false;
@ -47,17 +47,17 @@ export const useAb98UserStore = defineStore("ab98User", () => {
*/ */
const setUserInfo = (data: LoginData) => { const setUserInfo = (data: LoginData) => {
face_img.value = data.face_img face_img.value = data.face_img
localStorage.setItem(STORAGE_KEYS.FACE, btoa(data.face_img)) localStorage.setItem(STORAGE_KEYS.FACE, encodeURIComponent(data.face_img))
sex.value = data.sex sex.value = data.sex
localStorage.setItem(STORAGE_KEYS.SEX, btoa(data.sex)) localStorage.setItem(STORAGE_KEYS.SEX, encodeURIComponent(data.sex))
name.value = data.name name.value = data.name
localStorage.setItem(STORAGE_KEYS.NAME, btoa(data.name)) localStorage.setItem(STORAGE_KEYS.NAME, encodeURIComponent(data.name))
userid.value = data.userid userid.value = data.userid
localStorage.setItem(STORAGE_KEYS.USERID, btoa(data.userid)) localStorage.setItem(STORAGE_KEYS.USERID, encodeURIComponent(data.userid))
registered.value = data.registered registered.value = data.registered
localStorage.setItem(STORAGE_KEYS.REGISTERED, JSON.stringify(data.registered)) localStorage.setItem(STORAGE_KEYS.REGISTERED, JSON.stringify(data.registered))
tel.value = data.tel tel.value = data.tel
localStorage.setItem(STORAGE_KEYS.TEL, btoa(data.tel)) localStorage.setItem(STORAGE_KEYS.TEL, encodeURIComponent(data.tel))
} }
/** /**
@ -85,7 +85,7 @@ export const useAb98UserStore = defineStore("ab98User", () => {
* @param value - JWT格式的认证令牌 * @param value - JWT格式的认证令牌
*/ */
const setToken = (value: string) => { const setToken = (value: string) => {
localStorage.setItem(STORAGE_KEYS.TOKEN, btoa(value)) localStorage.setItem(STORAGE_KEYS.TOKEN, encodeURIComponent(value))
token.value = value token.value = value
} }