import { defineLoginPage, useEffect, useRef, useState } from '@byted-doubao-apps/framework'; import { exitApp, postLoginResult, showToast } from '@byted-doubao-apps/framework/api'; import './index.scss'; interface TextSegment { text: string; url?: string; } const AGREEMENT_SEGMENTS: TextSegment[] = [ { text: '登录即表示你已阅读并同意' }, { text: '《用户服务协议》', url: 'https://example.com' }, { text: '和' }, { text: '《隐私政策》', url: 'https://example.com' } ]; const MOCK_VERIFY_CODE = '123456'; const MOCK_SUCCESS_CODE = 'mock-login-code'; const COUNTDOWN_SECONDS = 60; const TEMPLATE_NOTICE_TITLE = '⚠️ 自定义登录页模板'; const TEMPLATE_NOTICE_DESCRIPTION = '这是自定义登录页模板,仅演示接入方式。真实项目请替换品牌、协议、登录服务以及校验逻辑。'; const loginReportState = { succeeded: false, failurePosted: false, posting: false, failureRequested: false }; function resetLoginReportState() { loginReportState.succeeded = false; loginReportState.failurePosted = false; loginReportState.posting = false; loginReportState.failureRequested = false; } async function reportLoginFailureIfNeeded() { if (loginReportState.succeeded || loginReportState.failurePosted) { return; } if (loginReportState.posting) { loginReportState.failureRequested = true; return; } loginReportState.posting = true; try { await postLoginResult({ result: false }); loginReportState.failurePosted = true; } catch (e) { console.log('[custom-login-page] post failure login result failed', e); } finally { loginReportState.posting = false; } } function normalizeDigits(value: string) { return value.replace(/\D/g, '').slice(0, 11); } function isValidPhone(phone: string) { return /^1\d{10}$/.test(phone); } function isValidCode(code: string) { return /^\d{6}$/.test(code); } function wait(ms: number) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } function CustomLoginPage() { const [phone, setPhone] = useState(''); const [verifyCode, setVerifyCode] = useState(''); const [agreementChecked, setAgreementChecked] = useState(true); const [countdown, setCountdown] = useState(0); const [hasSentCode, setHasSentCode] = useState(false); const [sendingCode, setSendingCode] = useState(false); const [submitting, setSubmitting] = useState(false); const countdownTimerRef = useRef | null>(null); useEffect(() => { resetLoginReportState(); return () => { if (countdownTimerRef.current) { clearInterval(countdownTimerRef.current); countdownTimerRef.current = null; } }; }, []); useEffect(() => { if (countdown <= 0 && countdownTimerRef.current) { clearInterval(countdownTimerRef.current); countdownTimerRef.current = null; } }, [countdown]); const handleLinkClick = (url?: string) => { const targetUrl = url?.trim(); if (!targetUrl) { return; } /** * 真实项目可以新增一个 web-view page 承载协议页,该页面不内置,需业务自行定义。 * * 新增 src/pages/agreement-webview/index.tsx: * import { definePage, getViewData, useEffect, useState } from '@byted-doubao-apps/framework'; * * interface AgreementWebViewPageData { * url?: string; * src?: string; * } * * function AgreementWebViewPage() { * const [webViewUrl, setWebViewUrl] = useState(''); * const [resolved, setResolved] = useState(false); * * useEffect(() => { * const viewData = getViewData(); * setWebViewUrl((viewData?.url ?? viewData?.src ?? '').trim()); * setResolved(true); * }, []); * * if (!resolved) { * return ; * } * * if (!webViewUrl) { * return 缺少协议链接; * } * * return ; * } * * export default definePage({ * render() { * return ; * } * }); * * 3. 在当前点击处理中引入 navigateTo 后跳转: * import { navigateTo } from '@byted-doubao-apps/framework/api'; * * await navigateTo({ * url: `/pages/agreement-webview/index?url=${encodeURIComponent(targetUrl)}` * }); */ showToast({ message: '请实现协议页面', type: 'warning' }); }; const handleClose = async () => { try { console.log('handleClose'); await exitApp(); } catch (e) { console.log('[custom-login-page] navigate back failed', e); } }; const startCountdown = () => { if (countdownTimerRef.current) { clearInterval(countdownTimerRef.current); } setCountdown(COUNTDOWN_SECONDS); countdownTimerRef.current = setInterval(() => { setCountdown((prev) => { if (prev <= 1) { if (countdownTimerRef.current) { clearInterval(countdownTimerRef.current); countdownTimerRef.current = null; } return 0; } return prev - 1; }); }, 1000); }; const handleSendCode = async () => { if (sendingCode || countdown > 0) { return; } if (!agreementChecked) { showToast({ message: '请先勾选协议', type: 'warning' }); return; } if (!isValidPhone(phone)) { showToast({ message: '请输入正确的手机号', type: 'warning' }); return; } setSendingCode(true); try { await wait(600); setHasSentCode(true); startCountdown(); showToast({ message: `验证码已发送(mock: ${MOCK_VERIFY_CODE})`, type: 'success' }); } catch (e) { console.log('[custom-login-page] mock send code failed', e); showToast({ message: '验证码发送失败', type: 'error' }); } finally { setSendingCode(false); } }; const handleSubmit = async () => { if (submitting) { return; } if (!agreementChecked) { showToast({ message: '请先勾选协议', type: 'warning' }); return; } if (!isValidPhone(phone)) { showToast({ message: '请输入正确的手机号', type: 'warning' }); return; } if (!hasSentCode) { showToast({ message: '请先获取验证码', type: 'warning' }); return; } if (!isValidCode(verifyCode)) { showToast({ message: '请输入 6 位验证码', type: 'warning' }); return; } if (verifyCode !== MOCK_VERIFY_CODE) { showToast({ message: '验证码错误,请输入 123456', type: 'error' }); return; } setSubmitting(true); loginReportState.posting = true; try { await wait(500); await postLoginResult({ result: true, code: `${MOCK_SUCCESS_CODE}:${phone}` }); loginReportState.succeeded = true; showToast({ message: '登录成功', type: 'success' }); await handleClose(); } catch (e) { console.log('[custom-login-page] post success login result failed', e); showToast({ message: '登录结果上报失败', type: 'error' }); } finally { loginReportState.posting = false; if (loginReportState.failureRequested && !loginReportState.succeeded) { void reportLoginFailureIfNeeded(); } setSubmitting(false); } }; const sendCodeButtonText = countdown > 0 ? `${countdown}s 后重发` : sendingCode ? '发送中...' : hasSentCode ? '重新发送' : '获取验证码'; const renderSegments = (segments: TextSegment[], baseClassName: string, linkClassName: string) => segments.map((segment, index) => segment.url ? ( { void handleLinkClick(segment.url); }} > {segment.text} ) : ( {segment.text} ) ); return ( {TEMPLATE_NOTICE_TITLE} {TEMPLATE_NOTICE_DESCRIPTION} 手机号 +86 setPhone(normalizeDigits(e.detail?.value ?? '')) } /> 验证码 setVerifyCode((e.detail?.value ?? '').replace(/\D/g, '').slice(0, 6)) } /> 0 ? 'custom-login-page__send-code--disabled' : '' }`} bindtap={handleSendCode} > {sendCodeButtonText} { setAgreementChecked((prev) => !prev); }} > {agreementChecked ? : null} {renderSegments( AGREEMENT_SEGMENTS, 'custom-login-page__agreement-text', 'custom-login-page__agreement-link' )} {submitting ? '登录中...' : '登录'} 取消登录 ); } export default defineLoginPage({ onDestroy() { void reportLoginFailureIfNeeded(); }, render() { return ; } });