Skip to content

后台管理系统特点与模版源码的阅读(Day1 2/2) ​

本页对应 day1,共 5 段视频,约 1 小时 28 分。

学习目标 ​

  • 能定位登录页组件源码中的页面容器、项目标题、账号/密码表单项、登录按钮及密码显隐切换逻辑,并说明各部分的职责。
  • 能按“model → rules → prop → trigger/validator”的顺序说明 Element Plus 表单校验配置,并指出校验规则对象、触发时机和自定义校验回调的作用。
  • 能解释登录提交时通过表单实例调用 validate 判断校验结果,并在校验通过后携带账号密码发起登录请求的完整流程。
  • 能阅读 Pinia 用户仓库源码,指出登录 action 的入参、token 本地持久化与删除、消息提示及路由跳转等关键逻辑。
  • 能说明 Axios 二次封装中基础路径、超时时间、环境变量、请求/响应拦截器、公共参数携带、响应数据简化及统一错误处理的作用。
  • 能结合源码说明后台管理系统登录模块中模板假接口与真实接口替换的关系,并概括二次封装 Axios 的必要性。

知识点 ​

后台管理系统特点:先建立“项目观” ​

后台管理系统和面向用户的商城前台不是一套思路。它的第一目标不是“让陌生人注册下单”,而是“让内部员工安全、高效地处理业务”。所以看模板前要先建立项目观。

为什么需要:如果不先理解后台系统的业务边界,读源码时就会把登录、权限、增删改查看成零散页面。怎么想:后台一般没有注册,只有登录;账号密码由公司分配,老板/超级管理员拥有更高权限;页面多为左右结构,左侧菜单、右侧内容;核心业务是增删改查;权限通常分菜单权限和按钮权限。怎么做:读模板时先找登录页、路由表、用户仓库、请求封装,再看菜单和按钮如何受权限控制。易错点:不要把前台“注册—登录—下单”的流程套到后台;不要以为登录成功就万事大吉,菜单和按钮权限才是后台的重点之一。

本节给出的模板入口要保留:

登录模块:模板里的请求为什么“看起来没发” ​

转写稿里反复强调:打开登录页,发现没有网络请求,说明模板里的登录请求很可能是假的。模板为了演示流程,常把账号密码写死,用定时器模拟成功/失败。为什么需要:工作中必须换成公司真实登录接口,否则项目无法真正登录。怎么想:先找登录页 login,再找登录按钮事件,最后追到用户仓库里的 login 方法。怎么做:把模板中写死的判断替换成真实 API 调用;登录成功后保存 token、获取用户信息、跳转首页;失败则提示错误。易错点:不要只改页面不改仓库;不要忘记失败时关闭 loading;不要在没有校验通过时发请求。

表单校验:el-form 的三步流程 ​

登录页通常由 el-form 包多个 el-form-item,每个表单项里放 el-input。转写稿中总结的表单校验流程很关键:

  1. 给 el-form 添加 :model,告诉表单数据收集在哪个对象上;
  2. 给 el-form 添加 :rules,书写校验规则;
  3. 给 el-form-item 添加 prop,属性值必须与校验字段名一致。

为什么需要:账号密码不符合规则时不应该发请求。怎么想:rules 里一个字段可以对应多个规则对象;required 表示必填,trigger 控制触发时机,blur 是失去焦点,change 是文本变化;validator 用于自定义校验,必须调用 callback。怎么做:点击登录时先调用表单实例的 validate:

js
const formRef = ref()

const handleLogin = async () => {
  await formRef.value.validate()
  // 校验通过后再发登录请求
}

易错点:prop 和 rules 字段名不一致会导致校验不生效;validate 返回 Promise,失败会 reject,要配合 await 或 catch;自定义 validator 不调用 callback 会卡住校验。

登录按钮、跳转与用户仓库 ​

