“帮我用 AI 做一个报名表”很容易变成一个看起来有输入框、实际上不能可靠使用的网页。字段没有标签,出错时只变红,按键盘找不到提交按钮,甚至只是前端弹出一句“报名成功”,就被误认为已经收到报名信息。

本文把这个需求缩小成一个能交付、能验收的练习:活动报名校验原型。它只有活动昵称和联系邮箱两个字段,所有操作在浏览器本地演示,不发送、不保存报名资料,不接支付或真实业务系统。你可以用 AI 辅助写代码,再用标签、错误提示和键盘操作逐项验收,把“页面做出来了”变成明确的小工具交付过程。

文中的需求和用户均为虚构示例,没有真实订单、客户或收入记录;这个原型也不等于已经通过完整无障碍合规审查。做真实接单项目之前,还需要按客户业务补齐后端、数据处理规则和相关测试。

一、先把服务范围写明白

本例交付一个 HTML 文件,双击可在现代浏览器打开,包含清晰字段标签、必填说明、错误列表、逐字段提示和本地演示成功状态。客户可以检查表单的交互方式,但不能用它收集正式报名数据。

不包含的能力要在需求确认时写清楚:没有服务器存储、邮件发送、验证码、支付、后台名单和并发处理。需要这些能力时应另行拆分范围与验收,不能在交付以后才说“数据库以后再加”。

这类小需求适合展示前端交互和沟通能力。客户购买的是限定范围内可使用、可维护的交付物与修订服务,并非“AI 自动生成了多少代码”。报价应根据页面数量、交互复杂度、兼容范围、测试和修改次数估算,本文不提供保证成交或固定收益数字。

二、把验收条件放在生成代码前面

准备文本编辑器、浏览器和一个空文件夹,不需要安装框架。可使用你已经开通、能够生成代码的 AI 工具;本文不限定某个付费模型,也不替所有工具宣称相同能力。

先写以下六条验收约定:每个字段有可见 label;空值能得到具体提示;邮箱使用浏览器内置格式检查;失败时保留已填内容;错误不能只靠颜色表达;演示通过后明确说明未提交数据。

邮箱格式检查只能排除部分明显错误,不能证明地址存在或属于填表者。昵称也不是实名认证。前端检查帮助用户纠错,不能代替服务端校验与鉴权,这条边界需要在代码说明和交付说明中都保留。

给 AI 的任务可以这样写:

制作一个独立HTML活动报名校验原型,中文,只有昵称和联系邮箱。
必须使用可见label,说明必填状态;不添加外部脚本、字体或追踪服务。
提交时在页面显示错误列表和字段旁提示,用文字说明如何修正。
错误项通过aria-describedby关联,错误字段设置aria-invalid。
首次失败把键盘焦点移到错误汇总,不清空用户输入。
通过校验仅显示“本地演示通过,未提交或保存”,禁止网络请求。
请先解释校验规则,再给完整HTML,并写出键盘验收步骤。

生成以后先读代码,再运行。发现网络请求、嵌入统计代码或自动上传逻辑,先问清用途并移除不在需求范围内的部分,不要把模型输出不经检查交给客户。

原型需求确认、AI起草、本地检查和交付验收的原创流程示意

三、一个可以从零保存的教学原型

把下面完整代码保存为 demo-form.html,文件编码使用 UTF-8。它是本站为本教程编写的简化示例,不包含第三方脚本。页面故意强调演示状态,不假装真实报名完成。

