
如何用 AI 把需求做成可点击原型:状态、交互与验收
卡片很好看,按钮也像能点,但确认预约后什么都没发生。把需求做成交互原型,需要先定义行为,而不只是生成一个屏幕。
先确定原型要验证的一个决定
以下使用虚构的双时段预约例子。访客选择上午或下午,每个时段初始两个名额;确认扣一个名额,重复确认不能再扣,取消恢复原时段的一个名额。
演示只使用页面内存,刷新会重置,不同标签页也不共享库存。这是明确的评审范围,不能当成生产预约系统。
| 状态 | 用户动作 | 预期结果 |
|---|---|---|
| 未选择 | 尝试确认 | 确认不可用,名额仍2/2 |
| 已选上午 | 确认 | 上午变1,预约为上午 |
| 已预约 | 重复确认 | 不再次扣减 |
| 已预约 | 取消 | 上午恢复2,清除预约 |
| 无预约 | 再取消 | 名额不变 |
| 上午满员场景 | 先选上午,再选下午 | 上午保持0且不能预约;下午仍可预约 |
| 切换评审场景 | 更改初始名额 | 清空预约和选择,恢复所选场景的初始值 |
这张表比“表单要直观”准确,让模型和评审者按同一标准检查。
给 AI 行为要求和实现范围
请生成单文件HTML预约原型。
使用原生label/select/button与可见状态文字。
初始上午2名额、下午2名额;每个页面会话最多一个预约。
未选择时禁止确认,选定时段有名额才允许确认。
已预约时禁止重复确认和切换选择。
取消时只向已预约时段归还一个名额,再清除预约。
重复取消无影响。用polite live region播报状态变化。
不使用外部库、网络请求、个人数据、支付或持久化。
返回完整HTML、打开方法及验收用例。
不承诺并发或生产预约能力。把状态表与提示词一起提交,要求返回完整HTML文件。打开文件,实际走完下方验收用例;截图或一句“已实现”不能代替这份可操作交付。
从 EvoLink 请求到可评审文件
- 打开 EvoLink Gemini 4 页面,按页面的 Agent 或 API 步骤接入,同时提交状态表和提示词。补充下方评审场景:正常名额为 2/2,上午满员为 0/2;切换场景时清空选择和预约。要求提供清晰布局、剩余名额,以及确认、取消和满员的明确反馈。
- 要求返回包含样式和脚本的完整 HTML 文档。使用 Agent 时,让它保存为
booking.html;使用 API 客户端时,将返回的 HTML 文本保存为该文件,去掉 Markdown 代码围栏,不要把 JSON 响应外壳直接保存成 HTML。打开前检查文件是否包含意料之外的远程脚本或网络请求。 - 在浏览器打开
booking.html。正常场景选择 Morning 并确认:剩余名额从 2 变为 1,并显示预约成功信息。取消后回到 2,选择清空;通过测试重复调用取消处理函数,名额仍应为 2。 - 切换到 Morning full,选择 Morning:显示剩余 0、说明已满员、禁用确认。改选 Afternoon 并确认,其名额从 2 变为 1。切回 Normal 后清空预约并恢复 2/2。场景控件设置的是测试数据,不代表模拟了其他用户或后端并发。
- 如果检查失败,把初始状态、具体点击、预期结果、实际结果和 HTML 一起交回相同接入流程,要求只修复相关问题,再重跑两个场景。通过网关比较其他模型前,记录验收结果和用量;生成了文件本身并不能证明质量或成本优势。
一份完整可运行的参考原型
booking.html,用浏览器打开:下面是编写的教学参考例子,并非模型调用的实测响应。七种语言保留同一份英文界面代码,方便对照运行。布局与反馈让流程可评审,但还不是最终产品设计。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Session booking · Review prototype</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; background: #f4f6f8; color: #172033; font: 16px/1.6 system-ui, sans-serif; }
main { max-width: 640px; margin: 48px auto; padding: 28px; background: white; border: 1px solid #dce2ea; border-radius: 16px; }
h1 { margin: 8px 0; font-size: 30px; line-height: 1.2; }
p { margin: 12px 0; }
.eyebrow { color: #526077; font-size: 13px; text-transform: uppercase; letter-spacing: .08em; }
label { display: block; font-weight: 600; margin: 16px 0 6px; }
select, button { font: inherit; padding: 10px 14px; border: 1px solid #aeb9c8; border-radius: 8px; }
select { width: 100%; background: white; color: #172033; }
.actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }
button { cursor: pointer; background: white; color: #172033; }
#confirm { background: #193e87; color: white; border-color: #193e87; }
button:disabled { opacity: .5; cursor: not-allowed; }
:focus-visible { outline: 3px solid #b56b00; outline-offset: 3px; }
#status { background: #edf3fc; padding: 16px; border-radius: 8px; }
small { display: block; color: #526077; }
fieldset { margin: 28px 0 0; padding: 16px; border: 1px dashed #aeb9c8; border-radius: 8px; }
legend { font-weight: 600; }
@media (max-width: 680px) { main { margin: 16px; padding: 20px; } }
</style>
</head>
<body><main>
<p class="eyebrow">Interactive prototype · Local demo</p>
<h1>Book a workshop session</h1>
<p>Choose a session, confirm your place, or cancel to release it.</p>
<p id="capacity"></p>
<label for="slot">Session</label>
<select id="slot"><option value="">Choose a session</option>
<option value="am">Morning</option><option value="pm">Afternoon</option></select>
<div class="actions">
<button id="confirm" type="button" disabled>Confirm booking</button>
<button id="cancel" type="button" disabled>Cancel booking</button>
</div>
<p id="status" role="status" aria-live="polite"></p>
<small>One booking per page. Refresh resets this demo; other tabs do not share availability.</small>
<fieldset><legend>Review scenarios</legend>
<label for="scenario">Starting availability</label>
<select id="scenario"><option value="normal">Normal · 2 morning / 2 afternoon</option>
<option value="full">Morning full · 0 morning / 2 afternoon</option></select>
<p><small>Changing the scenario clears the current booking and selection. This control is for review only.</small></p>
</fieldset>
</main>
<script>
const capacity = { am: 2, pm: 2 };
const names = { am: "Morning", pm: "Afternoon" };
let selected = "";
let booking = null;
const slot = document.getElementById("slot");
const scenario = document.getElementById("scenario");
const confirmButton = document.getElementById("confirm");
const cancelButton = document.getElementById("cancel");
const status = document.getElementById("status");
const availability = document.getElementById("capacity");
function render(message = "") {
const valid = Object.hasOwn(capacity, selected);
confirmButton.disabled = !valid || booking !== null || capacity[selected] <= 0;
cancelButton.disabled = booking === null;
slot.disabled = booking !== null;
availability.textContent = `Available: morning ${capacity.am}, afternoon ${capacity.pm}`;
status.textContent = message || (booking !== null
? `Booked: ${names[booking]}. Your place is reserved in this demo.`
: valid && capacity[selected] === 0
? `${names[selected]} is full. Choose another session.`
: "Choose a session to book your place.");
}
slot.addEventListener("change", () => { selected = slot.value; render(); });
confirmButton.addEventListener("click", () => {
if (!Object.hasOwn(capacity, selected) || booking !== null || capacity[selected] <= 0) return;
capacity[selected] -= 1;
booking = selected;
render();
});
cancelButton.addEventListener("click", () => {
if (booking === null) return;
const cancelled = names[booking];
capacity[booking] += 1;
booking = null;
selected = "";
slot.value = "";
render(`${cancelled} booking cancelled. Your place has been released.`);
});
scenario.addEventListener("change", () => {
capacity.am = scenario.value === "full" ? 0 : 2;
capacity.pm = 2;
booking = null;
selected = "";
slot.value = "";
render("Review scenario reset. Choose a session to test it.");
});
render();
</script>
</body></html>先检查行为,再调整布局
从未选择开始,确认按钮应禁用。选择上午并确认,查看已预约状态;取消后检查两个时段都回到两个名额。通过界面或自动检查重复取消,确保不能凭空多出第三个名额。
再试另一个时段,先清除已有预约后才能换选择。即使按钮禁用,处理函数也要检查条件,避免代码再次调用处理逻辑时造成错误扣减。
现在可以通过 Review scenarios 直接验证满员分支。网络失败和请求处理中状态仍不在这个本地示例范围内;接入真实服务前,需要另行定义这些状态。
找需求缺口,不只找HTML错误
| 观察 | 需要确定的问题 |
|---|---|
| 用户想预约后换时段 | 先取消再预约,还是独立改签? |
| 两个人同时抢最后一个名额 | 哪个服务管理容量并拒绝冲突? |
| 刷新后预约消失 | 本轮是否包含持久化? |
| 评审者不能用键盘完成 | 标签、焦点顺序和反馈是否足够? |
后续实现应由后端负责真实容量和预约冲突。局部计数器正确,不证明多人并发预约成立。
接入真实Next.js应用时,复用仓库组件系统和状态边界,不直接把独立页面当作生产代码。这个HTML的职责是把产品决定变成可评审的东西。
把需求和可运行文件一起交接
保留需求版本、原型、验收表、实际观察和未解决产品问题。取消规则变更时,状态表与界面一起改,再运行受影响用例。
常见问题
可点击原型一定要后端吗?
演示本地流程不需要;共享预约和持久化容量需要权威的实际实现。
设计参考之外还要提供什么?
状态、动作、预期、失败处理和范围,图片本身不能定义转换规则。
禁用按钮就是充分校验吗?
还要检查处理函数,不能只靠界面状态阻止无效更新。
为什么要重复取消?
可以发现同一个预约被多次归还名额的逻辑错误。
能直接复制上线吗?
应适配应用架构,补齐后端、验证与无障碍要求后再依赖它。
怎样才算可以评审?
文件能打开、约定交互可执行,交接中写清限制,评审者能复现验收用例。


