Files
device-voice-h5/pages/login/login.vue
sexygoat 8bdd3a2d2a
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 49s
fix: 登录UI
2026-04-11 18:02:57 +08:00

284 lines
5.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="login-page">
<view class="content">
<view class="card">
<view class="card-title">用户登录</view>
<view class="form-item">
<view class="label">设备标识</view>
<view class="input-wrap" :class="{ focus: inputFocus }">
<input
v-model="identifier"
class="input"
placeholder="请输入SN/IMEI/虚拟号/ICCID/MSISDN"
placeholder-class="placeholder"
@focus="inputFocus = true"
@blur="inputFocus = false"
@input="showError = false"
/>
<view v-if="identifier" class="clear" @tap="identifier = ''">
<text>×</text>
</view>
</view>
<text v-if="showError" class="error">请输入设备标识</text>
</view>
<view class="form-item">
<button
class="btn-login"
:class="{ disabled: !identifier || loading }"
:disabled="!identifier || loading"
@tap="handleLogin"
>
<view v-if="loading" class="loading">
<view class="dot"/>
<view class="dot"/>
<view class="dot"/>
</view>
<text v-else>登录</text>
</button>
</view>
<view class="agreement">
<text class="agree-text">登录即表示同意</text>
<text class="link">用户协议</text>
<text class="agree-text"></text>
<text class="link">隐私政策</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { authApi } from '@/api/index.js';
import { useUserStore } from '@/store/index.js';
import { APP_ID, USE_WECHAT_AUTH } from '@/utils/env.js';
const userStore = useUserStore();
const identifier = ref('');
const loading = ref(false);
const agreed = ref(true);
const inputFocus = ref(false);
const showError = ref(false);
const getCode = () => {
const params = new URLSearchParams(window.location.search);
return params.get('code');
};
const clearCode = () => {
window.history.replaceState({}, '', window.location.pathname);
};
const redirectToWxAuth = (assetToken) => {
const redirectUri = encodeURIComponent(window.location.href.split('?')[0]);
const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${APP_ID}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_userinfo&state=${assetToken}#wechat_redirect`;
window.location.href = url;
};
const handleLogin = () => {
if (!identifier.value) {
showError.value = true;
return;
}
doLogin();
};
const doLogin = async () => {
loading.value = true;
try {
const verifyData = await authApi.verifyAsset(identifier.value);
userStore.setAssetToken(verifyData.asset_token);
userStore.setIdentifier(identifier.value);
if (!USE_WECHAT_AUTH) {
const loginData = await authApi.wechatLogin(verifyData.asset_token, 'mock_code');
userStore.setToken(loginData.token);
uni.setStorageSync('identifier', userStore.state.identifier);
uni.redirectTo({ url: '/pages/index/index' });
return;
}
redirectToWxAuth(verifyData.asset_token);
} catch (e) {
console.error('登录失败', e);
loading.value = false;
}
};
const handleWechatCallback = async () => {
if (!USE_WECHAT_AUTH) return;
const code = getCode();
const assetToken = userStore.state.assetToken;
if (!code || !assetToken) return;
loading.value = true;
clearCode();
try {
const loginData = await authApi.wechatLogin(assetToken, code);
userStore.setToken(loginData.token);
uni.setStorageSync('identifier', userStore.state.identifier);
uni.redirectTo({ url: '/pages/index/index' });
} catch (e) {
console.error('微信登录失败', e);
loading.value = false;
}
};
onMounted(() => {
handleWechatCallback();
});
</script>
<style lang="scss" scoped>
.login-page {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #FFFFFF;
}
.card {
background: #FFFFFF;
border-radius: 24rpx;
padding: 48rpx;
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.08);
}
.card-title {
font-size: 40rpx;
font-weight: 600;
color: #1A1A1A;
margin-bottom: 48rpx;
}
.form-item {
margin-bottom: 32rpx;
}
.label {
font-size: 26rpx;
color: #6B7280;
margin-bottom: 16rpx;
}
.input-wrap {
display: flex;
align-items: center;
background: #F9FAFB;
border: 2rpx solid #E5E7EB;
border-radius: 16rpx;
padding: 0 24rpx;
height: 96rpx;
transition: all 0.2s;
}
.input-wrap.focus {
border-color: #2563EB;
background: #FFFFFF;
}
.input {
flex: 1;
font-size: 30rpx;
color: #1A1A1A;
background: transparent;
}
.placeholder {
color: #9CA3AF;
}
.clear {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
color: #9CA3AF;
font-size: 36rpx;
}
.error {
font-size: 24rpx;
color: #EF4444;
margin-top: 12rpx;
}
.btn-login {
width: 100%;
height: 96rpx;
background: #2563EB;
border: none;
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 500;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.btn-login.disabled {
background: #E5E7EB;
color: #9CA3AF;
}
.btn-login:active:not(.disabled) {
background: #1D4ED8;
}
.loading {
display: flex;
align-items: center;
gap: 8rpx;
}
.dot {
width: 8rpx;
height: 8rpx;
background: #9CA3AF;
border-radius: 50%;
animation: load 1.4s infinite ease-in-out both;
}
.dot:nth-child(1) { animation-delay: -0.32s; }
.dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes load {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
.agreement {
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: center;
gap: 8rpx;
margin-top: 40rpx;
}
.agree-text {
font-size: 24rpx;
color: #9CA3AF;
white-space: nowrap;
}
.link {
font-size: 24rpx;
color: #2563EB;
white-space: nowrap;
}
</style>