
简介这是一份面向前端开发者与uni-app初学者的跨平台登录功能快速实现模板解决移动应用开发中重复编写登录页、表单验证及状态管理等共性问题。资源包含81个文件主体为19个Vue组件如login、reg、pwd等页面及m-input、uni-forms-item等复用组件、17个JSON配置文件pages.json、manifest.json等、14个JS工具脚本含uqrcode.js、univerify.js等增强能力模块以及15个Markdown文档含changelog.md、README说明整体压缩包仅474KB轻量易集成。已有4721人学习下载体现其在实际项目中的高复用价值。用户可直接导入项目使用标准化登录流程获得完整页面结构、双向数据绑定逻辑、本地存储式记住密码实现、HTTPS网络请求封装、响应式布局适配及uni-module生态uni-id、uni-popup等集成范例尤其适合需要快速交付多端登录功能的中小型App或小程序项目。1. 这不是又一个“写完就扔”的登录页uni-app 登录模板真正要解决的是跨端一致性、状态可追溯、错误可拦截这三件事很多团队在用 uni-app 开发多端应用时登录页往往是第一个被快速复制粘贴的模块——H5 里抄一份微信小程序里改个button样式App 端再加个原生插件调用最后发现密码输入框在 iOS 上光标错位、验证码倒计时在支付宝小程序里不触发、token 刷新逻辑在 Android App 里静默失败却无日志。这不是 UI 不统一的问题而是登录这个关键链路缺乏可复用的状态管理契约、可插拔的认证适配层、可审计的错误传播路径。本篇讲的“基于 uni-app 框架的登录模板”核心不是组件样式或表单校验而是构建一个跨平台可收敛、业务逻辑可剥离、异常流可观测的登录基础设施。它适合正在维护 3 个端尤其含 App、已接入统一用户中心、且开始遭遇「登录成功但首页白屏」「微信授权后跳转丢失参数」这类隐性问题的中型项目团队。模板本身不绑定任何后端协议但预留了 OAuth2、手机号一键登录、生物认证等主流扩展点。2. 为什么必须用 Composition API Pinia 重构登录状态而不是直接写data()或vuex2.1 登录状态的本质是「跨页面、跨生命周期、跨平台能力」的聚合体传统data()方式声明isSubmitting,loginForm,errorTips等字段看似简单但在实际场景中会迅速失控微信小程序中onLoad触发时需预填充上一次失败的手机号但data()初始化后无法响应式更新App 端调起原生人脸识别后回调函数在mounted之外执行this指向丢失导致this.$refs.form.validate()报错H5 页面刷新后 token 仍存在 localStorage但data()中的isLoggedIn未同步还原导致导航守卫误判。Pinia 的 store 模块化 Composition API 的逻辑复用能力恰好解决这三类问题。它让登录状态成为独立于页面生命周期的「服务实例」而非依附于某个 Vue 实例的临时数据。2.2 构建useAuthStore最小可行的登录状态容器// stores/auth.ts import { defineStore } from pinia import { ref, computed } from vue import type { LoginParams, UserInfo } from /types/auth export const useAuthStore defineStore(auth, () { // 响应式状态跨页面共享 const token refstring() const userInfo refUserInfo | null(null) const isLoggingIn refboolean(false) const loginError refstring() // 持久化同步关键解决刷新丢失问题 const initFromStorage () { const storedToken uni.getStorageSync(auth_token) const storedUser uni.getStorageSync(auth_user) if (storedToken storedUser) { token.value storedToken userInfo.value JSON.parse(storedUser) as UserInfo } } // 登录主逻辑解耦平台差异 const login async (params: LoginParams) { isLoggingIn.value true loginError.value try { // 此处对接真实 API返回 { token, user_info } const res await uni.request({ url: /api/v1/login, method: POST, data: params, header: { Content-Type: application/json } }) if (res.statusCode 200 res.data.code 0) { const { token: t, user_info: u } res.data.data token.value t userInfo.value u // 统一持久化所有端生效 uni.setStorageSync(auth_token, t) uni.setStorageSync(auth_user, JSON.stringify(u)) return { success: true } } else { throw new Error(res.data.message || 登录失败) } } catch (err) { loginError.value err instanceof Error ? err.message : 网络异常请重试 return { success: false, message: loginError.value } } finally { isLoggingIn.value false } } // 退出登录清理所有端缓存 const logout () { token.value userInfo.value null uni.removeStorageSync(auth_token) uni.removeStorageSync(auth_user) } // 计算属性暴露给视图 const isLoggedIn computed(() !!token.value !!userInfo.value) return { token, userInfo, isLoggingIn, loginError, isLoggedIn, initFromStorage, login, logout } })提示initFromStorage必须在应用启动时主动调用如main.ts中useAuthStore().initFromStorage()否则页面首次加载时isLoggedIn为false导致守卫跳转逻辑失效。2.3 在登录页面中使用避免this.$store的旧式写法!-- pages/login/index.vue -- template view classlogin-container u-form :modelform refformRef u-form-item label手机号 propphone u-input v-modelform.phone placeholder请输入手机号 / /u-form-item u-form-item label验证码 propcode u-input v-modelform.code placeholder请输入验证码 template #right u-button sizesmall clicksendCode :disabledcountdown 0 {{ countdown 0 ? ${countdown}s : 获取 }} /u-button /template /u-input /u-form-item u-button typeprimary clickhandleSubmit :loadingauthStore.isLoggingIn 登录 /u-button /u-form /view /template script setup langts import { ref, onMounted } from vue import { useAuthStore } from /stores/auth import { validatePhone, validateCode } from /utils/validator const authStore useAuthStore() const formRef refany(null) const form ref({ phone: , code: }) const countdown ref(0) // 页面挂载时初始化 store 状态 onMounted(() { authStore.initFromStorage() }) // 发送验证码此处演示跨端差异处理 const sendCode async () { if (!validatePhone(form.value.phone)) { uni.showToast({ title: 手机号格式错误, icon: none }) return } // H5 和小程序走 HTTP 请求App 端可调用原生短信 SDK const platform uni.getSystemInfoSync().platform if (platform ios || platform android) { // 调用原生插件需提前配置 const res await uni.callNativePlugin({ name: SMSPlugin, method: sendCode, params: { phone: form.value.phone } }) if (!res.success) throw new Error(res.message) } else { // 小程序/H5 走 API await uni.request({ url: /api/v1/sms/send, method: POST, data: { phone: form.value.phone } }) } countdown.value 60 const timer setInterval(() { countdown.value-- if (countdown.value 0) clearInterval(timer) }, 1000) } // 表单提交 const handleSubmit async () { const valid await formRef.value?.validate?.() if (!valid) return const result await authStore.login({ phone: form.value.phone, code: form.value.code }) if (result.success) { // 登录成功后跳转自动携带 token uni.navigateTo({ url: /pages/home/index }) } else { uni.showToast({ title: result.message, icon: none }) } } /script注意uni.callNativePlugin是示例写法实际需按 uni-app 原生插件规范 配置 iOS/Android 原生代码。模板中通过platform判断分支确保同一份 JS 逻辑在不同端执行不同路径这才是「跨端一致性」的底层保障。3. 如何让登录模板支持微信小程序、App、H5 三端差异化行为3.1 微信小程序wx.logincode2Session的安全接入模式微信小程序不能直接传明文密码必须走wx.login获取临时 code再由后端调用微信接口换取 openid。登录模板需提供wechatLogin()方法// stores/auth.ts续 // 在 useAuthStore 内部添加 const wechatLogin async () { try { // 微信小程序专属 API const { code } await uni.login({ provider: weixin }) // 用 code 换取用户标识此步骤必须在服务端完成前端只传 code const res await uni.request({ url: /api/v1/wechat/login, method: POST, data: { code } }) if (res.statusCode 200 res.data.code 0) { const { token, user_info } res.data.data token.value token userInfo.value user_info uni.setStorageSync(auth_token, token) uni.setStorageSync(auth_user, JSON.stringify(user_info)) return { success: true } } else { throw new Error(res.data.message || 微信登录失败) } } catch (err) { loginError.value err instanceof Error ? err.message : 微信授权失败 return { success: false, message: loginError.value } } }关键点uni.login({ provider: weixin })返回的code仅能使用一次且有效期 5 分钟必须立即传给后端。前端绝不自行调用微信jscode2session接口涉及appid和secret泄露风险。3.2 App 端集成原生生物认证与设备指纹App 需支持 FaceID/TouchID并将设备 ID 作为登录凭证一部分。模板通过uni.getProvider检测能力并封装// utils/biometric.ts export const checkBiometricSupport (): Promise{ supported: boolean; type: face | fingerprint } { return new Promise((resolve) { uni.getProvider({ service: biometric, success: (res) { if (res.provider.length 0) { // iOS 返回 [face]Android 可能返回 [fingerprint] resolve({ supported: true, type: res.provider[0] as face | fingerprint }) } else { resolve({ supported: false, type: fingerprint }) } }, fail: () resolve({ supported: false, type: fingerprint }) }) }) } // stores/auth.ts续 const biometricLogin async () { const { supported, type } await checkBiometricSupport() if (!supported) { uni.showToast({ title: 设备不支持生物认证, icon: none }) return { success: false } } try { const res await uni.startSmsVerification({ // 注意startSmsVerification 是示例名实际需用 uni-app 支持的原生生物认证 API // 如 iOS 用 LocalAuthenticationAndroid 用 BiometricPrompt promptMessage: 请使用${type face ? 面容 : 指纹}验证 }) if (res.success) { // 生物认证通过后获取设备唯一标识 const deviceInfo await uni.getSystemInfo() const deviceId deviceInfo.deviceId || deviceInfo.model // 携带 deviceId 发起登录 const apiRes await uni.request({ url: /api/v1/biometric/login, method: POST, data: { device_id: deviceId } }) if (apiRes.statusCode 200) { // 同步 token 和用户信息 token.value apiRes.data.token userInfo.value apiRes.data.user_info uni.setStorageSync(auth_token, apiRes.data.token) uni.setStorageSync(auth_user, JSON.stringify(apiRes.data.user_info)) return { success: true } } } } catch (err) { loginError.value 生物认证失败请重试 return { success: false } } }3.3 H5 端兼容第三方 OAuth2如企业微信、钉钉H5 场景下常需跳转到企业微信扫码登录。模板提供oauthRedirect()方法避免硬编码 redirect_uri// stores/auth.ts续 const oauthRedirect (provider: ww | dd | qywx) { const redirectUri encodeURIComponent(uni.getStorageSync(base_url) /callback/oauth) const state Math.random().toString(36).substr(2, 9) // 防 CSRF uni.setStorageSync(oauth_state_${provider}, state) let url if (provider ww) { url https://open.weixin.qq.com/connect/qrconnect?appidYOUR_CORPIDredirect_uri${redirectUri}response_typecodescopesnsapi_loginstate${state}#wechat_redirect } else if (provider dd) { url https://oapi.dingtalk.com/connect/qrconnect?appidYOUR_APPKEYresponse_typecodescopeopenidredirect_uri${redirectUri}state${state} } if (url) { window.location.href url } }参数说明state用于防止跨站请求伪造CSRF回调页/callback/oauth必须校验该值redirect_uri必须与企业后台配置的完全一致包括协议、域名、路径否则微信/钉钉会拒绝跳转。4. 登录失败时如何精准定位问题三类错误的捕获与上报策略4.1 网络层错误区分超时、连接拒绝、HTTP 状态码uni-app 的uni.request默认不抛出网络异常需手动判断// utils/request.ts登录模板配套工具 export const safeRequest (options: UniApp.RequestOptions) { return new Promise((resolve, reject) { uni.request({ ...options, timeout: 10000, // 统一设置超时时间 success: (res) { // HTTP 4xx/5xx 归为业务错误非网络错误 if (res.statusCode 400) { const error { type: http_error as const, statusCode: res.statusCode, data: res.data, requestUrl: options.url } // 上报至监控系统如 Sentry reportError(error) reject(error) } else { resolve(res) } }, fail: (err) { // 网络层错误超时、无网络、SSL 证书错误等 const error { type: network_error as const, message: err.errMsg || 网络请求失败, requestUrl: options.url, timestamp: Date.now() } reportError(error) reject(error) } }) }) } // 在 login 方法中替换 uni.request 为 safeRequest const login async (params: LoginParams) { try { const res await safeRequest({ url: /api/v1/login, method: POST, data: params }) // ...后续处理 } catch (err) { if (err.type network_error) { loginError.value 网络不稳定请检查网络后重试 } else if (err.type http_error) { loginError.value err.data?.message || 服务器繁忙请稍后再试 } } }4.2 表单校验错误结构化错误提示而非字符串拼接避免uni.showToast({ title: 手机号不能为空 })这种弱提示改为字段级反馈// utils/validator.ts export const validatePhone (phone: string): boolean { return /^1[3-9]\d{9}$/.test(phone) } export const validateCode (code: string): boolean { return /^\d{6}$/.test(code) } // 在登录页面中 const handleSubmit async () { const errors: Recordstring, string {} if (!validatePhone(form.value.phone)) { errors.phone 请输入正确的手机号 } if (!validateCode(form.value.code)) { errors.code 验证码为6位数字 } if (Object.keys(errors).length 0) { // 使用 u-form 的 setRules 动态设置错误uView 组件库 formRef.value?.setRules?.({ phone: [{ required: true, message: errors.phone }], code: [{ required: true, message: errors.code }] }) return } // ...继续登录逻辑 }4.3 Token 失效错误拦截 401 并触发静默刷新当 API 返回 401 时不应直接登出而应尝试用 refresh_token 刷新// stores/auth.ts续 // 添加 refresh token 逻辑 const refreshToken async (): Promiseboolean { try { const refresh_token uni.getStorageSync(refresh_token) if (!refresh_token) return false const res await safeRequest({ url: /api/v1/token/refresh, method: POST, data: { refresh_token } }) if (res.statusCode 200) { const { token: newToken } res.data.data token.value newToken uni.setStorageSync(auth_token, newToken) return true } return false } catch { return false } } // 全局请求拦截器在 main.ts 中注册 uni.addInterceptor(request, { invoke(args) { const authStore useAuthStore() if (authStore.token args.url !args.url.includes(/login)) { args.header { ...args.header, Authorization: Bearer ${authStore.token} } } } }) uni.addInterceptor(fail, { invoke(err) { if (err.statusCode 401) { // 尝试刷新 token const authStore useAuthStore() if (authStore.refreshToken()) { // 刷新成功重发原请求需保存原请求参数 console.log(token 刷新成功重试请求) } else { // 刷新失败强制登出 authStore.logout() uni.navigateTo({ url: /pages/login/index }) } } } })注意uni.addInterceptor的fail拦截器需在main.ts中尽早注册确保所有请求都被捕获。refreshToken成功后需重新发起原请求此处简化为日志提示实际项目中建议封装retryRequest方法。5. 进阶技巧如何让登录模板支持「记住我」和「多账号切换」5.1 「记住我」开关控制 token 过期策略与存储方式默认uni.setStorageSync是永久存储但「记住我」应区分长期/短期 token场景Token 存储位置过期策略清理时机未勾选记住我uni.setStorageSync后端返回 short_token2h关闭 App 或页面刷新勾选记住我uni.setStorageSync后端返回 long_token30天用户主动登出或 token 失效// 登录方法增加 rememberMe 参数 const login async (params: LoginParams, rememberMe: boolean false) { // ...请求逻辑不变 if (res.data.data.token) { const storageKey rememberMe ? auth_long_token : auth_token uni.setStorageSync(storageKey, res.data.data.token) // 同时存储 rememberMe 状态用于下次自动勾选 uni.setStorageSync(remember_me, rememberMe) } } // 页面 mounted 时读取 rememberMe 状态 onMounted(() { authStore.initFromStorage() const rememberMe uni.getStorageSync(remember_me) if (rememberMe ! null) { form.value.rememberMe rememberMe } })5.2 多账号切换隔离不同用户的本地缓存当用户需要在「工作账号」和「个人账号」间切换时不能简单覆盖auth_token而应按用户 ID 建立命名空间// utils/storage.ts export const setUserData (userId: string, key: string, value: any) { const namespace user_${userId}_${key} uni.setStorageSync(namespace, value) } export const getUserData T(userId: string, key: string): T | null { const namespace user_${userId}_${key} const data uni.getStorageSync(namespace) return data ? JSON.parse(data) : null } // 修改 login 方法 const login async (params: LoginParams) { // ...API 请求 if (res.data.data.token res.data.data.user_info?.id) { const userId res.data.data.user_info.id setUserData(userId, token, res.data.data.token) setUserData(userId, user_info, res.data.data.user_info) // 当前活跃用户 ID 存入全局 uni.setStorageSync(active_user_id, userId) } } // 切换账号时 const switchUser (userId: string) { const token getUserDatastring(userId, token) const userInfo getUserDataUserInfo(userId, user_info) if (token userInfo) { authStore.token token authStore.userInfo userInfo uni.setStorageSync(active_user_id, userId) } }5.3 登录态验证在 App 启动时执行最小化健康检查避免用户打开 App 时看到首页再闪退到登录页应在onLaunch阶段预检// App.vue script export default { onLaunch() { const authStore useAuthStore() authStore.initFromStorage() // 如果有 token发起轻量级校验不卡住启动 if (authStore.token) { setTimeout(() { uni.request({ url: /api/v1/auth/verify, method: GET, success: (res) { if (res.statusCode ! 200) { authStore.logout() uni.navigateTo({ url: /pages/login/index }) } }, fail: () { authStore.logout() uni.navigateTo({ url: /pages/login/index }) } }) }, 100) } } } /script参数说明setTimeout延迟 100ms 执行校验确保不影响冷启动速度/api/v1/auth/verify接口只需返回{ code: 0 }即可无需返回完整用户信息降低首屏压力。本文还有配套的精品资源点击获取