681 lines
32 KiB
HTML
681 lines
32 KiB
HTML
<!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>
|