/** * OmniCard AI (CRMmp) - 统一身份认证登录业务驱动层 * 第一版可用骨架:支持账号密码真实登录对齐后端、手机号验证码、邮箱MagicLink与社交登录入口 */ import { getCurrentLocale, getOmniText, OmniLocale } from '../../common/locale.uts'; import { setAuthToken, setTenantId, httpPost } from '../../common/request.uts'; import { collectHallContact } from '../../common/api.uts'; import { consumePendingCollectCard } from '../owner/hall/useHall.uts'; export type LoginTab = 'password' | 'phone' | 'email'; export type SocialProvider = 'google' | 'apple'; export type LoginCopyKey = | 'title' | 'brandTag' | 'subtitle' | 'desc' | 'tabPassword' | 'tabPhone' | 'tabEmail' | 'usernamePlaceholder' | 'passwordPlaceholder' | 'accountRequired' | 'passwordRequired' | 'testAccountTip' | 'countryCode' | 'phonePlaceholder' | 'sendCode' | 'smsCode' | 'login' | 'magicLink' | 'emailPlaceholder' | 'orContinueWith' | 'google' | 'apple' | 'comingSoon' | 'phoneRequired' | 'emailRequired' | 'codeRequired' | 'termsNotice' | 'termsOfService' | 'privacyPolicy' | 'and' | 'countdownSuffix' | 'loginSuccess' | 'moreWaysComing' | 'loggingIn' | 'sentSuccess'; export type LoginState = { tab: LoginTab; username: string; password: string; countryCode: string; phone: string; smsCode: string; email: string; submitting: boolean; errorMessage: string; countdown: number; }; export type LoginAdminResponse = { token: string; expire?: number; refreshToken?: string; refreshExpire?: number; }; /** 外部登录(短信 / Magic Link / 社交)统一响应契约 */ export type ExternalAuthResponse = { status?: string; token?: string; url?: string; message?: string; }; function isZhLocale(): boolean { return getCurrentLocale() === 'zh-CN'; } /** * 登录文案:能复用 locale.uts 的走词典,缺 key 时中英简单分支 */ export function resolveLoginText(key: LoginCopyKey | string, targetLocale?: string): string { const loc = targetLocale != null && targetLocale !== '' ? targetLocale : getCurrentLocale(); if (key === 'loginSuccess') { return getOmniText(loc as OmniLocale, 'common.success'); } const zh = loc === 'zh-CN'; if (key === 'title') { return zh ? '登录 OmniCard' : 'Sign in to OmniCard'; } if (key === 'brandTag') { return zh ? '全球智能数字名片' : 'Global Digital Identity'; } if (key === 'subtitle') { return zh ? '极简现代商务,连接全球商机' : 'Minimalist global commerce & identity'; } if (key === 'tabPassword') { return zh ? '账号密码' : 'Password'; } if (key === 'tabPhone') { return zh ? '手机号' : 'Phone'; } if (key === 'tabEmail') { return zh ? '邮箱' : 'Email'; } if (key === 'usernamePlaceholder') { return zh ? '用户名或手机号' : 'Username or phone'; } if (key === 'passwordPlaceholder') { return zh ? '登录密码' : 'Password'; } if (key === 'accountRequired') { return zh ? '请填写账号' : 'Enter your username'; } if (key === 'passwordRequired') { return zh ? '请填写密码' : 'Enter your password'; } if (key === 'testAccountTip') { return zh ? '测试账号: admin / 123456 (点击一键填入)' : 'Test account: admin / 123456 (Click to autofill)'; } if (key === 'desc') { return zh ? '使用国际手机号或邮箱继续' : 'Continue with your phone or email'; } if (key === 'countryCode') { return zh ? '区号' : 'Code'; } if (key === 'phonePlaceholder') { return zh ? '手机号' : 'Phone number'; } if (key === 'sendCode') { return zh ? '发送验证码' : 'Send code'; } if (key === 'smsCode') { return zh ? '验证码' : 'Verification code'; } if (key === 'login') { return zh ? '登录' : 'Sign in'; } if (key === 'loggingIn') { return zh ? '登录中' : 'Signing in'; } if (key === 'moreWaysComing') { return zh ? '更多方式即将开放' : 'More sign-in options coming soon'; } if (key === 'magicLink') { return zh ? '发送登录链接' : 'Send sign-in link'; } if (key === 'emailPlaceholder') { return zh ? '工作邮箱' : 'Work email'; } if (key === 'orContinueWith') { return zh ? '或通过以下方式继续' : 'Or continue with'; } if (key === 'google') { return zh ? 'Google' : 'Google'; } if (key === 'apple') { return zh ? 'Apple' : 'Apple'; } if (key === 'comingSoon') { return zh ? '即将开放' : 'Coming soon'; } if (key === 'sentSuccess') { return zh ? '已发送' : 'Sent'; } if (key === 'phoneRequired') { return zh ? '请填写手机号' : 'Enter your phone number'; } if (key === 'emailRequired') { return zh ? '请填写邮箱' : 'Enter your email'; } if (key === 'codeRequired') { return zh ? '请填写验证码' : 'Enter the verification code'; } if (key === 'termsNotice') { return zh ? '登录即代表您已同意' : 'By continuing, you agree to our'; } if (key === 'termsOfService') { return zh ? '服务条款' : 'Terms of Service'; } if (key === 'privacyPolicy') { return zh ? '隐私政策' : 'Privacy Policy'; } if (key === 'and') { return zh ? '与' : 'and'; } if (key === 'countdownSuffix') { return zh ? '秒后重发' : 's'; } return ''; } function toastNone(title: string): void { uni.showToast({ title, icon: 'none', }); } function finishAfterLogin(): void { try { uni.showToast({ title: resolveLoginText('loginSuccess'), icon: 'success', }); } catch (e) {} const pendingCardId = consumePendingCollectCard(); if (pendingCardId !== '') { collectHallContact(pendingCardId).catch(() => {}); } try { if (typeof uni.switchTab === 'function') { uni.switchTab({ url: '/pages/owner/card/index', }); if (typeof window !== 'undefined' && window.location != null) { try { setTimeout(() => { const currentHash = window.location.hash || ''; if (!currentHash.includes('/pages/owner/card/index')) { window.location.hash = '#/pages/owner/card/index'; } }, 300); } catch (e) {} } return; } } catch (e) {} try { if (typeof uni.reLaunch === 'function') { uni.reLaunch({ url: '/pages/owner/card/index', }); } } catch (e) {} } export function createLoginState(): LoginState { return { tab: 'password', username: '', password: '', countryCode: '+86', phone: '', smsCode: '', email: '', submitting: false, errorMessage: '', countdown: 0, }; } export function switchLoginTab(state: LoginState, tab: LoginTab): void { state.tab = tab; state.errorMessage = ''; } /** * 一键填充测试账号 */ export function fillTestAccount(state: LoginState): void { state.username = 'admin'; state.password = '123456'; state.errorMessage = ''; } /** * 倒计时推进函数 */ export function tickCountdown(state: LoginState): void { if (state.countdown > 0) { state.countdown -= 1; } } /** * 登录成功路径:唯一允许写入本地 token 的入口。 * 空 / 空白 token 直接拒绝,绝不 setAuthToken。 */ export function completeLogin(token: string, tenantId?: string): boolean { const safeToken = token != null ? token.trim() : ''; if (safeToken === '') { return false; } setAuthToken(safeToken); const safeTenant = tenantId != null ? tenantId.trim() : ''; if (safeTenant !== '') { setTenantId(safeTenant); } finishAfterLogin(); return true; } function readPasswordLoginToken(res: LoginAdminResponse | null | undefined): string { if (res == null || res.token == null) { return ''; } return String(res.token).trim(); } /** * 账号密码真实登录:对齐后端 Cool-Admin POST /admin/base/open/login * 仅当接口明确返回非空 token 时写入本地凭证;失败 / 非成功 / 无 token 一律不写 */ export async function submitPasswordLogin(state: LoginState): Promise { const username = state.username != null ? state.username.trim() : ''; if (username === '') { state.errorMessage = resolveLoginText('accountRequired'); toastNone(state.errorMessage); return false; } const password = state.password != null ? state.password.trim() : ''; if (password === '') { state.errorMessage = resolveLoginText('passwordRequired'); toastNone(state.errorMessage); return false; } if (state.submitting) { return false; } state.submitting = true; state.errorMessage = ''; try { const res = await httpPost('/admin/base/open/login', { username, password, }); state.submitting = false; const token = readPasswordLoginToken(res); if (token !== '') { return completeLogin(token); } // 接口非失败体但缺少 token:不得 setAuthToken / 不得当成已登录 state.errorMessage = '登录失败,未获取到有效凭证'; toastNone(state.errorMessage); return false; } catch (err: any) { // 业务失败 / 网络异常:绝不写入 token state.submitting = false; const msg = err && err.message ? err.message : '账户或密码不正确~'; state.errorMessage = msg; return false; } } function readExternalStatus(res: ExternalAuthResponse | null | undefined): string { if (res == null || res.status == null) { return ''; } return String(res.status).trim(); } function readExternalToken(res: ExternalAuthResponse | null | undefined): string { if (res == null || res.token == null) { return ''; } return String(res.token).trim(); } function readExternalUrl(res: ExternalAuthResponse | null | undefined): string { if (res == null || res.url == null) { return ''; } return String(res.url).trim(); } /** * 处理外部登录响应:not_configured → comingSoon;sent → 已发送;ok+token → 写凭证 * 失败或不满足写 token 条件时绝不 setAuthToken */ function handleExternalAuthResult( res: ExternalAuthResponse | null | undefined, options?: { onSent?: () => void } ): boolean { const status = readExternalStatus(res); if (status === 'not_configured') { toastNone(resolveLoginText('comingSoon')); return false; } if (status === 'sent') { toastNone(resolveLoginText('sentSuccess')); if (options != null && options.onSent != null) { options.onSent(); } return true; } if (status === 'ok') { const token = readExternalToken(res); if (token !== '') { return completeLogin(token); } } return false; } function openOrCopyAuthUrl(url: string): void { const safeUrl = url != null ? url.trim() : ''; if (safeUrl === '') { return; } let opened = false; try { if (typeof window !== 'undefined' && typeof window.open === 'function') { window.open(safeUrl); opened = true; } } catch (e) {} try { uni.setClipboardData({ data: safeUrl, success: () => { if (!opened) { toastNone(safeUrl); } }, }); } catch (e) { if (!opened) { toastNone(safeUrl); } } } /** * 发送短信验证码:POST /app/user/external/smsSend * sent 才启动倒计时;not_configured / 失败不写 token */ export async function sendSmsCode(state: LoginState): Promise { if (state.countdown > 0) { return false; } const phone = state.phone != null ? state.phone.trim() : ''; if (phone === '') { state.errorMessage = resolveLoginText('phoneRequired'); toastNone(state.errorMessage); return false; } state.errorMessage = ''; try { const res = await httpPost('/app/user/external/smsSend', { phone, }); return handleExternalAuthResult(res, { onSent: () => { state.countdown = 60; }, }); } catch (err: any) { const msg = err && err.message ? String(err.message) : resolveLoginText('comingSoon'); state.errorMessage = msg; if (msg === resolveLoginText('comingSoon') || msg.indexOf('not_configured') >= 0) { toastNone(resolveLoginText('comingSoon')); } return false; } } /** * 国际手机号验证码登录:POST /app/user/external/smsLogin * 仅 status=ok 且有 token 时写入凭证 */ export async function submitPhoneLogin(state: LoginState): Promise { const phone = state.phone != null ? state.phone.trim() : ''; if (phone === '') { state.errorMessage = resolveLoginText('phoneRequired'); toastNone(state.errorMessage); return false; } const smsCode = state.smsCode != null ? state.smsCode.trim() : ''; if (smsCode === '') { state.errorMessage = resolveLoginText('codeRequired'); toastNone(state.errorMessage); return false; } if (state.submitting) { return false; } state.submitting = true; state.errorMessage = ''; try { const res = await httpPost('/app/user/external/smsLogin', { phone, code: smsCode, }); state.submitting = false; return handleExternalAuthResult(res); } catch (err: any) { state.submitting = false; const msg = err && err.message ? String(err.message) : resolveLoginText('comingSoon'); state.errorMessage = msg; return false; } } /** * 邮箱 Magic Link:POST /app/user/external/magicLink * sent 提示已发送;ok+token 才登录;not_configured 不写 token */ export async function sendMagicLink(state: LoginState): Promise { const email = state.email != null ? state.email.trim() : ''; if (email === '') { state.errorMessage = resolveLoginText('emailRequired'); toastNone(state.errorMessage); return false; } if (state.submitting) { return false; } state.submitting = true; state.errorMessage = ''; try { const res = await httpPost('/app/user/external/magicLink', { email, }); state.submitting = false; return handleExternalAuthResult(res); } catch (err: any) { state.submitting = false; const msg = err && err.message ? String(err.message) : resolveLoginText('comingSoon'); state.errorMessage = msg; return false; } } /** * Google / Apple:POST googleStart / appleStart * 有 url 则复制或打开;not_configured 且无 url 不打开假地址;不写 token */ export async function tapSocialLogin(provider: SocialProvider): Promise { const path = provider === 'apple' ? '/app/user/external/appleStart' : '/app/user/external/googleStart'; try { const res = await httpPost(path, {}); const status = readExternalStatus(res); const url = readExternalUrl(res); if (url !== '') { openOrCopyAuthUrl(url); return; } if (status === 'not_configured' || status === '') { toastNone(resolveLoginText('comingSoon')); return; } // 其它无 url 的成功态也不伪造打开地址、不写 token if (status === 'ok' || status === 'sent') { toastNone(resolveLoginText('comingSoon')); } } catch (err: any) { const msg = err && err.message ? String(err.message) : ''; if (msg === '' || msg.indexOf('not_configured') >= 0) { toastNone(resolveLoginText('comingSoon')); } } }