转写稿里登录按钮还涉及 loading、路由跳转和 Pinia 用户仓库。为什么需要:请求期间要防止重复点击;登录成功后要保存 token 并跳转;有 redirect 参数时要回到原页面。怎么想:按钮 loading 在请求开始时为真,成功或失败后关闭;登录 action 内调用 API,保存 token;跳转前判断 query.redirect。怎么做:用 defineStore 定义用户小仓库,login action 接收账号密码,调用接口后写 token;组件里 await userStore.login(...) 后再 router.push。易错点:token 要持久化,退出登录要清空;watch 默认懒执行,需要 immediate: true 才能首次收集路由参数;不要把所有请求都塞进组件。

axios 二次封装:面试也常问的“为什么封装” ​

模板里通常已有 axios 二次封装。为什么需要:统一配置基础路径、超时时间,统一携带 token,统一简化响应数据,统一处理网络错误和 token 过期。怎么想:用 axios.create 创建实例;请求拦截器带公共参数;响应拦截器判断业务 code,成功返回简化数据,失败提示并处理登录过期。怎么做:环境变量一般要求以 VITE_ 开头;拦截器里不要忘记 return。易错点:响应拦截器返回 response.data 后,业务层不要再取一层 .data;token 过期清空仓库后应跳登录页;错误提示不要重复弹。

整体读模板的顺序应是:先看后台特点,再看登录页,追表单校验,追用户仓库,最后看 axios 封装。这样源码才能串起来,而不是只记住几个组件名。

阅读目标与路由入口 ​

本节先解决两个问题:一是看模板路由如何组织,尤其是商品管理模块后续要接在哪里;二是看登录业务源码,确认模板里的登录、获取用户信息、退出登录到底是不是真接口。为什么需要:不先看路由和登录,后面加业务模块容易找不到入口,也会把模板假接口当成真接口。怎么想:先看路由表里已有哪些路由、守卫如何判断,再看登录页点击后有没有网络请求。怎么做:打开路由配置,找到商品管理模块对应路由,记录路径、组件和守卫关系;再打开登录页,在浏览器 Network 中观察点击登录是否发请求。转写中明确:模板里“连请求都没有”,登录、获取用户信息、退出登录请求都要替换成公司真实接口。易错点:不要直接在模板假登录上继续开发;商品管理路由要按现有结构补,别破坏守卫和菜单。

登录页结构与 Element Plus 表单 ​

登录页外层是登录容器,背景图可在样式中替换;内部使用 Element Plus 的 el-form 和 el-form-item。为什么需要:先认清“表单”和“表单项”的关系,后面校验才知道加在哪。怎么想:以 el- 开头通常是 Element Plus 组件;el-form 是表单,el-form-item 是其中一项。怎么做:账号、密码各是一个表单项;账号前图标、密码前锁图标、密码后眼睛图标都是组件;输入框用 v-model 收集数据。转写中强调 v-model 把账号、密码收集到 loginForm 对象上。易错点:Vue3 中组件上的 native 已废弃,不要照旧写 @click.native;图标大小、颜色按组件属性调。

密码显隐、聚焦与按钮加载 ​

密码框的 type 在 password 和 text 间切换,点击眼睛图标切换;点击眼睛后还通过 ref 拿到输入框实例并调用聚焦方法;登录按钮用 loading 控制加载效果。为什么需要:这些是登录页常见交互,调试时能判断事件是否真的触发。怎么想:眼睛点击改 type;ref 同名变量拿到组件实例;按钮点击后开始 loading,成功或失败都要关闭。怎么做:点击眼睛时切换类型并让密码框聚焦;登录按钮绑定点击事件,必要时用 .prevent 阻止默认行为。易错点:只开 loading 不关会导致按钮一直转;ref 名字要与模板一致;Vue3 组件内部方法需 defineExpose 暴露后才能被外部 ref 调用。

表单校验三步流程 ​

本节笔记给出核心流程,必须背下来:

