PayrollManagementSystem/frontend/README.md
liuwenxin 273ebf4657 chore: 初始化项目结构与登录鉴权模块
后端 FastAPI 六层结构(api/services/repositories/models/schemas/core),
统一响应信封、全局异常、日志脱敏;6 张表经 Alembic 迁移建库,SQL 留存
db/migration;登录鉴权含图形验证码(Redis GETDEL 防重放)、JWT、登录限流、
登出黑名单(TTL=令牌剩余有效期)、登录日志。

前端 Vue3 + TS + Element Plus 骨架,含登录页、路由守卫、请求封装与 Vite 代理。

工程配置:uv 锁定 Python 3.12、ruff 检查与格式化、editorconfig、gitattributes。

测试 184 项(单元 155 / 集成 29)全绿,浏览器端到端 8 项通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-09 18:46:36 +08:00

7.1 KiB
Raw Blame History

前端

AI摘要:衍水集团营销人员工资管理系统的前端。Vue 3 + TypeScript + Vite + Element Plus + Pinia。关键文件 src/api/request.ts(统一请求封装)、src/router/index.ts(路由守卫)、src/store/user.ts(会话状态)、src/views/login/index.vue(登录页)。当前完成登录链路,业务模块页面待接入。

职责

  • 登录页与图形验证码交互
  • 会话状态管理与路由守卫
  • 后续:基础数据、业务数据、绩效核算、报表等业务页面

技术栈与版本约束

组件 版本 说明
Vue 3.5.x
Vue Router 5.x 新大版本;对 vite/pinia 的 peer 均为 optional
Pinia 4.x @vue/devtools-api 是其非可选 peer,必须显式安装,否则装不上或运行时报警
Element Plus 2.14.x 全量引入,产物约 1MB(gzip 347KB);后续可改按需引入
Vite 8.x
TypeScript ~5.9.3 不可用 7.x —— TS 7 是 Go 重写的原生编译器,而 vue-tsc 的 peer 只写 >=5.0.0 拦不住,兼容性未经验证
包管理 pnpm

目录结构

src/
├── api/
│   ├── request.ts    # axios 实例、拦截器、统一信封解包
│   └── auth.ts       # 认证接口封装与类型
├── assets/styles/    # 全局样式
├── components/       # 通用组件(待补充)
├── composables/      # 组合式函数(待补充)
├── router/index.ts   # 路由表与全局守卫
├── store/
│   ├── index.ts      # Pinia 实例
│   └── user.ts       # 会话状态:token、用户信息、登录/登出
├── utils/storage.ts  # localStorage 读写
└── views/
    ├── login/        # 登录页
    └── dashboard/    # 首页(登录链路验证页)

启动

# 前置:后端已在 127.0.0.1:8000 运行
pnpm install
pnpm dev          # http://localhost:5173
pnpm type-check   # vue-tsc 类型检查
pnpm build        # 类型检查 + 生产构建

几个关键约定

开发期用 Vite 代理,不开 CORS /api 被代理到 http://127.0.0.1:8000。用 127.0.0.1 而非 localhost 是因为 Node 17+ 对 localhost 优先解析 IPv6 的 ::1,而 uvicorn 默认只监听 IPv4。 反向地,dev server 自身显式绑定 host: '127.0.0.1' —— 不指定时 Vite 只监听 [::1],会出现「localhost:5173 能开、127.0.0.1:5173 连不上」的困惑。此配置让两种写法都可用。 代理成立后 /api/docs(Swagger)也能从 5173 端口打开,联调期只需一个域名。

401 只表示「登录态失效」 登录接口自身的失败返回 400/403,绝不会是 401。因此前端拦截器可以放心地把 401 一律当作「清 token + 跳登录页」处理,不会出现「用户输错一次密码就被弹回登录页」的循环。 并发 401 用模块级标志去重,避免同时弹出多条提示并反复跳转。