<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>活动报名校验原型</title>
<style>
body{font:18px/1.7 system-ui,sans-serif;margin:2rem auto;padding:0 1rem;max-width:36rem;color:#18352d;background:#f5f8f3}
label{display:block;margin-top:1rem;font-weight:700}
input{box-sizing:border-box;width:100%;font:inherit;padding:.65rem;border:1px solid #66766f;border-radius:.35rem}
button{font:inherit;padding:.6rem 1rem;margin-top:1rem;background:#244f41;color:white;border:0;border-radius:.35rem}
:focus-visible{outline:3px solid #174aa3;outline-offset:3px}
.error{color:#9b2525;font-weight:600}
#errors{border:2px solid #9b2525;padding:1rem;background:#fff}
[hidden]{display:none!important}
</style>
<h1>活动报名校验原型</h1>
<p>两个字段均为必填。仅本地演示,不提交、不保存;请使用虚构资料。</p>
<section id="errors" tabindex="-1" aria-labelledby="error-heading" hidden>
  <h2 id="error-heading">请修正以下问题</h2><ul id="error-list"></ul>
</section>
<form id="demo" novalidate>
  <label for="nickname">活动昵称(必填)</label>
  <input id="nickname" name="nickname" required aria-describedby="nickname-help nickname-error">
  <small id="nickname-help">使用虚构昵称,不能只填空格。</small>
  <p class="error" id="nickname-error"></p>
  <label for="email">联系邮箱(必填)</label>
  <input id="email" name="email" type="email" required aria-describedby="email-help email-error">
  <small id="email-help">可用 demo@example.com 练习;仅检查格式。</small>
  <p class="error" id="email-error"></p>
  <button type="submit">检查示例填写</button>
</form>
<p id="status" role="status"></p>
<script>
const form=document.getElementById('demo');
const box=document.getElementById('errors');
const list=document.getElementById('error-list');
const statusText=document.getElementById('status');
form.addEventListener('submit',event=>{
  event.preventDefault();
  const issues=[];
  for(const id of ['nickname','email']){
    const field=document.getElementById(id);
    const value=field.value.trim();
    let message='';
    if(!value)message=id==='nickname'?'请填写活动昵称,不能只填空格。':'请填写联系邮箱。';
    else if(id==='email'&&!field.validity.valid)message='邮箱格式不正确,例如 demo@example.com。';
    field.setAttribute('aria-invalid',message?'true':'false');
    document.getElementById(id+'-error').textContent=message;
    if(message)issues.push({id,message});
  }
  list.replaceChildren();
  statusText.textContent='';
  for(const issue of issues){
    const li=document.createElement('li');
    const a=document.createElement('a');
    a.href='#'+issue.id;a.textContent=issue.message;
    a.addEventListener('click',event=>{event.preventDefault();document.getElementById(issue.id).focus();});
    li.append(a);list.append(li);
  }
  box.hidden=issues.length===0;
  if(issues.length)box.focus();
  else statusText.textContent='本地演示通过:没有提交或保存任何报名数据。';
});
</script>
</html>

这里使用 novalidate,是为了由页面统一显示教学用错误汇总,邮箱仍通过浏览器提供的 validity 状态检查。取消浏览器默认弹窗,不等于可以取消所有输入检查。修改代码时要保留这个区别,也不要通过删除 required 来让失败测试“通过”。

错误内容使用 textContent 插入,避免把用户输入当成 HTML 执行。示例没有把输入拼入页面,但如果后续增加回显,也应继续使用安全的文本方式,而不是随手改成 innerHTML。

四、逐项试验正常与异常输入

第一次打开文件,先不填写就点按钮。预期页面出现两个错误,昵称和邮箱旁都有说明,焦点移到错误汇总。随后点击汇总中的某条问题,应该回到对应字段,而不是跳到一个不存在的位置。

第二次昵称只填几个空格,邮箱填 demo@example.com,应仍提示昵称不能为空。第三次昵称填“阅读练习”,邮箱填 not-an-email,应只提示邮箱格式问题,昵称内容应保留。

第四次两个字段都填入虚构有效格式,预期错误汇总隐藏,页面显示“本地演示通过”,同时明确没有提交或保存。成功状态不能只写“报名成功”,否则客户或测试者可能误以为服务器已经收到资料。

最后再次把邮箱改错并提交,检查成功文字已清除,旧错误不会重复叠加,也没有残留“通过”的状态。这种“成功后再失败”的回归测试很容易漏掉,实际使用中却很常见。

五、用键盘和小屏幕再验收一遍

不用鼠标,使用 Tab 在昵称、邮箱和按钮之间移动,确认焦点可见,顺序与页面一致。按钮可用键盘激活,出错后能从汇总继续移动,不能让用户困在某个区域。不要为了美观删除焦点轮廓。

把窗口缩窄,检查长邮箱、错误提示和按钮是否仍能阅读,不产生整页横向滚动。放大页面时,文字和输入框不应互相遮挡。CSS 的最大宽度负责限制桌面阅读行长,小屏时则允许内容自然换行,不能把固定宽度强行塞进手机。

有读屏软件和相关经验时,可进一步核对字段名称、必填状态、错误关联与状态提示。但普通浏览器能操作、无明显错误,并不足以证明整个页面符合全部标准。正式无障碍要求应约定目标、范围和专业测试,不提供未经检验的合规承诺。

表单空值、错误邮箱、有效示例与键盘操作的原创验收矩阵

六、如何把原型变成可交付服务

先交付需求确认单,列出字段、校验规则、状态文案、浏览器范围和不涉及的功能。再提供源文件、演示截图与测试记录,每条问题写清输入、预期、实际与修订情况;这样客户能复查,而不是只收到一个“已完成”的聊天回复。

如果客户认可前端原型,希望正式上线,就把后端接口、数据保存、保留期限、删除机制、权限与通知另外列入下一阶段。前端验证逻辑可以辅助用户,但服务端必须独立验证输入;同样,提交成功应以服务端实际接受结果为准,不能直接沿用本例的本地成功提示。

获客演示可以使用这类不收集资料的练习页面,配合一份真实的测试报告说明你解决了哪些问题。不要用客户未授权的后台截图,不要伪造支付或报名订单,也不要把“使用 AI”本身包装成效果保证。

七、成本与修改边界怎么沟通

估算时把需求沟通、AI 起草、人工检查、浏览器测试、说明编写和修改时间分别记下来。工具调用费用只是成本的一部分;客户变更字段、增加后台或要求兼容旧浏览器,通常会改变工作范围,需要重新确认。

交付前约定可以修改几轮、什么算功能缺陷、什么算新增需求。比如错误链接不能定位字段属于原范围缺陷;新增短信验证码则是新的系统能力。把这些写清楚,更有利于稳定交付,而不是承诺“一次付款所有需求都包”。

八、常见故障排查

保存后浏览器显示代码文本,检查文件扩展名是不是变成了 .html.txt。提交按钮没有反应,先查看脚本是否完整复制、引号是否被编辑器替换,再查看浏览器控制台,避免直接删除错误处理逻辑。

错误汇总跳转不到字段,核对 label 的 for、输入框 id 和错误链接对应关系;它们必须一致且唯一。出现提示但读屏不知道对应哪个输入框,检查 aria-describedby 指向的元素是否实际存在,错误时 aria-invalid 是否更新。

如果客户说“填写之后为什么后台没有数据”,应回到最初范围说明:这是不发送数据的校验原型。把原型边界写在页面和交付文档里,比靠口头解释更可靠。一个好的 AI 小工具项目,从可核验的小交付开始,再根据实际需要扩展到可维护的业务系统。

官方参考资料