js
// 1. el-form 添加 :model,告诉表单数据收集在哪个对象上
// 2. el-form 添加 :rules,书写校验规则
// 3. el-form-item 添加 prop,值为校验字段名

为什么需要:表单校验要先知道“校验谁的数据”,再知道“规则是什么”,最后让每个表单项绑定字段。怎么想:model 指向数据对象,rules 里按字段写规则数组,prop 把表单项和字段对应起来。怎么做:规则数组里一个对象代表一条规则;required 表示必填,trigger 表示触发时机(如 blur、change);validator 是自定义校验回调,接收 value 和 callback。易错点:一个字段可写多条规则;prop 写错字段名规则不生效;自定义校验符合条件 callback(),不符合要 callback(new Error('提示'))。

点击登录先校验再请求 ​

点击登录时不能直接发请求,要先调用表单实例的 validate。为什么需要:账号密码不符合规则时不应发请求。怎么想:validate 返回 Promise,全部校验通过才继续;失败则中断。转写中调试:符合规则返回成功,不符合返回失败,因此后面用 await 等待。怎么做:通过 ref 拿 el-form 实例,await formRef.validate() 后再取 loginForm 的账号密码发登录请求;按钮 loading 在 finally 中关闭。易错点:失败后仍发请求;忘记关闭 loading;校验规则触发了但没等结果。

用户仓库与假登录接口 ​

登录按钮最终调用用户仓库的登录方法。转写中查看 Pinia:用 defineStore 定义小仓库,state 存 token 等数据,登录 action 里用定时器造 Promise,并写死账号密码判断,所以“根本没法请求”。为什么需要:确认模板假接口位置,工作时要换成公司接口。怎么做:保留仓库结构,把 action 内假逻辑替换为真实登录 API;登录成功存 token,失败提示。易错点:不要把模板写死账号密码当业务逻辑;token 本地存储的增删要与退出登录、过期处理一致。

axios 二次封装与环境变量 ​

模板已有 axios 二次封装:配置基础路径、超时时间,设置请求/响应拦截器。请求拦截器可携带公共参数如 token;响应拦截器简化返回数据、统一处理错误,token 过期时清用户仓库并跳登录。为什么需要:统一管理接口,避免每个请求重复写地址、token 和错误处理。怎么想:基础路径来自环境变量,Vite 中通过 import.meta.env 读取,变量需以 VITE_ 开头。怎么做:开发、测试、生产各配环境文件;封装后业务层只写接口函数。易错点:环境变量前缀写错读不到;响应拦截器判断成功码要与后端一致;错误提示和跳转不要重复。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