登录失败后必须刷新验证码 后端用 Redis GETDEL 原子消费验证码,任何一次提交后该验证码都已失效。登录页在 catch 分支里调用了 refreshCaptcha() —— 漏掉这一步,用户第二次提交必然报「验证码错误」,且极难自查。

令牌存 localStorage 实现最简,代价是 XSS 可窃取。缓解手段是 JWT 有效期上限 2 小时,以及后端支持 token_version 提版即时失效。若后续要求更高,可改为 httpOnly Cookie,届时只需改 src/utils/storage.ts 与后端签发方式。

视觉设计

设计立意:公司名「衍水」是水,而中文「流水账」字面就是流水之账——系统的全部工作正是记录回款怎么流、绩效怎么算。视觉方向由此而生:深水面板 + 流动的层理线,即公司名的意象,也是业务的隐喻。

配色(金色分两档是有意的:亮金只用于深色底、深金只用于浅色底,两处对比度都达标)

名 值 用途
深渊 #061A22 品牌面板底色
沉碧 #0F3646 面板渐变、水纹线
青金 #C9A227 深色底上的细线与边框
赭金 #8F6520 浅色底上的主按钮(配白字约 6.2:1)
素纸 #FAFAF7 表单侧底色
墨 #16262D 正文

字体:品牌名用宋体(衬线,拟正式、拟账册),正文用系统无衬线,数字与验证码用等宽。 界面不引入网络字体——内网环境字体加载不可靠,靠字重、字距与留白做层次更稳妥。新页面请沿用这套字号节奏。

签名元素:品牌面板的水纹层理线,越往下越密(如潜向深处),载入时用 stroke-dashoffset 逐条画出——一个统一的入场动作,而不是散落的特效。

构图:品牌内容(名称 / 金线 / 系统名 / 主标语 / 环节行)垂直居中聚成一块,配一层柔和径向光晕作落点。 早期版本用 space-between 把品牌与标语顶到两端,中间空出大半屏,两处都成了孤岛——超宽屏会把这种疏离放大得更明显。内容块要限宽(max-width),否则宽屏下文字会被拉散。

写代码时的三条约定

  • 覆盖 Element Plus 主题请改 CSS 变量(--el-button-bg-color 等),不要直接写 background-color —— 后者与 .el-button--primary 同权重,会受样式注入顺序影响而不稳定
  • 中文可在任意字间断行,关键文案的断行位置要显式控制(如主标语拆成两个 <span>),交给浏览器自动换行会出现单字独占一行的难看结果
  • 新页面需响应式到移动端、保留键盘焦点可见、并尊重 prefers-reduced-motion

验证码图片

后端自绘渲染(backend/app/core/captcha_image.py),不再使用 captcha 库的默认输出——那是随机彩色字符 + 重噪点,与界面视觉体系冲突。自绘后字符为墨蓝、底为素纸、干扰线与噪点取自同一套配色。

识别难度由逐字随机旋转(±14°)+ 垂直抖动(±2px)+ 正弦干扰曲线 + 双层噪点共同保证,与原实现相当;笔画加了 1px 描边——本系统用户是营销人员,屏幕与年龄跨度大,验证码过难会直接变成支持负担。

端到端测试

e2e/login_flow.py 用 Playwright 驱动真实浏览器,覆盖 8 项:路由守卫、验证码渲染、失败后自动刷新、登录跳转、用户信息展示、刷新恢复会话、登出、登出后令牌失效。

# 前置:MySQL/Redis 容器、后端、前端 dev server 均在运行
python e2e/login_flow.py

需要 Python playwright 包与 chromium(python -m playwright install chromium)。

常见问题

登录后刷新页面被踢回登录页 —— 检查 /api/v1/auth/me 是否可达;路由守卫靠它恢复会话。

验证码图片不显示 —— 多为代理未生效。访问 http://localhost:5173/api/health 应返回统一信封格式 JSON。

pnpm install 报 peer 依赖错误 —— 确认 @vue/devtools-api 在 dependencies 中(pinia 4 的必需 peer)。