ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

前端页面写不好,往往不是Vue不会,而是设计文档没读懂

2026/9/30 8:20:37 拓冰建站 浏览量
前端页面写不好,往往不是Vue不会,而是设计文档没读懂 前端页面写不好往往不是Vue不会而是设计文档没读懂我第一次独立写前端页面是三年前。那时候刚学完 Vue2看完文档觉得自己行了领导让我做个用户管理列表页我打开 IDE 就开始写el-table。写了两小时页面能跑数据也能显示。我兴冲冲地发给前端同事看他看了三十秒问了我五个问题这个页面在只读角色下是不是应该隐藏操作列手机号字段后端返回的是带星号的你这里为什么又 format 了一遍删除之后是刷新当前页还是回到第一页批量选择跨页保持吗这个状态字段的字典值你写死在前端还是从接口拿我一个都答不上来。不是因为我 Vue 写得烂而是我根本没看过设计文档——那个项目压根就没有完整的前端页面设计文档只有一份后端的接口清单。那次返工了三轮。后来我慢慢明白一件事后端转前端最大的坎不是语法是你不知道一个页面该有多少看不见的约定。这些约定全在设计文档里而后端出身的人习惯性地跳过文档直接看接口。这篇就讲讲我现在怎么读前端页面设计文档以及飞算JavaAI的/前端开发是怎么把这些文档变成可运行页面的。一、为什么后端转前端特别容易跳过设计文档后端写代码有个天然优势接口就是契约。给我一个 Swagger 地址我能把整个 service 层写出来因为入参出参类型都定死了剩下的只是逻辑。前端不是这样。前端的输入除了接口还有一大堆没写在接口里的东西这个字段在表格里要不要显示宽度多少超长怎么截断这个按钮什么条件下禁用这个页面从菜单进来和从详情页跳转进来行为是不是一样列表筛选条件要不要持久化到 URL权限点到底是控制到按钮还是控制到列。这些东西接口文档里一个字都没有全在前端页面设计文档里。后端转前端的人不知道有这份文档存在或者知道但觉得先写出来再说于是就开始返工循环。我现在的习惯是接口文档决定我能不能调通设计文档决定我能不能一次做对。两者的权重设计文档还更高一些。二、飞算JavaAI的前端开发是被上游文档卡住的这一点我第一次用的时候还挺意外。飞算JavaAIhttps://www.feisuanyz.com的/前端开发指令入口是智能会话 → 添加指令 → 选/前端开发但它不是你输了指令它就开写。它依赖/前后端设计产出的一整套文档工作区上下文、技术栈决策、数据库设计、接口设计、技术需求覆盖、后端设计规范基线。如果这些文件缺失它会直接提示你回到/前后端设计去补齐而不是硬着头皮生成一堆对不上的代码。我记得有个纯后端项目我直接跑/前端开发它给我回了缺失提示让我先把前端相关的设计文档补上。当时我嫌麻烦后来发现这个阻断是对的——没有页面设计文档就生成页面产出的东西跟随机拼凑没区别。完整链路是这样的/需求分析产出需求文档 业务设计文档落在项目 docs 目录/前后端设计产出数据库设计、接口设计、前端页面设计、技术栈决策等/前端开发严格依据前端页面设计文档生成代码实现高保真 UI 与交互逻辑npm install→npm run dev跑起来看效果。我特别认可第 2 步和第 3 步的分离。很多团队包括我以前是把设计和写代码混在一起的边写边想最后页面是出来了但没人说得清它为什么长这样。拆开之后设计文档成了可评审的交付物我可以在评审阶段就砍掉不合理的地方而不是等代码写完再改。不过要说实话生成的页面我只当作第一版骨架。它能把结构、字段、接口调用搭对但细节上我每次都要改。这个后面单独说。三、前端页面设计文档里真正要看的五类信息我把前端页面设计文档的信息分成五类。每次拿到文档我按这个顺序过一遍缺任何一类我都会退回要求补齐而不是自己脑补。类别具体要确认什么缺失时的后果页面结构页面分区筛选区/工具栏/表格/分页、组件层级、路由路径与参数布局全靠猜返工率最高字段映射每个展示项对应哪个接口字段、格式化规则、字典来源、空值兜底字段名对不上或格式二次加工出错接口调用调用哪个接口、何时触发挂载/点击/翻页、并发与串行关系、失败重试请求打多次或数据覆盖错乱状态管理哪些状态放组件内、哪些进全局 store、哪些同步到 URL刷新丢状态返回列表筛选条件没了权限控制权限点编码、控制粒度页面/按钮/列、无权限时的表现隐藏 or 禁用越权可见或者直接白屏1. 页面结构看这一块的时候我重点看路由参数。比如一个编辑页/user/edit/:id我就知道必须处理 id 缺失的情况、必须支持从列表带 id 跳入、必须支持浏览器刷新。这些如果文档里没写路由我八成会写成从 store 里取当前选中行然后一刷新页面就白屏——这个坑我踩过两次。2. 字段映射这是最琐碎但最容易出事的一类。我关心三点字段名是不是驼峰。后端 Java 是驼峰但有些老接口返回下划线前端转不转必须在文档里定死否则调试时满屏undefined。格式化在哪一侧做。时间戳转日期后端转还是前端转我现在的约定是后端返回原始值 前端统一格式化但金额和小数位数由后端定避免精度问题。这个不写清楚就会出现前端 format 了一遍已脱敏/已格式化的数据显示成***或者2024-01-01 00:00:00的二次加工错误——就是开头同事问我的那个问题。字典从哪来。状态、类型这类字段字典是前端写死还是从/dict接口拉我的原则是业务字典必须走接口只有纯展示用的枚举比如性别才写死。写死的字典一旦后端改了枚举值前端就显示空白。3. 接口调用除了调哪个接口我还会看触发时机和依赖关系。最典型的是级联下拉省市区三级联动是父级变了立刻清子级还是保留旧值这个文档里不写前端就会做出选了新的省市还显示上一个省的数据这种 bug。4. 状态管理我的判据很简单需要跨页面共享或刷新后保留的进 URL 或 store只在当前组件内部流转的放 local state。列表页的筛选条件我一律同步到 URL query因为用户会复制链接发给同事这个是管理后台的高频操作。5. 权限控制这块是安全事故高发区。我要求文档必须写清权限点编码和无权限时的表现。隐藏和禁用是两种完全不同的语义隐藏意味着不知道有这功能禁用意味着知道但不能用。多数管理后台用隐藏但涉及申请开通这类引导性操作时要用禁用。我读文档的顺序和一个反面案例拿到一份前端页面设计文档我现在的阅读顺序是固定的权限 → 路由 → 字段 → 接口 → 状态。为什么权限排第一因为权限决定页面骨架。如果某个角色看不到工具栏那你的组件树里可能压根不该渲染那一块而不是渲染出来再隐藏。先读权限能让你在搭结构的时候就少一层嵌套。路由排第二因为它决定组件的入参和生命周期。字段和接口是填充物放中间。状态最后看因为它是在前四者都确定之后才浮现出来的东西。反面案例说一个我自己的。去年做一个工单详情页设计文档里写了详情页支持从列表点击跳入我照做了。但文档里还有一句支持从邮件通知链接直接打开我没细看用的是从列表路由传参的方式拿工单 ID。结果运营同学从邮件点进来页面一片空白因为 URL 里没有那个参数而我没做从 query 读取的兜底。这个 bug 修起来只要五分钟——加一句route.query.id ?? route.params.id。但它在生产环境躺了三天因为运营以为工单系统是坏的直接绕过去用微信找研发手工处理。事后复盘问题不在我 Vue 写得不好在于我跳过了文档里那一行不起眼的话。从那以后我给自己定了个规矩设计文档里每一句带支持两个字的话都必须能在代码里找到对应实现。找不到的要么补实现要么回去问清楚为什么没做。四、从设计文档到 Vue 组件一个完整映射光讲原则没意思我拿一个告警规则列表页的真实文档片段演示一遍映射过程。设计文档里的描述简化后页面路径/alert/rule权限点alert:rule:list。顶部为筛选区规则名称模糊查询、状态下拉、启用开关工具栏含新建规则权限点alert:rule:add与批量删除权限点alert:rule:batchDel需选中至少一行才可用主体为表格列包括规则名称、告警级别、状态、最近触发时间、创建人、操作分页为服务端分页默认 20 条筛选条件同步至 URL query状态列用字典alert_rule_status无alert:rule:edit权限时操作列的编辑按钮隐藏。我把这段拆成组件结构是这么落的views/alert/rule/ ├── index.vue // 页面容器拼装筛选区工具栏表格分页 ├── components/ │ ├── RuleFilter.vue // 筛选区受控组件v-model 双向绑定 │ └── RuleTable.vue // 表格纯展示通过 props 收数据、emit 抛事件 ├── composables/ │ └── useRuleList.ts // 接口调用 分页 URL 同步 └── types.ts // 与后端 VO 对齐的 TS 类型关键的映射规则是文档里的区对应组件文档里的状态对应 composable。筛选区、表格是组件分页页码、筛选条件、选中行是状态统一进 composable不散在组件里。下面是useRuleList.ts的核心逻辑注意 URL 同步那一段// composables/useRuleList.tsimport{ref,watch}fromvueimport{useRoute,useRouter}fromvue-routerimport{fetchRulePage,typeRuleQuery,typeRuleVO}from/api/alert/ruleexportfunctionuseRuleList(){constrouteuseRoute()constrouteruseRouter()// 筛选条件初始值从 URL query 恢复保证刷新和分享链接都有效constqueryrefRuleQuery({name:(route.query.nameasstring)??,status:(route.query.statusasstring)??,enabled:route.query.enabledtrue,pageNum:Number(route.query.pageNum??1),pageSize:Number(route.query.pageSize??20),})constrowsrefRuleVO[]([])consttotalref(0)constloadingref(false)asyncfunctionload(){loading.valuetruetry{constresawaitfetchRulePage(query.value)// 后端统一 ResultT 包装业务码非 0 时由拦截器抛出rows.valueres.data.items total.valueres.data.total}finally{loading.valuefalse}}// 筛选条件变化 → 同步回 URL并重置到第一页watch(()({...query.value}),(val){router.replace({query:{...val,pageNum:String(val.pageNum)}})load()},{deep:true})return{query,rows,total,loading,load}}这里有两个点值得说。一是筛选条件变化必须重置 pageNum否则你在第 5 页改了筛选条件请求的还是第 5 页很可能返回空列表用户以为查不到数据。这个 bug 我见过太多次了。二是router.replace而不是push避免用户点返回键时在筛选历史里绕不出来。表格组件里权限控制我用统一的指令处理不要在模板里写v-ifhasPerm(xxx)这种散落判断!-- components/RuleTable.vue -- template el-table :datarows v-loadingloading selection-changeonSelect el-table-column typeselection width48 / el-table-column propruleName label规则名称 min-width180 show-overflow-tooltip / el-table-column proplevel label告警级别 width100 template #default{ row } !-- 级别用字典渲染字典来自全局字典 store -- el-tag :typelevelTagType(row.level){{ dictLabel(alert_level, row.level) }}/el-tag /template /el-table-column el-table-column propstatus label状态 width90 template #default{ row } {{ dictLabel(alert_rule_status, row.status) }} /template /el-table-column el-table-column proplastTriggerTime label最近触发 width170 template #default{ row } !-- 后端返回毫秒时间戳前端统一格式化空值必须有兜底 -- {{ row.lastTriggerTime ? formatTime(row.lastTriggerTime) : — }} /template /el-table-column el-table-column label操作 width140 fixedright template #default{ row } !-- 权限点控制到按钮级无权限时隐藏而非禁用 -- el-button v-permalert:rule:edit link typeprimary clickemit(edit, row) 编辑 /el-button el-button v-permalert:rule:del link typedanger clickemit(delete, row) 删除 /el-button /template /el-table-column /el-table /templatev-perm是个自定义指令逻辑很简单——拿全局权限列表比对不在列表里就直接remove()掉 DOM 节点。这样做的好处是权限判断只有一处实现评审时也好检查// directives/perm.tsimporttype{Directive}fromvueimport{useUserStore}from/store/userexportconstperm:DirectiveHTMLElement,string{mounted(el,binding){constcodesuseUserStore().permCodesif(!codes.includes(binding.value)){el.remove()// 直接移除不留占位}},}接口层我建议单独抽一个api/目录并且类型直接由 OpenAPI 生成不要手写。手写 TS 类型等于把接口契约抄了一遍抄错的概率不低// api/alert/rule.ts —— 该文件由 openapi-generator 产出不要手改importrequestfrom/utils/requestimporttype{Result,PageResult,RuleVO,RuleQuery}from./typesexportfunctionfetchRulePage(params:RuleQuery){returnrequest.getResultPageResultRuleVO(/api/v1/alert/rules,{params})}exportfunctiondeleteRules(ids:number[],idempotencyKey:string){returnrequest.postResultnull(/api/v1/alert/rules/batch-delete,{ids},{headers:{Idempotency-Key:idempotencyKey}})}注意批量删除带了Idempotency-Key——这就是设计文档里接口调用那一类要写清的东西。如果文档没标前端根本不知道要传这个头后端也没强制校验最后就是重复提交产生脏数据。五、飞算JavaAI生成的页面我每次都要改这几处/前端开发生成的页面能跑结构也对但我每次都会改字典来源。它倾向于把状态、类型的中文映射直接写死在前端的 map 里。这在管理后台是隐患我一律改成走全局字典接口。权限粒度。它通常会做页面级和按钮级但列级权限基本没有。涉及敏感字段成本、手机号的列我手动加v-perm。空值和超长兜底。生成的表格列经常没有show-overflow-tooltip长文本会把行高撑开空值显示成空白而不是—。这些都是小改但很影响观感。URL 同步。这一块它基本不管筛选条件不会进 URL。我都会按上面的useRuleList模式重做一遍。分页重置。前面说的改筛选不重置页码生成的代码里普遍存在。改完这五处页面才算能进提测。六、一句话总结Vue 语法三天能学完一个页面该有多少约定要踩一年坑才补得齐先读文档再写代码是后端转前端最省时间的一条路。我的设计文档检查清单拿到文档先过一遍缺一项就退回页面路由路径和参数写了吗刷新场景考虑了吗每个展示字段对应的接口字段名、格式化规则、空值兜底标了吗字典是走接口还是写死写清楚了吗接口的触发时机、依赖关系、失败语义标了吗哪些状态进 store、哪些进 URL分清楚了吗权限点编码给了吗控制到按钮还是列无权限是隐藏还是禁用分页默认值和改条件是否重置页码定了吗这七条能在文档阶段对齐页面基本一次成型。对齐不了就等着在联调阶段一条条吵回来。