代码 1:登录表单 + 表单校验三步流程(model / rules / prop)

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>登录 - 后台管理系统</title>
  <style>
    * { box-sizing: border-box; }
    body { margin: 0; height: 100vh; display: flex; align-items: center; justify-content: center;
           background: #2d3a4b; font-family: system-ui, "Microsoft YaHei"; }
    .login-container { width: 380px; padding: 30px; background: #fff; border-radius: 6px; }
    .title { margin: 0 0 24px; text-align: center; font-size: 20px; color: #303133; }
    .form-item { position: relative; margin-bottom: 22px; }
    .form-item input { width: 100%; height: 40px; padding: 0 40px; border: 1px solid #dcdfe6;
                       border-radius: 4px; outline: none; font-size: 14px; }
    .form-item input:focus { border-color: #409eff; }
    .icon { position: absolute; left: 12px; top: 12px; font-size: 14px; color: #c0c4cc; }
    .eye  { position: absolute; right: 12px; top: 10px; cursor: pointer; user-select: none; }
    .err  { height: 18px; line-height: 18px; padding-left: 4px; font-size: 12px; color: #f56c6c; }
    button { width: 100%; height: 40px; border: none; border-radius: 4px; cursor: pointer;
             background: #409eff; color: #fff; font-size: 14px; }
    button:disabled { opacity: .6; cursor: not-allowed; }
  </style>
</head>
<body>
  <div class="login-container">
    <h3 class="title">商品后台管理系统</h3>
    <form id="loginForm" novalidate>
      <div class="form-item">
        <span class="icon">👤</span>
        <input id="username" type="text" placeholder="请输入账号" autocomplete="off">
        <div class="err" id="err-username"></div>
      </div>
      <div class="form-item">
        <span class="icon">🔒</span>
        <input id="password" type="password" placeholder="请输入密码">
        <span class="eye" id="eye">👁</span>
        <div class="err" id="err-password"></div>
      </div>
      <button id="btnLogin" type="submit">登录</button>
    </form>
  </div>

  <script>
    /* ---------- 1. model:告诉表单数据收集在哪个对象身上 ---------- */
    const loginForm = { username: '', password: '' };

    /* ---------- 2. rules:书写表单校验规则 ---------- */
    const loginRules = {
      username: [{
        required: true,
        trigger: 'change',                       // 文本变化就校验
        validator(rule, value, callback) {
          if (!value) return callback(new Error('账号不能为空'));
          if (value.length < 5) return callback(new Error('账号至少 5 位'));
          callback();                            // 校验通过:不传错误
        }
      }],
      password: [{
        required: true,
        trigger: 'blur',                         // 失去焦点才校验
        validator(rule, value, callback) {
          if (!/^\d{6,}$/.test(value)) return callback(new Error('密码至少 6 位数字'));
          callback();
        }
      }]
    };

    /* ---------- 3. prop:表单项绑定校验字段名 ---------- */
    const el = id => document.getElementById(id);
    const inputU = el('username'), inputP = el('password');
    const errBox = { username: el('err-username'), password: el('err-password') };

    function validateField(prop) {
      let msg = '';
      for (const rule of loginRules[prop]) {
        rule.validator(rule, loginForm[prop], err => { if (err) msg = err.message; });
        if (msg) break;
      }
      errBox[prop].textContent = msg;
      return !msg;
    }
    function validateAll() {
      const a = validateField('username');
      const b = validateField('password');
      return a && b;
    }

    // 双向绑定:input 收集数据,按 trigger 决定何时校验
    inputU.addEventListener('input', e => { loginForm.username = e.target.value; validateField('username'); });
    inputP.addEventListener('input', e => { loginForm.password = e.target.value; });
    inputP.addEventListener('blur', () => { loginForm.password = inputP.value; validateField('password'); });

    // 密码后面的眼睛:来回切换 type,并让密码框重新聚焦
    let passwordType = 'password';
    el('eye').addEventListener('click', () => {
      passwordType = passwordType === 'password' ? 'text' : 'password';
      inputP.type = passwordType;
      el('eye').textContent = passwordType === 'password' ? '👁' : '🙈';
      inputP.focus();
    });

    // 登录:先校验,全部通过才发请求
    const btn = el('btnLogin');
    el('loginForm').addEventListener('submit', async e => {
      e.preventDefault();                                  // 阻止表单默认提交
      if (!validateAll()) return;                          // 校验不通过,不发请求
      btn.disabled = true; btn.textContent = '登录中...';   // 开启 loading
      try {
        const data = await mockLogin(loginForm.username, loginForm.password);
        localStorage.setItem('TOKEN', data.token);
        alert('登录成功,token = ' + data.token);
      } catch (err) {
        alert(err.message);
      } finally {
        btn.disabled = false; btn.textContent = '登录';      // 成功/失败都要关 loading
      }
    });

    // 模拟模板里写死的假接口:admin / 111111
    function mockLogin(username, password) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          username === 'admin' && password === '111111'
            ? resolve({ token: 'mock-token-123', username })
            : reject(new Error('用户名或密码不对'));
        }, 1000);
      });
    }
  </script>
</body>
</html>

要点解释与易错点:

  • 三步缺一不可:`

常见错误 ​

  • 现象:登录页能点但没有任何网络请求。原因:直接使用模板的假登录逻辑,未替换公司真实接口。修法:先建 API 层统一管理登录/用户信息/退出接口,再由 Pinia action 调用,组件只触发 action。
  • 现象:表单输入后校验不触发或提交时校验不生效。原因:el-form 缺 model/rules,或 el-form-item 的 prop 与 rules 字段名不一致。修法:按“model 绑对象、rules 写规则、prop 对齐字段名”逐项核对。
  • 现象:自定义校验规则写了但一直通过/一直报错。原因:validator 内未调用 callback,或成功/失败时错误信息传反。修法:成功分支只 callback(),失败分支 callback(new Error('提示'))。
  • 现象:点击登录未校验就发请求。原因:未 await formRef.value.validate(),或未处理 Promise reject。修法:先 await validate(),通过后再调登录 action,失败直接终止。
  • 现象:登录按钮 loading 一直转或失败后不恢复。原因:只在成功分支关闭 loading,异常分支遗漏。修法:用 try/finally,无论成功失败都在 finally 关闭 loading。
  • 现象:密码显隐切换后图标/输入框状态不同步。原因:只改 type 未同步图标状态,或 ref 未同名获取组件实例。修法:统一维护 passwordType 与图标状态,点击后切换并调用输入框 focus()。
  • 现象:环境变量读取为 undefined。原因:变量未以 VITE_ 开头,或读取路径/文件名不对。修法:按当前构建环境在 .env.* 中定义 VITE_ 前缀变量,并通过 import.meta.env 读取。
  • 现象:axios 封装后调用方拿不到预期数据。原因:响应拦截器简化数据结构后,调用方仍按完整响应取值。修法:统一约定返回结构,API 层与页面层按同一约定取值。

练习 ​

  1. 将模板登录页的假登录替换为真实接口链路:登录页 → Pinia action → API 函数 → axios 实例,并保证失败时有错误提示。
  2. 完成登录表单校验:账号必填且不少于 5 位,密码必填且为 6 位数字;分别验证 blur 与 change 触发时机的差异。
  3. 实现密码显隐切换:点击眼睛图标切换输入框类型与图标状态,并让密码框重新聚焦。
  4. 排查并修复登录按钮 loading 问题:要求成功、失败、接口异常三种情况下都能正确关闭 loading。
  5. 配置 axios 二次封装与环境变量:设置基础路径、超时时间、请求拦截携带 token、响应拦截统一处理错误,并说明开发/测试/生产环境如何切换地址。

原视频对照 ​

#视频时长
1模版源码阅读4-登录模块25:52
2模版阅读源码5-表单校验21:10
3模版阅读源码6-登录组件源码完毕14:27
4pinia用户仓库源码查看07:08
5源码阅读7-axios二次封装19:43

小结 ​

  • 后台模板只是脚手架,登录、用户信息、退出等接口必须替换为公司真实接口。
  • 登录主链路应固定为:表单校验通过 → 调 Pinia action → 调 API → axios 发请求 → 成功跳转/失败提示。
  • 表单校验的关键不是“写了很多规则”,而是 model、rules、prop 三者字段严格对齐。
  • 异步登录必须 await validate(),并用 finally 管理 loading,避免重复提交和状态残留。
  • axios 封装要统一基础路径、超时、token 携带、响应简化和网络错误处理,页面层不要各自散写请求。
  • 环境变量必须按构建环境区分并使用规定前缀,否则上线切换地址容易失效。
  • 阅读模板源码建议按“路由 → 登录页 → Pinia 用户仓库 → API/axios → 路由守卫”的顺序串起来。
  • 权限菜单、按钮权限、token 过期跳转等要在登录链路稳定后接入,不要一开始混在登录页里硬改。