{T}

LoginForm 组件抽象与 SinglePage 布局

概述

登录页正式落地时,与其把所有表单结构硬写在页面里,不如先抽出一个独立的 LoginForm 组件,再由页面决定它摆在左、中、右哪个位置。配套地,认证页不应沿用后台三段式主布局,而是用 SinglePage 这类轻布局承载。本章把认证页的实现层次理顺:页面层管路由与布局,SinglePage 管整体承载与全局工具入口,LoginForm 管表单结构,i18n 与第三方登录作为可配置能力补充。

学习目标

  • 理解接入认证页前先清理临时演示路由的必要性
  • 掌握把 LoginForm 抽成独立组件、用 position 解耦布局位置的思路
  • 识别最值得先参数化的三类 props:positiontitleloginItems
  • getIcon() 图标工厂函数统一输入框前缀图标
  • SinglePage 独立布局承载认证页,并用 hideMenu 排除后台菜单
  • 在独立布局顶部挂语言切换与暗黑模式入口,复用已有全局组件
  • 理解 titlehideMenu 可同时存在,元信息照常工作但不进导航
  • 认识分层职责:布局壳与表单组件的边界不可互相越界
  • 掌握认证页与注册/找回页共享 LoginForm 壳子的复用思路

一、接入认证页前先清理临时演示路由

登录注册页是系统正式入口,不应建立在一堆演示页面之上。先删掉为演示菜单折叠、顶部导航临时复制的页面,保留系统真正需要的页面结构,避免登录页一接入后路由和菜单仍残留无意义入口。这一步也有助于后续权限控制和菜单收口。

二、先抽 LoginForm 组件,再用 position 解耦布局位置

把"登录框"抽成独立组件后,页面层负责布局、表单组件负责输入结构,后续注册页、忘记密码页也更容易复用这套壳子。课程抽象出一个关键的布局变量 positionleft / center / right),描述表单整体在页面中的位置——位置是布局属性而非表单业务属性,先做成 props 比后面拆布局逻辑更轻松。

ts
interface LoginFormProps {
  position?: "left" | "center" | "right"
  title?: string
  loginItems?: LoginItem[]
}

三、最值得先参数化的是展示层差异大的部分

positiontitleloginItems 三者带有明显的展示层差异:不同项目登录框位置不同、标题可有可无、第三方入口数量与图标不同。相比起来,账号、手机号、验证码等基础字段在这一节还不需要先做成动态表单模型。先抽项目差异大的部分收益最高,字段先写死、展示入口先抽象,是更务实的推进顺序。

四、基础表单模型先覆盖常见认证字段

先把最常见的认证字段收进一个 reactive 对象:usernamepasswordphonecodeemailrePasswordremember。这一步的重点是"先建模",覆盖用户名 / 密码 / 手机验证码 / 邮箱注册 / 确认密码 / 记住密码等模式,后续再按登录态和注册态拆分。

ts
const form = reactive({
  username: "",
  password: "",
  phone: "",
  code: "",
  email: "",
  rePassword: "",
  remember: false
})

五、getIcon() 统一输入框前缀图标

多个 el-input 需要用户、锁、手机、邮箱等前缀图标,若每个字段都重复写一段图标渲染逻辑,代码很快啰嗦。getIcon() 这类小型工厂函数返回渲染函数,统一风格并减少重复,后续若要统一图标尺寸、颜色也更有价值。

ts
function getIcon(icon: string) {
  return () => h(Iconify, { icon })
}

六、用 SinglePage 独立布局承载认证页

登录页和后台主布局需求完全不同:它不需要 Header / Sidebar / Content 三段式,更需要整页居中、左右分栏或单卡片。通过 definePage({ meta: { layout: "single-page" } }) 让它走单独布局,比复用后台主布局清晰得多,后续加注册页、找回密码页也更方便。

七、position 本质是对齐策略而非绝对定位

控制表单位置时,更合理的是页面整体 flex 占满全屏,再通过 self-start / self-center / self-end 调整主轴位置,并把布局轴改成 flex-col,使"左 / 中 / 右"成为横向自对齐策略。这比写死 left: 0 / right: 0 更灵活、更适合响应式;为避免贴边,还应补横向 margin。位置是"布局壳"行为,不应散落在表单内部节点上。

八、i18n 优先提取高频入口文案

登录页属于入口页,国际化文案更要统一稳定。优先提取 placeholder、登录按钮、注册账号、记住我等高频率文案,收进同一命名空间(如 pages.login.usernamepages.login.submit)。这类页面文案量小但结构完整,很适合作为 i18n 实践页。

九、SinglePage 顶部挂全局工具入口,并用 hideMenu 排除菜单

认证页接入 i18n 与暗黑模式后,若页面没有切换入口,能力实际不可用。由于认证页不走主后台 Header,最自然的做法是在 SinglePage 顶部 fixed 挂一组轻量工具按钮,复用已有的 DarkModeToggleChangeLocale,既不干扰中间表单又所有认证页共享。同时,登录页即便配置了 title,也应通过路由 metahideMenu: true 排除在后台菜单之外——titlehideMenu 可同时存在的,元信息和 i18n 继续工作但不进工作区导航。

ts
definePage({
  meta: {
    layout: "single-page",
    title: "pages.login.title",
    hideMenu: true
  }
})

分层职责应保持清晰:SinglePage 管全屏居中、顶部工具区与路由承载,LoginForm 只管标题、输入框、按钮、第三方入口。不要把暗黑模式、语言切换再塞回 LoginForm;数据(如 locales)可先局部复用,后续再决定是否提到全局配置。

十、认证页与注册、找回页共享壳子

LoginForm 抽出后最大的收益是复用:注册页、找回密码页、甚至企业邀请激活页,都可以共用同一套表单壳,只替换标题、loginItems 与提交逻辑。共享壳子时要守住边界——表单组件只暴露 submit 事件和必要的 v-model 字段,页面层负责决定提交后跳哪、调哪个接口。这样认证相关页面越多,壳子的边际成本越低,也不会出现每个页面重写一遍输入框图标的浪费。

ts
// 注册页复用 LoginForm,只改提交
<LoginForm
  :title="$t('pages.register.title')"
  :login-items="registerItems"
  @submit="handleRegister"
/>

常见问题

问题原因解决方案
登录页一上来就写得很重试图一次性做完登录、注册、短信、验证码和校验先抽 LoginForm 基础壳子,只落地账号密码登录
登录页沿用后台主布局很奇怪Header、Sidebar 对认证页是冗余结构单独做 SinglePage 布局
表单位置切换难控制位置逻辑写死在页面节点里position props,在布局壳做对齐策略
前缀图标写得很重复每个输入框都手写图标组件getIcon() 统一生成
登录页已接 i18n 但用户没法切换语言认证页没提供切换入口SinglePage 顶部复用 ChangeLocale
登录页配了 title 却进了后台菜单菜单系统读取页面元信息生成导航路由 metahideMenu: true
暗黑/语言切换塞回 LoginForm职责越界放 SinglePage 顶部全局工具区
注册页又重写一遍表单没复用壳子共用 LoginForm,只换 title/items/submit
title 和 hideMenu 冲突误以为二者互斥可同时设置,元信息工作但不进菜单

延伸阅读