2026-09-10 10:57:40 +08:00
|
|
|
<template>
|
2026-09-15 16:16:55 +08:00
|
|
|
<view class="login-page page-white" :style="pageStyle()">
|
2026-09-18 12:59:43 +08:00
|
|
|
<view :style="statusStyle(44)"></view>
|
|
|
|
|
<scroll-view scroll-y :style="contentStyle(44, false)">
|
|
|
|
|
<view class="login-content">
|
|
|
|
|
<BrandLogo />
|
|
|
|
|
<text class="welcome">欢迎登录 AI融资</text>
|
|
|
|
|
<text class="login-subtitle">智能评估,让融资更简单</text>
|
2026-10-22 11:24:15 +08:00
|
|
|
<text class="form-heading">{{ wechatBindingTicket.length > 0 ? '微信绑定手机号' : (mode == 'phone' ? '手机号登录 / 注册' : '密码登录') }}</text>
|
|
|
|
|
<text v-if="wechatBindingTicket.length > 0" class="binding-note">首次微信登录需验证手机号。已有账户将直接关联,保留原有资料和积分。</text>
|
2026-10-14 14:53:13 +08:00
|
|
|
<view class="input-box"><text class="input-prefix">+86</text><input v-model="phone" :disabled="loading || authenticated" type="number" maxlength="11" placeholder="请输入手机号" class="field-input" /></view>
|
|
|
|
|
<view v-if="mode == 'password'" class="input-box"><input v-model="password" :disabled="loading || authenticated" password placeholder="请输入登录密码" class="field-input" /></view>
|
|
|
|
|
<view v-if="mode == 'phone' && smsSent" class="input-box"><input v-model="smsCode" :disabled="loading || authenticated" type="number" maxlength="6" placeholder="请输入 6 位验证码" class="field-input" /><text class="countdown" @click="resendSms">{{ countdown > 0 ? countdown.toString() + 's 后重试' : '重新获取' }}</text></view>
|
2026-09-18 12:59:43 +08:00
|
|
|
<text v-if="errorMessage.length > 0" class="error-message">{{ errorMessage }}</text>
|
2026-10-22 11:24:15 +08:00
|
|
|
<button class="login-button" :disabled="loading" @click="login">{{ loading ? '处理中…' : (authenticated ? '确认同意记录并继续' : (mode == 'phone' ? (smsSent ? (wechatBindingTicket.length > 0 ? '验证并登录' : '登录') : '获取验证码') : '登录')) }}</button>
|
|
|
|
|
<view class="mode-row"><text v-if="wechatBindingTicket.length == 0" class="mode-link" @click="switchMode">{{ mode == 'phone' ? '密码登录' : '使用验证码登录' }}</text><text v-else class="mode-link" @click="cancelWechatBinding">取消绑定,使用手机号登录</text></view>
|
|
|
|
|
<view v-if="wechatBindingTicket.length == 0 && !authenticated" class="other-login"><view class="line"></view><text class="other-text">其他登录方式</text><view class="line"></view></view>
|
|
|
|
|
<view v-if="wechatBindingTicket.length == 0 && !authenticated" class="social-row"><view v-if="wechatSupported" class="social-button" @click="beginWechatLogin"><image class="social-icon" src="/packages/account/static/wechat.svg" mode="aspectFit" /><text class="social-label">微信登录</text></view><view class="social-button" @click="quickLogin"><image class="social-icon" src="/packages/account/static/quick-login.svg" mode="aspectFit" /><text class="social-label">一键登录</text></view></view>
|
2026-10-14 14:53:13 +08:00
|
|
|
<view class="agreement"><view class="check-hit" @click="toggleAgreement"><view :class="agreed ? 'check checked' : 'check'"><text v-if="agreed" class="check-mark">✓</text></view></view><text class="agreement-copy">我已阅读并同意<text class="agreement-link" @click="showAgreement('terms')">《用户协议》</text>和<text class="agreement-link" @click="showAgreement('privacy')">《隐私政策》</text></text></view>
|
|
|
|
|
<text v-if="agreementState == 'ready'" class="agreement-note">确认版本:用户协议 {{ terms.version }} / 隐私政策 {{ privacy.version }}</text>
|
|
|
|
|
<text v-else class="agreement-note">{{ agreementNote }}</text>
|
|
|
|
|
<text v-if="agreementState == 'error' || agreementState == 'unavailable'" class="agreement-retry" @click="loadAgreements">重新加载协议</text>
|
|
|
|
|
<text v-if="authenticated" class="agreement-note">登录已成功。确认同意记录失败时,点击上方按钮以原请求重试,无需再次验证密码或短信。</text>
|
2026-09-10 10:57:40 +08:00
|
|
|
</view>
|
2026-09-18 12:59:43 +08:00
|
|
|
</scroll-view>
|
2026-09-10 10:57:40 +08:00
|
|
|
</view>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="uts">
|
2026-09-15 16:16:55 +08:00
|
|
|
import { usePageLayout } from '@/services/layout.uts'
|
2026-09-10 10:57:40 +08:00
|
|
|
import BrandLogo from '@/components/BrandLogo.uvue'
|
2026-10-22 11:24:15 +08:00
|
|
|
import { API_BASE_URL, requestSmsCode, requestSmsLogin, requestPasswordLogin, acceptSession } from '@/packages/account/services/auth.uts'
|
|
|
|
|
import { wechatLoginSupported, requestWechatAuthorization, requestWechatLogin, requestWechatBindingCode, bindWechatPhone, acceptWechatSession } from '@/packages/account/services/wechat-login.uts'
|
|
|
|
|
import { getSession, ApiRequestError } from '@/services/api.uts'
|
2026-10-14 14:53:13 +08:00
|
|
|
import { Agreement, emptyAgreement, fetchAgreement, agreementRoute, agreementUnavailable, agreementChanged, grantConsent, consentRequestId, sameConsentSession } from '@/services/agreements.uts'
|
2026-09-20 10:52:10 +08:00
|
|
|
import { consumeInitialLaunch } from '@/services/launch.uts'
|
2026-09-28 17:50:46 +08:00
|
|
|
import { assistantReturnPath } from '@/services/assistant-return.uts'
|
|
|
|
|
import { nextStepRoute } from '@/services/next-step-route.uts'
|
|
|
|
|
import { reportRoute } from '@/services/report-route.uts'
|
2026-10-12 09:28:09 +08:00
|
|
|
import { matchReturnPath } from '@/services/match-route.uts'
|
2026-09-28 17:50:46 +08:00
|
|
|
let afterLoginUrl = '/pages/home/home'
|
|
|
|
|
onLoad((options) => {
|
|
|
|
|
consumeInitialLaunch()
|
2026-10-01 20:26:41 +08:00
|
|
|
if (options?.['resumeLesson'] == '1' && /^[1-9][0-9]{0,9}$/.test(options?.['lessonId'] ?? '')) {
|
|
|
|
|
afterLoginUrl = '/packages/content/lesson/lesson?id=' + options?.['lessonId']
|
|
|
|
|
} else if (options?.['resumeMessages'] == '1') {
|
|
|
|
|
afterLoginUrl = '/packages/account/messages/messages'
|
|
|
|
|
} else if (options?.['resumeHelp'] == '1') {
|
|
|
|
|
afterLoginUrl = '/packages/content/help/help?feedback=1'
|
2026-10-05 20:11:24 +08:00
|
|
|
const feedbackId = options?.['feedbackId'] ?? ''
|
|
|
|
|
if (/^[1-9][0-9]{0,9}$/.test(feedbackId) && !(feedbackId.length == 10 && feedbackId > '2147483647')) afterLoginUrl += '&feedbackId=' + feedbackId
|
2026-10-01 20:26:41 +08:00
|
|
|
} else if (options?.['resumeAssistant'] == '1') {
|
2026-09-30 19:11:44 +08:00
|
|
|
const target = assistantReturnPath(options?.['assessmentId'] ?? '', options?.['productIds'] ?? '', options?.['chatId'] ?? '', options?.['from'] ?? '', options?.['revisionId'] ?? '')
|
2026-09-28 17:50:46 +08:00
|
|
|
if (target.length > 0) afterLoginUrl = target
|
|
|
|
|
} else if (options?.['resumeNextStep'] == '1') {
|
2026-09-30 19:11:44 +08:00
|
|
|
const target = nextStepRoute(options?.['assessmentId'] ?? '', options?.['productId'] ?? '', options?.['section'] ?? 'actions', options?.['revisionId'] ?? '')
|
2026-09-28 17:50:46 +08:00
|
|
|
if (target.length > 0) afterLoginUrl = target
|
|
|
|
|
} else if (options?.['resumeReport'] == '1') {
|
2026-09-30 19:11:44 +08:00
|
|
|
const target = reportRoute(options?.['assessmentId'] ?? '', options?.['tab'] ?? 'overview', options?.['revisionId'] ?? '')
|
2026-09-28 17:50:46 +08:00
|
|
|
if (target.length > 0) afterLoginUrl = target
|
2026-10-07 10:01:05 +08:00
|
|
|
} else if (options?.['resumeMatch'] == '1') {
|
|
|
|
|
const target = matchReturnPath(options?.['matchPage'] ?? '', options?.['assessmentId'] ?? '', options?.['productIds'] ?? '', options?.['revisionId'] ?? '', options?.['productId'] ?? '', options?.['group'] ?? 'eligible', options?.['tab'] ?? 'explanation', options?.['from'] ?? '')
|
|
|
|
|
if (target.length > 0) afterLoginUrl = target
|
2026-10-04 08:29:53 +08:00
|
|
|
} else if (options?.['resumePoints'] == '1') {
|
|
|
|
|
afterLoginUrl = '/packages/account/points/points'
|
2026-10-14 14:53:13 +08:00
|
|
|
} else if (options?.['resumeConsents'] == '1') {
|
|
|
|
|
afterLoginUrl = '/packages/account/consents/consents'
|
|
|
|
|
} else if (options?.['resumeAgreement'] == '1') {
|
|
|
|
|
const target = agreementRoute(options?.['agreementCode'] ?? '')
|
|
|
|
|
if (target.length > 0) afterLoginUrl = target
|
2026-09-28 17:50:46 +08:00
|
|
|
}
|
|
|
|
|
})
|
2026-09-18 12:59:43 +08:00
|
|
|
const { statusStyle, contentStyle, pageStyle } = usePageLayout()
|
2026-09-22 17:25:52 +08:00
|
|
|
const mode = ref('password')
|
2026-09-18 12:59:43 +08:00
|
|
|
const phone = ref('')
|
2026-09-10 10:57:40 +08:00
|
|
|
const password = ref('')
|
2026-09-11 20:23:10 +08:00
|
|
|
const smsCode = ref('')
|
|
|
|
|
const smsSent = ref(false)
|
|
|
|
|
const countdown = ref(0)
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
const errorMessage = ref('')
|
2026-09-18 12:59:43 +08:00
|
|
|
const agreed = ref(false)
|
2026-10-14 14:53:13 +08:00
|
|
|
const authenticated = ref(false)
|
2026-10-22 11:24:15 +08:00
|
|
|
const wechatSupported = wechatLoginSupported()
|
|
|
|
|
const wechatBindingTicket = ref('')
|
|
|
|
|
let wechatBindingExpiresAt = 0; let wechatOriginSession = ''
|
2026-10-14 14:53:13 +08:00
|
|
|
const terms = ref<Agreement>(emptyAgreement()); const privacy = ref<Agreement>(emptyAgreement())
|
|
|
|
|
const agreementState = ref('loading'); const agreementNote = ref('正在加载用户协议与隐私政策…')
|
2026-09-18 12:59:43 +08:00
|
|
|
const isDemo = API_BASE_URL.length == 0
|
2026-10-14 14:53:13 +08:00
|
|
|
let pageVisible = false; let generation = 0; let agreementGeneration = 0; let accountTimer = 0; let authToken = ''; let authUserId = ''
|
|
|
|
|
type LoginConsentCommand = { agreement: Agreement, requestId: string }
|
|
|
|
|
let pendingConsents: LoginConsentCommand[] = []; let confirmedCount = 0; let selectedTerms: Agreement | null = null; let selectedPrivacy: Agreement | null = null
|
|
|
|
|
const showAgreement = (code: string) => { const item = code == 'terms' ? terms.value : privacy.value; const url = agreementRoute(code, item.version); if (pageVisible && url.length > 0) uni.navigateTo({ url }) }
|
2026-10-22 11:24:15 +08:00
|
|
|
const quickLogin = () => {
|
|
|
|
|
if (!pageVisible || loading.value || authenticated.value || wechatBindingTicket.value.length > 0) return
|
|
|
|
|
uni.showModal({ title: '一键登录暂未开放', content: '请先使用手机号登录。该登录方式将在正式服务接入后开放。', showCancel: false, confirmText: '我知道了' })
|
|
|
|
|
}
|
2026-10-14 14:53:13 +08:00
|
|
|
const sameAuthentication = (): boolean => { const current = getSession(); return sameConsentSession(authToken, authUserId, current?.getString('token') ?? '', current?.getString('userId') ?? '') }
|
|
|
|
|
const clearAuthentication = () => { authToken = ''; authUserId = ''; authenticated.value = false; pendingConsents = []; confirmedCount = 0; agreed.value = false; selectedTerms = null; selectedPrivacy = null; generation++ }
|
|
|
|
|
const toggleAgreement = () => {
|
|
|
|
|
if (loading.value || agreementState.value != 'ready' || pendingConsents.length > 0) return
|
|
|
|
|
agreed.value = !agreed.value
|
|
|
|
|
selectedTerms = agreed.value ? terms.value : null; selectedPrivacy = agreed.value ? privacy.value : null
|
|
|
|
|
}
|
|
|
|
|
async function loadAgreements() {
|
|
|
|
|
if (!pageVisible || loading.value || pendingConsents.length > 0) return
|
|
|
|
|
const request = ++agreementGeneration; agreementState.value = 'loading'; agreementNote.value = '正在加载用户协议与隐私政策…'
|
|
|
|
|
let nextTerms: Agreement | null = null; let nextPrivacy: Agreement | null = null; let missing = false; let failure = ''
|
|
|
|
|
try { nextTerms = await fetchAgreement('terms') } catch (err) { if (agreementUnavailable(err)) missing = true; else failure = err instanceof Error ? err.message : '协议加载失败' }
|
|
|
|
|
try { nextPrivacy = await fetchAgreement('privacy') } catch (err) { if (agreementUnavailable(err)) missing = true; else failure = err instanceof Error ? err.message : '协议加载失败' }
|
|
|
|
|
if (!pageVisible || request != agreementGeneration) return
|
|
|
|
|
if (failure.length > 0 || missing || nextTerms == null || nextPrivacy == null) {
|
|
|
|
|
agreed.value = false; selectedTerms = null; selectedPrivacy = null; terms.value = emptyAgreement(); privacy.value = emptyAgreement()
|
|
|
|
|
agreementState.value = failure.length > 0 ? 'error' : 'unavailable'
|
|
|
|
|
agreementNote.value = failure.length > 0 ? failure + ',请重新加载协议。' : '用户协议或隐私政策尚未正式发布,暂不记录同意;现有账户可使用密码登录。'
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (selectedTerms?.version != nextTerms!.version || selectedTerms?.contentHash != nextTerms!.contentHash || selectedPrivacy?.version != nextPrivacy!.version || selectedPrivacy?.contentHash != nextPrivacy!.contentHash) { agreed.value = false; selectedTerms = null; selectedPrivacy = null }
|
|
|
|
|
terms.value = nextTerms!; privacy.value = nextPrivacy!; agreementState.value = 'ready'; agreementNote.value = ''
|
|
|
|
|
}
|
|
|
|
|
const agreementReadyForLogin = (): boolean => {
|
|
|
|
|
if (agreementState.value == 'unavailable' && mode.value == 'password') return true
|
|
|
|
|
if (agreementState.value != 'ready') { errorMessage.value = agreementState.value == 'unavailable' ? '协议未正式发布,请使用现有账户密码登录' : '请先重新加载用户协议与隐私政策'; return false }
|
|
|
|
|
if (!agreed.value || selectedTerms == null || selectedPrivacy == null) { errorMessage.value = '请先阅读并勾选用户协议与隐私政策'; return false }
|
|
|
|
|
return true
|
|
|
|
|
}
|
2026-09-15 16:16:55 +08:00
|
|
|
let countdownTimer = 0
|
2026-09-18 12:59:43 +08:00
|
|
|
watch(phone, () => {
|
|
|
|
|
smsSent.value = false; smsCode.value = ''; countdown.value = 0; errorMessage.value = ''
|
|
|
|
|
clearInterval(countdownTimer)
|
|
|
|
|
})
|
2026-10-14 14:53:13 +08:00
|
|
|
const switchMode = () => { if (loading.value || authenticated.value) return; mode.value = mode.value == 'phone' ? 'password' : 'phone'; errorMessage.value = '' }
|
2026-10-22 11:24:15 +08:00
|
|
|
const sessionIdentity = (): string => { const session = getSession(); return (session?.getString('token') ?? '') + ':' + (session?.getString('userId') ?? '') }
|
|
|
|
|
const currentLoginRequest = (request: number, origin: string): boolean => pageVisible && request == generation && origin == sessionIdentity()
|
2026-09-11 20:23:10 +08:00
|
|
|
const validPhone = (): boolean => {
|
2026-09-18 12:59:43 +08:00
|
|
|
if (!/^1[3-9][0-9]{9}$/.test(phone.value)) { errorMessage.value = '请输入正确的 11 位手机号'; return false }
|
2026-09-11 20:23:10 +08:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
const startCountdown = () => {
|
|
|
|
|
countdown.value = 60
|
2026-09-15 16:16:55 +08:00
|
|
|
clearInterval(countdownTimer)
|
2026-09-11 20:23:10 +08:00
|
|
|
countdownTimer = setInterval(() => {
|
2026-09-18 12:59:43 +08:00
|
|
|
if (countdown.value <= 1) { countdown.value = 0; clearInterval(countdownTimer) } else countdown.value -= 1
|
2026-09-11 20:23:10 +08:00
|
|
|
}, 1000)
|
|
|
|
|
}
|
2026-10-22 11:24:15 +08:00
|
|
|
const resetWechatBinding = () => {
|
|
|
|
|
wechatBindingTicket.value = ''; wechatBindingExpiresAt = 0; wechatOriginSession = ''
|
|
|
|
|
smsSent.value = false; smsCode.value = ''; countdown.value = 0; clearInterval(countdownTimer)
|
|
|
|
|
}
|
|
|
|
|
const cancelWechatBinding = () => {
|
|
|
|
|
if (loading.value || authenticated.value) return
|
|
|
|
|
generation++; resetWechatBinding(); mode.value = 'phone'; errorMessage.value = ''
|
|
|
|
|
}
|
|
|
|
|
const bindingReady = (): boolean => {
|
|
|
|
|
if (wechatBindingTicket.value.length == 0) return true
|
|
|
|
|
if (wechatOriginSession != sessionIdentity()) {
|
|
|
|
|
resetWechatBinding(); errorMessage.value = '登录状态已变化,请重新微信登录'; return false
|
|
|
|
|
}
|
|
|
|
|
if (Date.now() >= wechatBindingExpiresAt) {
|
|
|
|
|
resetWechatBinding(); errorMessage.value = '绑定已过期,请重新微信登录'; return false
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
async function handleWechatError(error: any | null, request: number) {
|
|
|
|
|
if (!pageVisible || request != generation) return
|
|
|
|
|
if (agreementChanged(error)) {
|
|
|
|
|
resetWechatBinding(); agreed.value = false; selectedTerms = null; selectedPrivacy = null
|
|
|
|
|
loading.value = false; await loadAgreements(); errorMessage.value = '协议版本已更新,请重新阅读并勾选后微信登录。'
|
|
|
|
|
} else {
|
|
|
|
|
if (error instanceof ApiRequestError && error.errorCode == 'WECHAT_BINDING_EXPIRED') resetWechatBinding()
|
|
|
|
|
errorMessage.value = error instanceof Error ? error.message : '微信登录失败,请重新操作'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const acceptWechatResult = (result: UTSJSONObject, request: number, origin: string) => {
|
|
|
|
|
if (!currentLoginRequest(request, origin)) return
|
|
|
|
|
const token = acceptWechatSession(result)
|
|
|
|
|
const session = getSession()
|
|
|
|
|
if (session?.getString('token') != token) throw new Error('登录状态已变化,请重新登录')
|
|
|
|
|
authToken = token; authUserId = session?.getString('userId') ?? ''; authenticated.value = true
|
|
|
|
|
pendingConsents = []; resetWechatBinding()
|
|
|
|
|
uni.redirectTo({ url: afterLoginUrl })
|
|
|
|
|
}
|
|
|
|
|
const beginWechatLogin = async () => {
|
|
|
|
|
if (!pageVisible || loading.value || authenticated.value || !wechatSupported) return
|
|
|
|
|
// 密码登录保留的协议未发布例外,不适用于新微信入口。
|
|
|
|
|
if (agreementState.value != 'ready' || !agreed.value || selectedTerms == null || selectedPrivacy == null) {
|
|
|
|
|
errorMessage.value = '请先阅读并确认已正式发布的用户协议和隐私政策'; return
|
|
|
|
|
}
|
|
|
|
|
const commands: UTSJSONObject[] = [
|
|
|
|
|
{ agreementCode: 'terms', agreementVersion: selectedTerms!.version, contentHash: selectedTerms!.contentHash, requestId: consentRequestId() },
|
|
|
|
|
{ agreementCode: 'privacy', agreementVersion: selectedPrivacy!.version, contentHash: selectedPrivacy!.contentHash, requestId: consentRequestId() }
|
|
|
|
|
]
|
|
|
|
|
const request = ++generation; const origin = sessionIdentity()
|
|
|
|
|
loading.value = true; errorMessage.value = ''
|
|
|
|
|
try {
|
|
|
|
|
const code = await requestWechatAuthorization()
|
|
|
|
|
if (!currentLoginRequest(request, origin)) return
|
|
|
|
|
const result = await requestWechatLogin(code, commands)
|
|
|
|
|
if (!currentLoginRequest(request, origin)) return
|
|
|
|
|
if (result.getString('status') == 'bind_required') {
|
|
|
|
|
resetWechatBinding(); mode.value = 'phone'; phone.value = ''; password.value = ''
|
|
|
|
|
wechatBindingTicket.value = result.getString('bindingTicket') ?? ''
|
|
|
|
|
wechatBindingExpiresAt = Date.now() + (result.getNumber('expiresIn') ?? 0) * 1000; wechatOriginSession = origin
|
|
|
|
|
} else acceptWechatResult(result, request, origin)
|
|
|
|
|
} catch (error) { if (currentLoginRequest(request, origin)) await handleWechatError(error, request) }
|
|
|
|
|
finally { if (request == generation) loading.value = false }
|
|
|
|
|
}
|
2026-10-14 14:53:13 +08:00
|
|
|
async function confirmLoginConsents(request: number) {
|
|
|
|
|
if (!pageVisible || request != generation || !sameAuthentication()) throw new Error('登录状态已变化,请重新登录')
|
|
|
|
|
if (pendingConsents.length == 0 && agreementState.value == 'ready') {
|
|
|
|
|
if (!agreed.value || selectedTerms == null || selectedPrivacy == null) throw new Error('请重新阅读并确认当前协议')
|
|
|
|
|
pendingConsents = [{ agreement: selectedTerms!, requestId: consentRequestId() }, { agreement: selectedPrivacy!, requestId: consentRequestId() }]; confirmedCount = 0
|
|
|
|
|
}
|
|
|
|
|
while (confirmedCount < pendingConsents.length) {
|
|
|
|
|
const command = pendingConsents[confirmedCount]
|
|
|
|
|
await grantConsent(command.agreement, command.requestId)
|
|
|
|
|
if (!pageVisible || request != generation || !sameAuthentication()) return
|
|
|
|
|
confirmedCount++
|
|
|
|
|
}
|
|
|
|
|
if (pageVisible && request == generation && sameAuthentication()) { pendingConsents = []; uni.redirectTo({ url: afterLoginUrl }) }
|
|
|
|
|
}
|
2026-09-11 20:23:10 +08:00
|
|
|
const resendSms = async () => {
|
2026-10-14 14:53:13 +08:00
|
|
|
if (!pageVisible || authenticated.value || loading.value || countdown.value > 0 || !validPhone()) return
|
2026-10-22 11:24:15 +08:00
|
|
|
if (!bindingReady()) return
|
2026-10-14 14:53:13 +08:00
|
|
|
if (!agreementReadyForLogin()) return
|
2026-10-22 11:24:15 +08:00
|
|
|
const request = generation; const origin = sessionIdentity()
|
2026-09-18 12:59:43 +08:00
|
|
|
loading.value = true; errorMessage.value = ''
|
2026-09-11 20:23:10 +08:00
|
|
|
try {
|
2026-10-22 11:24:15 +08:00
|
|
|
const sent = wechatBindingTicket.value.length > 0 ? await requestWechatBindingCode(wechatBindingTicket.value, phone.value) : await requestSmsCode(phone.value)
|
|
|
|
|
if (!sent) throw new Error('验证码发送失败,请稍后重试')
|
|
|
|
|
if (!currentLoginRequest(request, origin)) return
|
2026-09-11 20:23:10 +08:00
|
|
|
startCountdown()
|
2026-09-20 14:18:55 +08:00
|
|
|
uni.showToast({ title: isDemo ? '请输入 6 位验证码' : '验证码已重新发送', icon: 'none' })
|
2026-10-22 11:24:15 +08:00
|
|
|
} catch (error) { if (currentLoginRequest(request, origin)) await handleWechatError(error, request) } finally { if (request == generation) loading.value = false }
|
2026-09-11 20:23:10 +08:00
|
|
|
}
|
|
|
|
|
const login = async () => {
|
2026-10-14 14:53:13 +08:00
|
|
|
if (!pageVisible || loading.value) return
|
2026-09-11 20:23:10 +08:00
|
|
|
errorMessage.value = ''
|
2026-10-22 11:24:15 +08:00
|
|
|
if (!bindingReady()) return
|
2026-10-14 14:53:13 +08:00
|
|
|
if (authenticated.value && !sameAuthentication()) { clearAuthentication(); errorMessage.value = '登录状态已变化,请重新登录'; return }
|
|
|
|
|
if (pendingConsents.length == 0 && !agreementReadyForLogin()) return
|
|
|
|
|
if (!authenticated.value && !validPhone()) return
|
2026-10-22 11:24:15 +08:00
|
|
|
const request = ++generation; const origin = sessionIdentity()
|
2026-09-11 20:23:10 +08:00
|
|
|
loading.value = true
|
|
|
|
|
try {
|
2026-10-14 14:53:13 +08:00
|
|
|
if (authenticated.value) { await confirmLoginConsents(request); return }
|
|
|
|
|
let token = ''
|
2026-09-11 20:23:10 +08:00
|
|
|
if (mode.value == 'phone') {
|
|
|
|
|
if (!smsSent.value) {
|
2026-10-22 11:24:15 +08:00
|
|
|
const sent = wechatBindingTicket.value.length > 0 ? await requestWechatBindingCode(wechatBindingTicket.value, phone.value) : await requestSmsCode(phone.value)
|
|
|
|
|
if (!sent) throw new Error('验证码发送失败,请稍后重试')
|
|
|
|
|
if (!currentLoginRequest(request, origin)) return
|
2026-09-18 12:59:43 +08:00
|
|
|
smsSent.value = true; startCountdown()
|
2026-09-20 14:18:55 +08:00
|
|
|
uni.showToast({ title: isDemo ? '请输入 6 位验证码' : '验证码已发送', icon: 'none' })
|
2026-10-14 14:53:13 +08:00
|
|
|
return
|
2026-09-11 20:23:10 +08:00
|
|
|
} else {
|
2026-09-15 16:16:55 +08:00
|
|
|
if (!/^[0-9]{6}$/.test(smsCode.value)) throw new Error('请输入 6 位数字验证码')
|
2026-10-22 11:24:15 +08:00
|
|
|
if (wechatBindingTicket.value.length > 0) {
|
|
|
|
|
const result = await bindWechatPhone(wechatBindingTicket.value, phone.value, smsCode.value)
|
|
|
|
|
acceptWechatResult(result, request, origin); return
|
|
|
|
|
}
|
|
|
|
|
const result = await requestSmsLogin(phone.value, smsCode.value)
|
|
|
|
|
if (!currentLoginRequest(request, origin)) return
|
|
|
|
|
token = acceptSession(result, phone.value)
|
2026-09-11 20:23:10 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-09-15 16:16:55 +08:00
|
|
|
if (password.value.length < 6) throw new Error('密码至少需要 6 位')
|
2026-10-22 11:24:15 +08:00
|
|
|
const result = await requestPasswordLogin(phone.value, password.value)
|
|
|
|
|
if (!currentLoginRequest(request, origin)) return
|
|
|
|
|
token = acceptSession(result, phone.value)
|
2026-09-11 20:23:10 +08:00
|
|
|
}
|
2026-10-14 14:53:13 +08:00
|
|
|
if (!pageVisible || request != generation) return
|
|
|
|
|
const session = getSession()
|
|
|
|
|
if (session?.getString('token') != token) throw new Error('登录状态已变化,请重新登录')
|
|
|
|
|
authToken = token; authUserId = session?.getString('userId') ?? ''; authenticated.value = true
|
|
|
|
|
await confirmLoginConsents(request)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (!pageVisible || request != generation) return
|
2026-10-22 11:24:15 +08:00
|
|
|
if (!authenticated.value && origin != sessionIdentity()) return
|
|
|
|
|
if (wechatBindingTicket.value.length > 0) { await handleWechatError(err, request); return }
|
2026-10-14 14:53:13 +08:00
|
|
|
if (agreementChanged(err)) { pendingConsents = []; confirmedCount = 0; agreed.value = false; selectedTerms = null; selectedPrivacy = null; loading.value = false; await loadAgreements(); errorMessage.value = '协议版本已更新,请重新阅读并勾选后继续。' }
|
|
|
|
|
else errorMessage.value = (err instanceof Error ? err.message : '登录失败,请稍后重试') + (authenticated.value ? ';同意记录未确认,请点击按钮以原请求重试。' : '')
|
|
|
|
|
} finally { if (request == generation) loading.value = false }
|
2026-09-11 20:23:10 +08:00
|
|
|
}
|
2026-10-14 14:53:13 +08:00
|
|
|
onShow(async () => {
|
|
|
|
|
pageVisible = true
|
2026-10-22 11:24:15 +08:00
|
|
|
if (wechatBindingTicket.value.length > 0) bindingReady()
|
2026-10-14 14:53:13 +08:00
|
|
|
if (authenticated.value && !sameAuthentication()) clearAuthentication()
|
|
|
|
|
clearInterval(accountTimer); accountTimer = setInterval(() => { if (pageVisible && authenticated.value && !sameAuthentication()) { clearAuthentication(); loading.value = false; errorMessage.value = '登录状态已变化,请重新登录'; loadAgreements() } }, 1000)
|
|
|
|
|
if (pendingConsents.length == 0) await loadAgreements()
|
|
|
|
|
})
|
|
|
|
|
const hidePage = () => { pageVisible = false; generation++; agreementGeneration++; loading.value = false; clearInterval(accountTimer) }
|
|
|
|
|
onHide(() => hidePage())
|
2026-10-22 11:24:15 +08:00
|
|
|
onUnload(() => { hidePage(); clearInterval(countdownTimer); resetWechatBinding(); clearAuthentication() })
|
2026-10-14 14:53:13 +08:00
|
|
|
onUnmounted(() => { clearInterval(countdownTimer); clearInterval(accountTimer) })
|
2026-09-10 10:57:40 +08:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style>
|
2026-09-18 12:59:43 +08:00
|
|
|
.login-content { padding: 20rpx 40rpx 48rpx; }
|
|
|
|
|
.welcome { font-size: 42rpx; line-height: 60rpx; font-weight: 800; margin-top: 38rpx; color: #152443; }
|
|
|
|
|
.login-subtitle { font-size: 26rpx; color: #748198; line-height: 40rpx; margin-top: 8rpx; }
|
|
|
|
|
.form-heading { margin-top: 54rpx; font-size: 28rpx; font-weight: 600; color: #22324a; }
|
2026-10-22 11:24:15 +08:00
|
|
|
.binding-note { margin-top: 16rpx; color: #748198; font-size: 24rpx; line-height: 38rpx; }
|
2026-09-18 12:59:43 +08:00
|
|
|
.input-box { height: 96rpx; margin-top: 22rpx; border: 1rpx solid #dce4ef; border-radius: 16rpx; padding: 0 24rpx; flex-direction: row; align-items: center; background-color: #fafcff; }
|
|
|
|
|
.input-prefix { font-size: 28rpx; color: #31415a; margin-right: 22rpx; flex-shrink: 0; }
|
|
|
|
|
.field-input { flex: 1; min-width: 0; font-size: 28rpx; color: #17243c; }
|
|
|
|
|
.countdown { text-align: right; color: #0868f7; font-size: 24rpx; flex-shrink: 0; margin-left: 12rpx; }
|
|
|
|
|
.error-message { margin-top: 16rpx; color: #d64040; font-size: 24rpx; line-height: 36rpx; }
|
|
|
|
|
.login-button { height: 92rpx; line-height: 92rpx; margin-top: 28rpx; border-radius: 16rpx; color: #fff; font-size: 30rpx; font-weight: 600; background-color: #0868f7; }
|
|
|
|
|
.mode-row { flex-direction: row; justify-content: space-between; align-items: center; margin-top: 12rpx; }
|
|
|
|
|
.mode-link { padding: 16rpx 0; color: #31415a; font-size: 26rpx; }
|
|
|
|
|
.other-login { margin-top: 66rpx; flex-direction: row; align-items: center; justify-content: center; }
|
|
|
|
|
.other-text { font-size: 24rpx; color: #8490a3; flex-shrink: 0; }
|
|
|
|
|
.line { height: 1rpx; background-color: #e5eaf1; flex: 1; margin: 0 20rpx; }
|
|
|
|
|
.social-row { flex-direction: row; justify-content: center; margin-top: 28rpx; }
|
|
|
|
|
.social-button { width: 150rpx; min-height: 112rpx; align-items: center; justify-content: center; margin: 0 26rpx; }
|
2026-09-20 10:52:10 +08:00
|
|
|
.social-icon { width: 62rpx; height: 62rpx; flex-shrink: 0; }
|
2026-09-18 12:59:43 +08:00
|
|
|
.social-label { color: #58677e; font-size: 24rpx; margin-top: 12rpx; }
|
|
|
|
|
.agreement { margin-top: 52rpx; flex-direction: row; align-items: flex-start; }
|
|
|
|
|
.check-hit { width: 44rpx; min-height: 44rpx; flex-shrink: 0; justify-content: center; }
|
|
|
|
|
.check { width: 30rpx; height: 30rpx; border: 2rpx solid #9aa8ba; border-radius: 6rpx; align-items: center; justify-content: center; }
|
|
|
|
|
.checked { background-color: #0868f7; border-color: #0868f7; }
|
|
|
|
|
.check-mark { font-size: 23rpx; color: #fff; }
|
|
|
|
|
.agreement-copy { flex: 1; min-width: 0; color: #748198; font-size: 24rpx; line-height: 42rpx; }
|
|
|
|
|
.agreement-link { color: #0868f7; font-size: 24rpx; line-height: 42rpx; }
|
2026-10-14 14:53:13 +08:00
|
|
|
.agreement-note { color: #748198; font-size: 23rpx; line-height: 38rpx; margin-top: 16rpx; }
|
|
|
|
|
.agreement-retry { color: #0868f7; font-size: 24rpx; padding: 18rpx 0; }
|
2026-09-10 10:57:40 +08:00
|
|
|
</style>
|