Files
nexus/Project/fonrey/UI_DESIGN/登录_账号密码_UI.html
2026-04-30 18:40:55 +08:00

681 lines
32 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1280">
<title>Fonrey 登录管理 · 双方式登录Story 2 + Story 5</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
50: '#F0FDFA', 100: '#CCFBF1', 200: '#99F6E4',
500: '#14B8A6', 600: '#0F766E', 700: '#115E59', 800: '#134E4A'
},
neutral: {
50: '#F8FAFC', 100: '#F1F5F9', 200: '#E2E8F0', 300: '#CBD5E1',
400: '#94A3B8', 500: '#64748B', 600: '#475569', 700: '#334155',
800: '#1E293B', 900: '#0F172A'
},
success: { 50: '#F0FDF4', 600: '#16A34A' },
warning: { 50: '#FFFBEB', 600: '#D97706' },
danger: { 50: '#FEF2F2', 600: '#DC2626' }
}
}
}
}
</script>
<style>
[x-cloak] { display: none !important; }
.captcha-track { background: linear-gradient(90deg, #E2E8F0 0%, #F1F5F9 100%); }
.captcha-success { background: linear-gradient(90deg, #F0FDF4 0%, #16A34A 100%); }
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-4px); }
75% { transform: translateX(4px); }
}
.captcha-shake { animation: shake .22s linear 2; }
</style>
</head>
<body class="min-h-screen bg-neutral-50 font-sans text-sm text-neutral-700 antialiased" x-data="dualLoginPage()" x-init="init()">
<div class="fixed inset-0 -z-10">
<div class="absolute inset-y-0 left-0 w-[56%] bg-gradient-to-br from-primary-800 via-primary-700 to-primary-600"></div>
<div class="absolute -top-16 -left-24 w-96 h-96 rounded-full bg-white/10 blur-2xl"></div>
<div class="absolute bottom-0 left-1/3 w-80 h-80 rounded-full bg-primary-200/15 blur-2xl"></div>
</div>
<main class="mx-auto max-w-[1440px] min-h-screen grid grid-cols-12">
<section class="col-span-7 px-12 py-12 text-white flex flex-col justify-between">
<div>
<div class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-white/10 border border-white/20">
<div class="w-7 h-7 rounded-md bg-primary-500/90 flex items-center justify-center text-white font-semibold">F</div>
<span class="text-base font-semibold">Fonrey 房睿</span>
</div>
<h1 class="mt-8 text-4xl font-semibold leading-tight">经纪人登录</h1>
<p class="mt-4 text-primary-100 text-base leading-7 max-w-xl">
支持两种登录方式:手机号密码登录、手机号验证码登录。
微信扫码登录保留为“即将开放”禁用态入口。
</p>
</div>
<div class="grid grid-cols-2 gap-4 max-w-2xl">
<div class="rounded-lg border border-white/20 bg-white/10 p-4">
<div class="text-xs text-primary-100">当前租户</div>
<div class="mt-1 text-xl font-semibold truncate" x-text="tenantName || '未识别租户'"></div>
</div>
<div class="rounded-lg border border-white/20 bg-white/10 p-4">
<div class="text-xs text-primary-100">登录策略</div>
<div class="mt-1 text-xl font-semibold">双 Tab 登录 + 滑块验证</div>
</div>
</div>
</section>
<section class="col-span-5 px-10 py-10 flex items-center justify-center">
<div class="w-full max-w-md rounded-xl bg-white border border-neutral-200 shadow-lg p-6">
<template x-if="tenantMissing">
<div class="rounded-md border border-warning-600/30 bg-warning-50 px-3 py-2 text-xs text-warning-600 mb-4 flex items-center justify-between gap-2">
<span>未检测到有效 Tenant 信息,请先完成识别</span>
<a href="./登录_UI.html" class="text-primary-600 hover:underline">返回识别页</a>
</div>
</template>
<div class="flex items-center justify-between mb-4">
<div class="min-w-0">
<p class="text-xs text-neutral-500">正在登录</p>
<p class="text-sm font-semibold text-neutral-800 truncate" x-text="tenantName || '租户未识别'"></p>
</div>
<button type="button" @click="openSwitchModal=true" class="text-sm text-primary-600 hover:text-primary-700 hover:underline underline-offset-2">切换公司</button>
</div>
<template x-if="sessionExpiredNotice">
<div class="mb-3 rounded-md border border-warning-600/30 bg-warning-50 px-3 py-2 text-xs text-warning-600 flex items-start justify-between gap-2">
<span>登录已过期,请重新登录</span>
<button @click="sessionExpiredNotice=false" class="text-neutral-500 hover:text-neutral-700" aria-label="关闭会话过期提示"></button>
</div>
</template>
<template x-if="loginSuccessNotice">
<div class="mb-3 rounded-md border border-success-600/30 bg-success-50 px-3 py-2 text-xs text-success-600 flex items-start justify-between gap-2">
<span>密码已重置,请使用新密码登录</span>
<button @click="loginSuccessNotice=false" class="text-neutral-500 hover:text-neutral-700" aria-label="关闭重置成功提示"></button>
</div>
</template>
<h2 class="text-xl font-semibold text-neutral-800">登录</h2>
<p class="mt-1 text-sm text-neutral-500">请选择登录方式并完成验证</p>
<div class="mt-4 grid grid-cols-2 rounded-lg border border-neutral-200 bg-neutral-50 p-1">
<button
type="button"
@click="switchTab('password')"
class="h-9 rounded-md text-sm font-medium transition"
:class="activeTab==='password' ? 'bg-white text-primary-700 shadow-xs border border-primary-200' : 'text-neutral-600 hover:text-neutral-800'"
>密码登录</button>
<button
type="button"
@click="switchTab('sms')"
class="h-9 rounded-md text-sm font-medium transition"
:class="activeTab==='sms' ? 'bg-white text-primary-700 shadow-xs border border-primary-200' : 'text-neutral-600 hover:text-neutral-800'"
>验证码登录</button>
</div>
<template x-if="activeTab==='password'">
<form class="mt-5 space-y-4" @submit.prevent="submitPasswordLogin" x-cloak>
<div class="space-y-1">
<label for="phone-password" class="block text-sm font-medium text-neutral-700">手机号<span class="text-danger-600">*</span></label>
<input
id="phone-password"
type="text"
inputmode="numeric"
maxlength="11"
x-model="phonePassword"
@input="sanitizePhone('password')"
placeholder="请输入您的手机号"
class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400 focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20"
:disabled="passwordLoading || accountState!=='active' || tenantMissing"
>
<p x-show="passwordFieldError" x-text="passwordFieldError" class="text-xs text-danger-600"></p>
</div>
<div class="space-y-1">
<label for="password" class="block text-sm font-medium text-neutral-700">密码<span class="text-danger-600">*</span></label>
<div class="relative">
<input
id="password"
:type="passwordVisible ? 'text' : 'password'"
x-model="password"
placeholder="请输入密码"
class="block w-full px-3 py-2 pr-10 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400 focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20"
:disabled="passwordLoading || accountState!=='active' || tenantMissing"
>
<button type="button" @click="passwordVisible=!passwordVisible" class="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-neutral-500 hover:text-neutral-700" aria-label="显示或隐藏密码">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.644C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.009 9.963 7.178.07.207.07.431 0 .638C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.009-9.964-7.178Z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/></svg>
</button>
</div>
<p x-show="passwordInputError" x-text="passwordInputError" class="text-xs text-danger-600"></p>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<label class="block text-sm font-medium text-neutral-700">滑块验证<span class="text-danger-600">*</span></label>
<button type="button" @click="refreshCaptcha('password')" class="inline-flex items-center gap-1 text-xs text-neutral-500 hover:text-neutral-700" aria-label="刷新滑块验证">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182"/></svg>
刷新
</button>
</div>
<div :class="passwordCaptchaState==='fail' ? 'captcha-shake' : ''" class="rounded-lg border border-neutral-200 p-3">
<div class="relative h-24 rounded-md overflow-hidden bg-gradient-to-r from-neutral-100 to-neutral-200">
<div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_30%,rgba(15,118,110,0.18),transparent_45%),radial-gradient(circle_at_80%_70%,rgba(37,99,235,0.15),transparent_45%)]"></div>
<div class="absolute top-7 h-10 w-9 rounded bg-neutral-300/90 border border-neutral-400/70" :style="`left:${passwordCaptchaTarget}%`"></div>
<div class="absolute top-7 h-10 w-9 rounded border border-primary-600/70 bg-primary-100/90 transition-all" :style="`left: calc(${passwordSliderValue}% - 18px)`"></div>
</div>
<div class="mt-2 rounded-md p-2 border" :class="passwordCaptchaState==='pass' ? 'captcha-success border-success-600/30' : 'captcha-track border-neutral-200'">
<input
type="range"
min="0"
max="100"
step="1"
x-model="passwordSliderValue"
@change="verifyCaptcha('password')"
@input="passwordCaptchaState='idle'"
:disabled="passwordCaptchaState==='pass' || passwordLoading || accountState!=='active' || tenantMissing"
class="w-full accent-primary-600"
>
</div>
<p class="mt-1 text-xs"
:class="passwordCaptchaState==='pass' ? 'text-success-600' : (passwordCaptchaState==='fail' ? 'text-danger-600' : 'text-neutral-500')"
x-text="passwordCaptchaState==='pass' ? '验证通过' : (passwordCaptchaState==='fail' ? '验证失败,请重试' : '拖动滑块完成拼图')"></p>
</div>
</div>
<template x-if="passwordError">
<div class="rounded-md border border-danger-600/30 bg-danger-50 px-3 py-2 text-xs text-danger-600" x-text="passwordError"></div>
</template>
<button
type="submit"
class="inline-flex w-full items-center justify-center gap-1.5 px-4 py-2 text-sm font-medium rounded-md bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="!canPasswordLogin || passwordLoading || accountState!=='active' || tenantMissing"
>
<svg x-show="passwordLoading" class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M16.75 7.25a6.5 6.5 0 1 0 0 9.5"/></svg>
<span x-text="passwordLoading ? '登录中…' : '登录'"></span>
</button>
</form>
</template>
<template x-if="activeTab==='sms'">
<form class="mt-5 space-y-4" @submit.prevent="submitSmsLogin" x-cloak>
<div class="space-y-1">
<label for="phone-sms" class="block text-sm font-medium text-neutral-700">手机号<span class="text-danger-600">*</span></label>
<input
id="phone-sms"
type="text"
inputmode="numeric"
maxlength="11"
x-model="phoneSms"
@input="sanitizePhone('sms')"
placeholder="请输入您的手机号"
class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400 focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20"
:disabled="smsLoading || accountState!=='active' || tenantMissing"
>
<p x-show="smsPhoneError" x-text="smsPhoneError" class="text-xs text-danger-600"></p>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<label class="block text-sm font-medium text-neutral-700">滑块验证<span class="text-danger-600">*</span></label>
<button type="button" @click="refreshCaptcha('sms')" class="inline-flex items-center gap-1 text-xs text-neutral-500 hover:text-neutral-700" aria-label="刷新滑块验证">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182"/></svg>
刷新
</button>
</div>
<div :class="smsCaptchaState==='fail' ? 'captcha-shake' : ''" class="rounded-lg border border-neutral-200 p-3">
<div class="relative h-24 rounded-md overflow-hidden bg-gradient-to-r from-neutral-100 to-neutral-200">
<div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_30%,rgba(15,118,110,0.18),transparent_45%),radial-gradient(circle_at_80%_70%,rgba(37,99,235,0.15),transparent_45%)]"></div>
<div class="absolute top-7 h-10 w-9 rounded bg-neutral-300/90 border border-neutral-400/70" :style="`left:${smsCaptchaTarget}%`"></div>
<div class="absolute top-7 h-10 w-9 rounded border border-primary-600/70 bg-primary-100/90 transition-all" :style="`left: calc(${smsSliderValue}% - 18px)`"></div>
</div>
<div class="mt-2 rounded-md p-2 border" :class="smsCaptchaState==='pass' ? 'captcha-success border-success-600/30' : 'captcha-track border-neutral-200'">
<input
type="range"
min="0"
max="100"
step="1"
x-model="smsSliderValue"
@change="verifyCaptcha('sms')"
@input="smsCaptchaState='idle'"
:disabled="smsCaptchaState==='pass' || smsLoading || accountState!=='active' || tenantMissing"
class="w-full accent-primary-600"
>
</div>
<p class="mt-1 text-xs"
:class="smsCaptchaState==='pass' ? 'text-success-600' : (smsCaptchaState==='fail' ? 'text-danger-600' : 'text-neutral-500')"
x-text="smsCaptchaState==='pass' ? '验证通过' : (smsCaptchaState==='fail' ? '验证失败,请重试' : '拖动滑块完成拼图')"></p>
</div>
</div>
<div class="space-y-1">
<label for="sms-code" class="block text-sm font-medium text-neutral-700">短信验证码<span class="text-danger-600">*</span></label>
<div class="grid grid-cols-[1fr_auto] gap-2">
<input
id="sms-code"
type="text"
inputmode="numeric"
maxlength="6"
x-model="smsCode"
@input="sanitizeSmsCode"
placeholder="请输入6位验证码"
class="block w-full px-3 py-2 text-sm rounded-md border border-neutral-300 placeholder:text-neutral-400 focus:outline-none focus:border-primary-600 focus:ring-2 focus:ring-primary-600/20"
:disabled="smsLoading || accountState!=='active' || tenantMissing"
>
<button
type="button"
@click="sendSmsCode"
class="px-3 h-[38px] rounded-md border border-primary-200 text-primary-700 bg-primary-50 hover:bg-primary-100 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="!canSendSms || accountState!=='active' || tenantMissing"
x-text="otpCountdown>0 ? `重新获取(${otpCountdown}s)` : (otpSending ? '发送中…' : '获取验证码')"
></button>
</div>
<p x-show="smsCodeError" x-text="smsCodeError" class="text-xs text-danger-600"></p>
</div>
<template x-if="smsError">
<div class="rounded-md border border-danger-600/30 bg-danger-50 px-3 py-2 text-xs text-danger-600" x-text="smsError"></div>
</template>
<button
type="submit"
class="inline-flex w-full items-center justify-center gap-1.5 px-4 py-2 text-sm font-medium rounded-md bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="!canSmsLogin || smsLoading || accountState!=='active' || tenantMissing"
>
<svg x-show="smsLoading" class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M16.75 7.25a6.5 6.5 0 1 0 0 9.5"/></svg>
<span x-text="smsLoading ? '登录中…' : '登录'"></span>
</button>
</form>
</template>
<div class="mt-3 flex items-center justify-between text-xs">
<a href="./登录_重置密码_UI.html?mode=recover" class="text-primary-600 hover:text-primary-700 hover:underline underline-offset-2">忘记密码</a>
<span class="text-neutral-400">账号锁定后请联系管理员</span>
</div>
<div class="mt-4 border-t border-neutral-200 pt-4 space-y-2">
<button type="button" disabled class="w-full px-3 py-2 rounded-md border border-neutral-300 bg-neutral-100 text-neutral-400 cursor-not-allowed text-sm">微信扫码登录(即将开放)</button>
</div>
<details class="mt-4 rounded-md border border-neutral-200 bg-neutral-50 p-2">
<summary class="cursor-pointer text-xs text-neutral-500">原型状态切换(仅评审演示)</summary>
<div class="mt-2 grid grid-cols-2 gap-2 text-xs">
<button @click="simulateLock" class="px-2 py-1.5 rounded border border-neutral-300 bg-white hover:bg-neutral-100">模拟账号锁定</button>
<button @click="simulateDisabled" class="px-2 py-1.5 rounded border border-neutral-300 bg-white hover:bg-neutral-100">模拟账号停用</button>
<button @click="sessionExpiredNotice=true" class="px-2 py-1.5 rounded border border-neutral-300 bg-white hover:bg-neutral-100">模拟会话过期</button>
<button @click="resetAllState" class="px-2 py-1.5 rounded border border-neutral-300 bg-white hover:bg-neutral-100">重置状态</button>
</div>
</details>
</div>
</section>
</main>
<div x-show="openSwitchModal" x-cloak class="fixed inset-0 z-50" @keydown.escape.window="openSwitchModal=false">
<div class="absolute inset-0 bg-neutral-900/40" @click="openSwitchModal=false"></div>
<div class="absolute inset-0 flex items-center justify-center p-4 pointer-events-none">
<div class="w-full max-w-sm bg-white rounded-xl shadow-lg border border-neutral-200 pointer-events-auto">
<div class="p-5 text-center space-y-3">
<div class="mx-auto w-12 h-12 rounded-full bg-warning-50 flex items-center justify-center">
<svg class="w-6 h-6 text-warning-600" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9.303 3.376c.866 1.5-.217 3.374-1.948 3.374H4.646c-1.73 0-2.813-1.874-1.948-3.374l7.354-12.748c.866-1.5 3.03-1.5 3.896 0l7.355 12.748Z"/><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5h.008v.008H12v-.008Z"/></svg>
</div>
<h3 class="text-base font-semibold text-neutral-800">切换公司</h3>
<p class="text-sm text-neutral-500">切换公司将退出当前账号并清除租户识别缓存,是否继续?</p>
</div>
<div class="flex items-center justify-center gap-2 px-5 py-3 border-t border-neutral-200 bg-neutral-50">
<button @click="openSwitchModal=false" class="px-4 py-1.5 text-sm border border-neutral-300 rounded-md bg-white hover:bg-neutral-50">取消</button>
<button @click="confirmSwitchCompany" class="px-4 py-1.5 text-sm rounded-md bg-danger-600 text-white hover:bg-danger-600/90">继续切换</button>
</div>
</div>
</div>
</div>
<script>
function dualLoginPage() {
return {
tenantCode: '',
tenantName: '',
tenantMissing: false,
sessionExpiredNotice: false,
loginSuccessNotice: false,
openSwitchModal: false,
activeTab: 'password',
accountState: 'active', // active | locked | disabled
phonePassword: '',
password: '',
passwordVisible: false,
passwordFieldError: '',
passwordInputError: '',
passwordError: '',
passwordLoading: false,
passwordFailCount: 0,
phoneSms: '',
smsCode: '',
smsPhoneError: '',
smsCodeError: '',
smsError: '',
smsLoading: false,
otpSending: false,
otpCountdown: 0,
otpTimer: null,
otpSent: false,
otpFailCount: 0,
mockOtpCode: '123456',
passwordCaptchaTarget: 46,
passwordSliderValue: 0,
passwordCaptchaState: 'idle',
smsCaptchaTarget: 52,
smsSliderValue: 0,
smsCaptchaState: 'idle',
init() {
const params = new URLSearchParams(window.location.search)
this.tenantCode = params.get('tenantCode') || localStorage.getItem('tenant_code') || ''
this.tenantName = params.get('tenantName') || localStorage.getItem('tenant_name') || ''
this.tenantMissing = !this.tenantCode || !this.tenantName
this.sessionExpiredNotice = params.get('reason') === 'session_expired'
this.loginSuccessNotice = params.get('reason') === 'password_reset_success'
this.refreshCaptcha('password')
this.refreshCaptcha('sms')
},
switchTab(tab) {
if (this.activeTab === tab) return
this.activeTab = tab
this.clearTabStates()
},
clearTabStates() {
this.passwordFieldError = ''
this.passwordInputError = ''
this.passwordError = ''
this.smsPhoneError = ''
this.smsCodeError = ''
this.smsError = ''
this.phonePassword = ''
this.password = ''
this.phoneSms = ''
this.smsCode = ''
this.otpSent = false
this.otpFailCount = 0
this.stopOtpCountdown()
this.refreshCaptcha('password')
this.refreshCaptcha('sms')
},
sanitizePhone(scene) {
if (scene === 'password') {
this.phonePassword = this.phonePassword.replace(/\D/g, '').slice(0, 11)
this.passwordFieldError = ''
} else {
this.phoneSms = this.phoneSms.replace(/\D/g, '').slice(0, 11)
this.smsPhoneError = ''
}
},
sanitizeSmsCode() {
this.smsCode = this.smsCode.replace(/\D/g, '').slice(0, 6)
this.smsCodeError = ''
},
refreshCaptcha(scene) {
if (scene === 'password') {
this.passwordCaptchaTarget = Math.floor(Math.random() * 60) + 20
this.passwordSliderValue = 0
this.passwordCaptchaState = 'idle'
} else {
this.smsCaptchaTarget = Math.floor(Math.random() * 60) + 20
this.smsSliderValue = 0
this.smsCaptchaState = 'idle'
}
},
verifyCaptcha(scene) {
if (scene === 'password') {
const diff = Math.abs(this.passwordSliderValue - this.passwordCaptchaTarget)
if (diff <= 3) {
this.passwordCaptchaState = 'pass'
this.passwordError = ''
} else {
this.passwordCaptchaState = 'fail'
setTimeout(() => this.refreshCaptcha('password'), 700)
}
return
}
const diff = Math.abs(this.smsSliderValue - this.smsCaptchaTarget)
if (diff <= 3) {
this.smsCaptchaState = 'pass'
this.smsError = ''
} else {
this.smsCaptchaState = 'fail'
setTimeout(() => this.refreshCaptcha('sms'), 700)
}
},
get canPasswordLogin() {
return this.phonePassword.length === 11 && !!this.password && this.passwordCaptchaState === 'pass'
},
get canSendSms() {
return this.phoneSms.length === 11 && this.smsCaptchaState === 'pass' && this.otpCountdown === 0 && !this.otpSending
},
get canSmsLogin() {
return this.phoneSms.length === 11 && this.smsCode.length === 6
},
submitPasswordLogin() {
this.passwordError = ''
this.passwordFieldError = ''
this.passwordInputError = ''
if (this.accountState === 'locked') {
this.passwordError = '账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁'
return
}
if (this.accountState === 'disabled') {
this.passwordError = '账号已停用,请联系您的管理员'
return
}
if (this.phonePassword.length === 0) {
this.passwordFieldError = '请输入手机号'
return
}
if (this.phonePassword.length < 11) {
this.passwordFieldError = '请输入完整的 11 位手机号'
return
}
if (!this.password) {
this.passwordInputError = '请输入密码'
return
}
if (this.passwordCaptchaState !== 'pass') {
this.passwordError = '请完成滑块验证'
return
}
this.passwordLoading = true
setTimeout(() => {
this.passwordLoading = false
const normalPass = this.phonePassword === '13800138000' && this.password === 'Fonrey@2025'
const initialPasswordPass = this.phonePassword === '13800138001' && this.password === 'Fonrey@2025'
if (normalPass) {
window.location.href = './房源列表_UI.html?from=login&login=success&name=' + encodeURIComponent('王顺')
return
}
if (initialPasswordPass) {
window.location.href = './登录_重置密码_UI.html?mode=initial&phone=' + this.phonePassword
return
}
this.passwordFailCount += 1
this.passwordError = '手机号或密码错误,请重新输入'
this.password = ''
this.refreshCaptcha('password')
if (this.passwordFailCount >= 5) {
this.accountState = 'locked'
this.passwordError = '账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁'
}
}, 900)
},
sendSmsCode() {
this.smsError = ''
this.smsPhoneError = ''
if (this.smsCaptchaState !== 'pass') {
this.smsError = '请先完成滑块验证'
return
}
if (this.phoneSms.length === 0 || this.phoneSms.length < 11) {
this.smsPhoneError = '请输入完整的 11 位手机号'
return
}
this.otpSending = true
setTimeout(() => {
this.otpSending = false
this.otpSent = true
this.smsError = '验证码已发送请注意查收演示码123456'
this.startOtpCountdown()
}, 600)
},
submitSmsLogin() {
this.smsError = ''
this.smsPhoneError = ''
this.smsCodeError = ''
if (this.accountState === 'locked') {
this.smsError = '账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁'
return
}
if (this.accountState === 'disabled') {
this.smsError = '账号已停用,请联系您的管理员'
return
}
if (this.phoneSms.length < 11) {
this.smsPhoneError = '请输入完整的 11 位手机号'
return
}
if (this.smsCode.length < 6) {
this.smsCodeError = '请输入 6 位验证码'
return
}
if (!this.otpSent) {
this.smsError = '请先获取验证码'
return
}
this.smsLoading = true
setTimeout(() => {
this.smsLoading = false
if (this.smsCode !== this.mockOtpCode) {
this.otpFailCount += 1
if (this.otpFailCount >= 5) {
this.smsError = '验证码已失效,请重新获取'
this.smsCode = ''
this.otpSent = false
this.otpFailCount = 0
this.stopOtpCountdown()
} else {
this.smsError = '验证码有误,请重新输入'
}
return
}
const initialUser = this.phoneSms === '13800138001'
if (initialUser) {
window.location.href = './登录_重置密码_UI.html?mode=initial&phone=' + this.phoneSms
return
}
window.location.href = './房源列表_UI.html?from=login&login=success&name=' + encodeURIComponent('王顺')
}, 800)
},
startOtpCountdown() {
this.stopOtpCountdown()
this.otpCountdown = 60
this.otpTimer = setInterval(() => {
this.otpCountdown -= 1
if (this.otpCountdown <= 0) {
this.stopOtpCountdown()
}
}, 1000)
},
stopOtpCountdown() {
if (this.otpTimer) {
clearInterval(this.otpTimer)
this.otpTimer = null
}
this.otpCountdown = 0
},
simulateLock() {
this.accountState = 'locked'
this.passwordError = '账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁'
this.smsError = '账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁'
},
simulateDisabled() {
this.accountState = 'disabled'
this.passwordError = '账号已停用,请联系您的管理员'
this.smsError = '账号已停用,请联系您的管理员'
},
resetAllState() {
this.accountState = 'active'
this.passwordFailCount = 0
this.passwordLoading = false
this.smsLoading = false
this.passwordVisible = false
this.sessionExpiredNotice = false
this.clearTabStates()
},
confirmSwitchCompany() {
this.openSwitchModal = false
localStorage.removeItem('tenant_code')
localStorage.removeItem('tenant_name')
window.location.href = './登录_UI.html?from=switch-company'
}
}
}
</script>
</body>
</html>