GPT Image 2.5 Flare 与 Sunburst 已上线 EvoLink立即体验
精密金属交互组件的分解组装结构
应用场景

如何用 AI 把需求做成可点击原型:状态、交互与验收

EvoLink Team
EvoLink Team
产品团队
2026年10月16日
13 分钟阅读

卡片很好看,按钮也像能点,但确认预约后什么都没发生。把需求做成交互原型,需要先定义行为,而不只是生成一个屏幕。

先写状态和验收用例,再生成能演示它们的最小界面。 用本地HTML就能先讨论流程,不必一开始搭完整后端。

先确定原型要验证的一个决定

以下使用虚构的双时段预约例子。访客选择上午或下午,每个时段初始两个名额;确认扣一个名额,重复确认不能再扣,取消恢复原时段的一个名额。

演示只使用页面内存,刷新会重置,不同标签页也不共享库存。这是明确的评审范围,不能当成生产预约系统。

状态用户动作预期结果
未选择尝试确认确认不可用,名额仍2/2
已选上午确认上午变1,预约为上午
已预约重复确认不再次扣减
已预约取消上午恢复2,清除预约
无预约再取消名额不变
上午满员场景先选上午,再选下午上午保持0且不能预约;下午仍可预约
切换评审场景更改初始名额清空预约和选择,恢复所选场景的初始值

这张表比“表单要直观”准确,让模型和评审者按同一标准检查。

给 AI 行为要求和实现范围

请生成单文件HTML预约原型。
使用原生label/select/button与可见状态文字。
初始上午2名额、下午2名额;每个页面会话最多一个预约。
未选择时禁止确认,选定时段有名额才允许确认。
已预约时禁止重复确认和切换选择。
取消时只向已预约时段归还一个名额,再清除预约。
重复取消无影响。用polite live region播报状态变化。
不使用外部库、网络请求、个人数据、支付或持久化。
返回完整HTML、打开方法及验收用例。
不承诺并发或生产预约能力。
通过 EvoLink Gemini 4 页面的Agent或API入口生成完整文件,而不是截图或组件清单。需求与代码放在一起,后续变更才有可比的约定。

把状态表与提示词一起提交,要求返回完整HTML文件。打开文件,实际走完下方验收用例;截图或一句“已实现”不能代替这份可操作交付。

  1. 打开 EvoLink Gemini 4 页面,按页面的 Agent 或 API 步骤接入,同时提交状态表和提示词。补充下方评审场景:正常名额为 2/2,上午满员为 0/2;切换场景时清空选择和预约。要求提供清晰布局、剩余名额,以及确认、取消和满员的明确反馈。
  2. 要求返回包含样式和脚本的完整 HTML 文档。使用 Agent 时,让它保存为 booking.html;使用 API 客户端时,将返回的 HTML 文本保存为该文件,去掉 Markdown 代码围栏,不要把 JSON 响应外壳直接保存成 HTML。打开前检查文件是否包含意料之外的远程脚本或网络请求。
  3. 在浏览器打开 booking.html。正常场景选择 Morning 并确认:剩余名额从 2 变为 1,并显示预约成功信息。取消后回到 2,选择清空;通过测试重复调用取消处理函数,名额仍应为 2。
  4. 切换到 Morning full,选择 Morning:显示剩余 0、说明已满员、禁用确认。改选 Afternoon 并确认,其名额从 2 变为 1。切回 Normal 后清空预约并恢复 2/2。场景控件设置的是测试数据,不代表模拟了其他用户或后端并发。
  5. 如果检查失败,把初始状态、具体点击、预期结果、实际结果和 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>
原生按钮元素提供真实的键盘操作控件,状态使用ARIA live region,在不移动焦点时播报变化。仍需在目标环境核对键盘与辅助技术体验。

先检查行为,再调整布局

从未选择开始,确认按钮应禁用。选择上午并确认,查看已预约状态;取消后检查两个时段都回到两个名额。通过界面或自动检查重复取消,确保不能凭空多出第三个名额。

再试另一个时段,先清除已有预约后才能换选择。即使按钮禁用,处理函数也要检查条件,避免代码再次调用处理逻辑时造成错误扣减。

现在可以通过 Review scenarios 直接验证满员分支。网络失败和请求处理中状态仍不在这个本地示例范围内;接入真实服务前,需要另行定义这些状态。

找需求缺口,不只找HTML错误

观察需要确定的问题
用户想预约后换时段先取消再预约,还是独立改签?
两个人同时抢最后一个名额哪个服务管理容量并拒绝冲突?
刷新后预约消失本轮是否包含持久化?
评审者不能用键盘完成标签、焦点顺序和反馈是否足够?

后续实现应由后端负责真实容量和预约冲突。局部计数器正确,不证明多人并发预约成立。

接入真实Next.js应用时,复用仓库组件系统和状态边界,不直接把独立页面当作生产代码。这个HTML的职责是把产品决定变成可评审的东西。

把需求和可运行文件一起交接

保留需求版本、原型、验收表、实际观察和未解决产品问题。取消规则变更时,状态表与界面一起改,再运行受影响用例。

如果下一步是根据资料制作带依据和判分的学习活动,可继续阅读文章转互动测验流程。

常见问题

可点击原型一定要后端吗?

演示本地流程不需要;共享预约和持久化容量需要权威的实际实现。

设计参考之外还要提供什么?

状态、动作、预期、失败处理和范围,图片本身不能定义转换规则。

禁用按钮就是充分校验吗?

还要检查处理函数,不能只靠界面状态阻止无效更新。

为什么要重复取消?

可以发现同一个预约被多次归还名额的逻辑错误。

能直接复制上线吗?

应适配应用架构,补齐后端、验证与无障碍要求后再依赖它。

怎样才算可以评审?

文件能打开、约定交互可执行,交接中写清限制,评审者能复现验收用例。

参考资料

通过统一 API 接入 AI 模型

比较模型能力与当前价格,再按 API 文档接入适合你任务的模型。