8ecbd44948
- 添加反欺诈服务,定期检查设备上的黑名单应用 - 检测到黑名单应用时自动强制用户打卡下班 - 在登录后启动反欺诈检查,每5分钟执行一次 - 更新后端API支持强制打卡事件 - 调整Android最低SDK版本至24 - 添加全局事件监听处理强制打卡事件
148 lines
5.2 KiB
Vue
148 lines
5.2 KiB
Vue
<template>
|
||
<div class="flex justify-center items-center mobile-viewport bg-gray-100 safe-top safe-bottom">
|
||
<div class="w-full max-w-sm p-8 space-y-3 bg-white rounded-2xl shadow-lg">
|
||
<!-- App Logo -->
|
||
<div class="flex justify-center">
|
||
<ArrowRightOnRectangleIcon class="w-16 h-16 text-blue-600" />
|
||
</div>
|
||
|
||
<!-- Title -->
|
||
<h2 class="text-3xl font-extrabold text-center text-gray-900">
|
||
{{ t('login') }}
|
||
</h2>
|
||
|
||
<form @submit.prevent="handleLogin" class="space-y-6">
|
||
<!-- Username -->
|
||
<div>
|
||
<label for="username" class="sr-only">{{ t('username') }}</label>
|
||
<input type="text" id="username" v-model="username" autocomplete="username"
|
||
class="w-full px-4 py-3 text-gray-700 bg-gray-50 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
:placeholder="t('username')"
|
||
required />
|
||
</div>
|
||
|
||
<!-- Password -->
|
||
<div>
|
||
<label for="password" class="sr-only">{{ t('password') }}</label>
|
||
<input type="password" id="password" v-model="password" autocomplete="current-password"
|
||
class="w-full px-4 py-3 text-gray-700 bg-gray-50 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
:placeholder="t('password')"
|
||
required />
|
||
</div>
|
||
|
||
<!-- Remember Me Checkbox -->
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex items-center">
|
||
<input type="checkbox" id="rememberMe" v-model="rememberMe"
|
||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500" />
|
||
<label for="rememberMe" class="ml-2 block text-sm text-gray-900">
|
||
{{ t('rememberMe') }}
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Submit Button -->
|
||
<button type="submit"
|
||
class="w-full py-3 text-lg font-semibold text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-transform transform hover:scale-105 disabled:opacity-60 disabled:cursor-not-allowed shadow-md"
|
||
:disabled="loading">
|
||
{{ loading ? t('loggingIn') : t('login') }}
|
||
</button>
|
||
|
||
<!-- Error -->
|
||
<p v-if="error" class="text-sm text-center text-red-600 mt-4">
|
||
{{ t(error) !== error ? t(error) : error }}
|
||
</p>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { ArrowRightOnRectangleIcon } from '@heroicons/vue/24/outline'
|
||
import { authService } from '@/services/authService.js'
|
||
import { nativeServicesManager } from '@/services/nativeServicesManager.js'
|
||
import { antiSpoofingService } from '@/services/antiSpoofingService.js'
|
||
|
||
const { t } = useI18n()
|
||
|
||
const router = useRouter()
|
||
const username = ref('')
|
||
const password = ref('')
|
||
const rememberMe = ref(false)
|
||
const error = ref('')
|
||
const loading = ref(false)
|
||
|
||
const handleLogin = async () => {
|
||
loading.value = true
|
||
error.value = ''
|
||
|
||
try {
|
||
const loginResult = await authService.login(
|
||
username.value,
|
||
password.value,
|
||
rememberMe.value
|
||
)
|
||
|
||
if (loginResult.success) {
|
||
console.log('✅ LOGIN SUCCESS:', loginResult)
|
||
|
||
sessionStorage.setItem('userId', loginResult.userId.toString())
|
||
sessionStorage.setItem('userRole', loginResult.userRole)
|
||
sessionStorage.setItem('token', loginResult.token)
|
||
|
||
console.log('✅ SESSION STORAGE SET:', {
|
||
userId: sessionStorage.getItem('userId'),
|
||
userRole: sessionStorage.getItem('userRole')
|
||
})
|
||
|
||
if (loginResult.userRole === 'worker') {
|
||
console.log('🔄 WORKER LOGIN - Starting native services...')
|
||
|
||
try {
|
||
await nativeServicesManager.onUserLogin()
|
||
console.log('✅ NATIVE SERVICES STARTED')
|
||
// Start anti-spoofing checks after native services are ready
|
||
antiSpoofingService.startSecurityChecks();
|
||
} catch (serviceError) {
|
||
console.error('❌ NATIVE SERVICES FAILED:', serviceError)
|
||
}
|
||
|
||
console.log('🔄 NAVIGATING TO WORKER DASHBOARD...')
|
||
try {
|
||
await router.push('/worker/dashboard')
|
||
console.log('✅ NAVIGATION COMPLETED')
|
||
} catch (navError) {
|
||
console.error('❌ NAVIGATION FAILED:', navError)
|
||
}
|
||
} else {
|
||
console.log('ℹ️ NON-WORKER LOGIN - Worker client app only')
|
||
error.value = 'This application is designed for workers only.'
|
||
await authService.logout()
|
||
}
|
||
} else {
|
||
if (loginResult.error.includes('Device not authorized')) {
|
||
error.value = 'deviceNotAuthorized'
|
||
} else if (loginResult.error.includes('Invalid credentials')) {
|
||
error.value = 'invalidCredentials'
|
||
} else if (loginResult.error.includes('Network error')) {
|
||
error.value = 'failedConnection'
|
||
} else {
|
||
error.value = loginResult.error
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('Login error:', err)
|
||
error.value = 'failedConnection'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* All styles are now handled by Tailwind CSS classes in the template. */
|
||
</style>
|