Cursor + Claude 3.5 编程实战:不懂代码的普通人如何一天写出一个 Web App

**先说明模型可用性:**截至你实际操作时,Cursor 中可选择的模型会随版本、地区、账户权限和服务策略变化。本文所说的 Claude 3.5,主要指 Claude 3.5 Sonnet 这类适合编程的模型;如果 Cursor 的模型列表中已经没有它,请直接选择当时可用的 Claude 编程模型。操作方法和提示词仍然适用。本文不承诺特定模型免费,也不引用可能过期的价格或额度。

为什么普通人用 AI 编程,仍然经常做不出产品

很多零基础用户第一次打开 Cursor,会直接输入:

帮我做一个网站,要高级、好看、功能完整。

随后通常会遇到几个问题:

  1. 需求太模糊:AI 不知道页面给谁用、解决什么问题。
  2. 一次生成太多代码:出错后不知道改哪一个文件。
  3. 只看页面,不测功能:刷新后数据消失、按钮无效、手机端错位。
  4. 频繁要求“全部重写”:刚修好的功能又被覆盖。
  5. 一开始就接数据库、支付和登录:复杂度突然失控。
  6. 把 AI 当成全自动外包:自己不确认每一步,最后无法维护。

真正适合一天完成的,不是复杂 SaaS,而是一个范围明确的 MVP(最小可用产品)。

本文将用 Cursor + Claude 3.5 完成一个可以实际运行的 Web App:

  • 添加任务
  • 标记任务完成
  • 删除任务
  • 按状态筛选
  • 自动保存到浏览器
  • 25 分钟专注计时
  • 适配手机和电脑
  • 不需要服务器、数据库或第三方接口
  • 可以部署到 GitHub Pages

项目名为:Focus Board 番茄任务板。


一天能做到什么,不能做到什么

适合一天完成

  • 个人工具
  • 单页落地页
  • 待办清单
  • 计算器
  • 习惯打卡工具
  • 内容整理工具
  • 调用一个已有 API 的简单应用
  • 不登录、不支付的静态 Web App

不适合一天承诺完成

  • 真实支付系统
  • 多用户权限
  • 即时聊天
  • 复杂后台管理系统
  • 医疗、金融等高风险业务
  • 大规模数据同步
  • 完整商业级 SaaS

本文中的“一天写出一个 Web App”,指完成一个可以打开、可以操作、可以保存本地数据、可以部署分享的 MVP,不等于一天完成成熟商业产品。


准备工作

1. 安装必要工具

你需要:

  • Cursor 编辑器
  • 一个可用的 Cursor 账户
  • Chrome、Edge 或 Firefox 等现代浏览器
  • Git
  • GitHub 账户,用于部署
  • 可选:Python,用于启动本地静态服务器

Cursor、模型权限和使用限制可能变化,请以 Cursor 官方网站及应用内显示为准。

2. 在 Cursor 中选择模型

打开 Cursor 的聊天或 Agent 面板,在模型选择器中寻找 Claude 3.5 Sonnet。

如果没有这个选项:

  • 不要寻找所谓“隐藏开启方法”
  • 不要使用来历不明的中转接口
  • 直接选择 Cursor 当前提供的 Claude 模型
  • 保留本文的任务拆分方法和提示词即可

Cursor 不同版本可能把功能命名为 Agent、Chat、Edit 等,界面位置也可能调整。核心原则是:

  • 聊天模式:讨论方案、解释错误
  • 编辑或 Agent 模式:创建和修改项目文件
  • 终端:运行项目和执行 Git 命令

3. 创建项目文件夹

新建一个空文件夹:

focus-board

使用 Cursor 打开这个文件夹。

然后在 Cursor 的终端中执行:

git init

这一步不是为了炫技,而是为了让你在 AI 改坏代码后能够回退。


项目结构

最终只需要三个文件:

focus-board/
├── index.html
├── styles.css
└── app.js

我们不使用 React、Vue、数据库或构建工具。

原因很简单:对零基础用户而言,原生 HTML、CSS、JavaScript 更容易运行,也更容易排查问题。


第一步:先让 Claude 写需求,不要立即写代码

把下面的提示词发给 Cursor 中的 Claude 3.5:

你是资深产品经理和前端工程师。

我要在一天内完成一个名为 Focus Board 的单页 Web App,目标用户是需要管理任务和专注时间的普通人。

核心功能:
1. 添加任务
2. 标记任务完成
3. 删除任务
4. 筛选全部、进行中、已完成任务
5. 使用 localStorage 保存任务,刷新页面后不能丢失
6. 提供 25 分钟专注计时器
7. 适配手机和桌面端
8. 不使用后端、数据库、第三方 API 和前端框架

请先不要写代码。

请输出:
- 一句话产品定位
- 用户操作流程
- 功能清单
- 不做的功能
- 验收标准
- 项目文件结构

如果需求存在不合理之处,请直接指出。

为什么要先做这一步

AI编程最重要的不是“让 AI 多写代码”,而是让 AI 明确边界。

你需要重点检查它给出的验收标准,至少应包括:

  • 输入空内容时不能添加任务
  • 新任务能够立即显示
  • 刷新页面后任务仍存在
  • 完成状态能够切换
  • 删除按钮有效
  • 三种筛选状态正确
  • 计时器能够开始、暂停和重置
  • 页面在窄屏下不横向溢出

如果 Claude 自行增加了登录、云同步、AI 分析、排行榜等功能,要求它删除。


第二步:让 Cursor 创建基础文件

继续发送:

请根据刚才确认的需求创建以下三个文件:

- index.html
- styles.css
- app.js

技术要求:
1. 只使用原生 HTML、CSS 和 JavaScript
2. 不引用外部 CDN
3. 不使用内联 JavaScript
4. 所有按钮必须有明确的 type 属性
5. 输入框必须有关联的 label
6. 动态任务内容必须使用 textContent 渲染,不能直接拼接 innerHTML
7. localStorage 读取失败时要有容错
8. 代码中添加适量中文注释
9. 先完成最小可用版本,不增加额外功能

请逐个文件修改,并在完成后说明每个文件的作用。

如果 Cursor 提示是否应用修改,先查看文件范围。确认它只修改当前项目中的三个文件,再接受修改。

为了让教程可以直接执行,下面给出一套完整代码。你既可以手动复制,也可以用它对照 Cursor 的输出。


第三步:编写页面结构

在 index.html 中放入:

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">

  <!-- 让页面在手机上按真实设备宽度显示 -->
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <meta
    name="description"
    content="Focus Board:一个带番茄计时器的本地任务管理工具。"
  >

  <title>Focus Board|番茄任务板</title>

  <!-- 样式与脚本分离,便于维护和排错 -->
  <link rel="stylesheet" href="styles.css">
  <script src="app.js" defer></script>
</head>

<body>
  <main class="app-shell">
    <header class="hero">
      <p class="eyebrow">FOCUS BOARD</p>
      <h2>把今天真正重要的事做完</h2>
      <p class="hero-description">
        添加任务,开始 25 分钟专注,并在当前浏览器中保存进度。
      </p>
    </header>

    <section class="panel" aria-labelledby="task-heading">
      <div class="section-heading">
        <div>
          <p class="section-label">TASKS</p>
          <h3 id="task-heading">今日任务</h3>
        </div>

        <!-- aria-live 会让辅助技术获知数量变化 -->
        <p id="task-count" class="task-count" aria-live="polite">
          0 个未完成
        </p>
      </div>

      <form id="task-form" class="task-form">
        <label class="sr-only" for="task-input">任务内容</label>

        <input
          id="task-input"
          name="task"
          type="text"
          maxlength="80"
          autocomplete="off"
          placeholder="例如:完成产品首页文案"
          required
        >

        <button class="primary-button" type="submit">
          添加任务
        </button>
      </form>

      <p id="form-message" class="form-message" aria-live="polite"></p>

      <div class="toolbar">
        <div class="filters" aria-label="任务筛选">
          <button class="filter-button is-active" type="button" data-filter="all">
            全部
          </button>

          <button class="filter-button" type="button" data-filter="active">
            进行中
          </button>

          <button class="filter-button" type="button" data-filter="completed">
            已完成
          </button>
        </div>

        <button id="clear-completed" class="text-button" type="button">
          清除已完成
        </button>
      </div>

      <!-- JavaScript 会把任务插入这里 -->
      <ul id="task-list" class="task-list"></ul>

      <p id="empty-state" class="empty-state">
        还没有任务,先添加一件今天最重要的事。
      </p>
    </section>

    <section class="panel timer-panel" aria-labelledby="timer-heading">
      <div>
        <p class="section-label">FOCUS TIMER</p>
        <h3 id="timer-heading">25 分钟专注</h3>
        <p class="timer-tip">一次只处理一件事。</p>
      </div>

      <!-- role="timer" 表示这里展示的是计时信息 -->
      <div
        id="timer-display"
        class="timer-display"
        role="timer"
        aria-live="off"
      >
        25:00
      </div>

      <div class="timer-actions">
        <button id="timer-toggle" class="primary-button" type="button">
          开始
        </button>

        <button id="timer-reset" class="secondary-button" type="button">
          重置
        </button>
      </div>

      <p id="timer-status" class="timer-status" aria-live="polite">
        准备开始
      </p>
    </section>

    <footer>
      数据仅保存在当前浏览器中。清除浏览器数据后,任务可能丢失。
    </footer>
  </main>
</body>
</html>

这段 HTML 做了什么

  • form 负责添加任务
  • ul 用于承载动态任务
  • data-filter 保存筛选条件
  • aria-live 用于改善无障碍体验
  • defer 确保 HTML 解析完成后再执行 JavaScript
  • 不使用外部字体或组件库,减少加载失败的可能

第四步:加入响应式样式

在 styles.css 中放入:

/* ---------- 基础变量 ---------- */

:root {
  --background: #f4f1ea;
  --panel: rgba(255, 255, 255, 0.88);
  --text: #20231f;
  --muted: #6b7069;
  --border: #dcded7;
  --primary: #315c47;
  --primary-hover: #264b39;
  --accent: #dff0e6;
  --danger: #a43f3f;
  --shadow: 0 18px 50px rgba(34, 43, 37, 0.08);
  --radius: 22px;
}

/* 让元素宽度包含 padding 和 border */
* {
  box-sizing: border-box;
}

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  color: var(--text);
  background:
    radial-gradient(circle at top left, #e4efe8, transparent 34rem),
    var(--background);
  font-family:
    Inter, system-ui, -apple-system, BlinkMacSystemFont,
    "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}

button,
input {
  font: inherit;
}

button {
  cursor: pointer;
}

button:focus-visible,
input:focus-visible {
  outline: 3px solid rgba(49, 92, 71, 0.25);
  outline-offset: 2px;
}

/* ---------- 页面布局 ---------- */

.app-shell {
  width: min(920px, calc(100% - 32px));
  margin: 0 auto;
  padding: 64px 0 36px;
}

.hero {
  max-width: 680px;
  margin-bottom: 28px;
}

.eyebrow,
.section-label {
  margin: 0 0 8px;
  color: var(--primary);
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.16em;
}

h2,
h3,
p {
  margin-top: 0;
}

h2 {
  margin-bottom: 14px;
  font-size: clamp(2rem, 6vw, 4.6rem);
  line-height: 1.03;
  letter-spacing: -0.045em;
}

h3 {
  margin-bottom: 0;
  font-size: 1.45rem;
}

.hero-description,
.timer-tip,
footer {
  color: var(--muted);
  line-height: 1.7;
}

.panel {
  padding: 28px;
  margin-bottom: 20px;
  border: 1px solid rgba(255, 255, 255, 0.8);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: var(--shadow);
  backdrop-filter: blur(14px);
}

.section-heading,
.toolbar,
.timer-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.task-count {
  margin-bottom: 0;
  color: var(--muted);
  font-size: 0.9rem;
}

/* ---------- 添加任务 ---------- */

.task-form {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 10px;
  margin-top: 24px;
}

.task-form input {
  width: 100%;
  min-height: 48px;
  padding: 0 15px;
  border: 1px solid var(--border);
  border-radius: 12px;
  color: var(--text);
  background: #fff;
}

.task-form input::placeholder {
  color: #969b95;
}

.primary-button,
.secondary-button,
.filter-button,
.text-button,
.delete-button {
  border: 0;
  border-radius: 11px;
  transition:
    background-color 160ms ease,
    color 160ms ease,
    transform 160ms ease;
}

.primary-button {
  min-height: 48px;
  padding: 0 20px;
  color: #fff;
  background: var(--primary);
  font-weight: 700;
}

.primary-button:hover {
  background: var(--primary-hover);
}

.primary-button:active,
.secondary-button:active {
  transform: translateY(1px);
}

.secondary-button {
  min-height: 48px;
  padding: 0 20px;
  color: var(--primary);
  background: var(--accent);
  font-weight: 700;
}

.form-message {
  min-height: 22px;
  margin: 8px 0 0;
  color: var(--danger);
  font-size: 0.9rem;
}

/* ---------- 筛选栏 ---------- */

.toolbar {
  padding: 16px 0;
  border-bottom: 1px solid var(--border);
}

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.filter-button {
  padding: 8px 12px;
  color: var(--muted);
  background: transparent;
}

.filter-button:hover,
.filter-button.is-active {
  color: var(--primary);
  background: var(--accent);
}

.text-button {
  padding: 8px;
  color: var(--muted);
  background: transparent;
}

.text-button:hover {
  color: var(--danger);
}

/* ---------- 任务列表 ---------- */

.task-list {
  display: grid;
  gap: 10px;
  padding: 16px 0 0;
  margin: 0;
  list-style: none;
}

.task-item {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 12px;
  min-width: 0;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: #fff;
}

.task-checkbox {
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: var(--primary);
}

.task-text {
  min-width: 0;
  overflow-wrap: anywhere;
  line-height: 1.5;
}

.task-item.is-completed .task-text {
  color: var(--muted);
  text-decoration: line-through;
}

.delete-button {
  padding: 7px 10px;
  color: var(--muted);
  background: transparent;
}

.delete-button:hover {
  color: var(--danger);
  background: #faecec;
}

.empty-state {
  padding: 28px 12px 10px;
  margin-bottom: 0;
  color: var(--muted);
  text-align: center;
}

/* ---------- 计时器 ---------- */

.timer-panel {
  text-align: center;
}

.timer-display {
  margin: 24px 0;
  font-size: clamp(4rem, 13vw, 7.5rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.06em;
  font-variant-numeric: tabular-nums;
}

.timer-actions {
  justify-content: center;
}

.timer-status {
  min-height: 24px;
  margin: 14px 0 0;
  color: var(--muted);
}

footer {
  padding: 8px;
  font-size: 0.85rem;
  text-align: center;
}

/* 只给屏幕阅读器读取,不在视觉上显示 */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- 手机适配 ---------- */

@media (max-width: 620px) {
  .app-shell {
    width: min(100% - 20px, 920px);
    padding-top: 36px;
  }

  .panel {
    padding: 20px;
    border-radius: 18px;
  }

  .task-form {
    grid-template-columns: 1fr;
  }

  .toolbar,
  .section-heading {
    align-items: flex-start;
    flex-direction: column;
  }

  .task-item {
    grid-template-columns: auto minmax(0, 1fr) auto;
  }
}

/* 尊重用户的“减少动画”系统设置 */
@media (prefers-reduced-motion: reduce) {
  * {
    scroll-behavior: auto !important;
    transition: none !important;
  }
}

第五步:实现任务和计时器逻辑

在 app.js 中放入:

"use strict";

/* ---------- 配置 ---------- */

const STORAGE_KEY = "focus-board-tasks-v1";
const FOCUS_DURATION = 25 * 60;

/* ---------- 获取页面元素 ---------- */

const taskForm = document.querySelector("#task-form");
const taskInput = document.querySelector("#task-input");
const taskList = document.querySelector("#task-list");
const taskCount = document.querySelector("#task-count");
const emptyState = document.querySelector("#empty-state");
const formMessage = document.querySelector("#form-message");
const clearCompletedButton = document.querySelector("#clear-completed");
const filterButtons = document.querySelectorAll("[data-filter]");

const timerDisplay = document.querySelector("#timer-display");
const timerToggleButton = document.querySelector("#timer-toggle");
const timerResetButton = document.querySelector("#timer-reset");
const timerStatus = document.querySelector("#timer-status");

/* ---------- 应用状态 ---------- */

let tasks = loadTasks();
let currentFilter = "all";

let remainingSeconds = FOCUS_DURATION;
let timerId = null;
let timerMessage = "准备开始";

/* ---------- 本地存储 ---------- */

/**
 * 从 localStorage 读取任务。
 * JSON 可能被破坏或被手动修改,所以必须使用 try...catch。
 */
function loadTasks() {
  try {
    const savedValue = localStorage.getItem(STORAGE_KEY);

    if (!savedValue) {
      return [];
    }

    const parsedValue = JSON.parse(savedValue);

    if (!Array.isArray(parsedValue)) {
      return [];
    }

    // 只保留结构基本正确的数据,避免异常内容破坏页面。
    return parsedValue.filter((task) => {
      return (
        task &&
        typeof task.id === "string" &&
        typeof task.text === "string" &&
        typeof task.completed === "boolean"
      );
    });
  } catch (error) {
    console.error("读取任务失败:", error);
    return [];
  }
}

/**
 * 将当前任务保存到浏览器。
 */
function saveTasks() {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
  } catch (error) {
    console.error("保存任务失败:", error);
    formMessage.textContent = "保存失败,请检查浏览器存储权限。";
  }
}

/* ---------- 任务功能 ---------- */

/**
 * 生成任务 ID。
 * 优先使用浏览器提供的 randomUUID。
 */
function createTaskId() {
  if (
    window.crypto &&
    typeof window.crypto.randomUUID === "function"
  ) {
    return window.crypto.randomUUID();
  }

  // 为较旧环境提供简单后备方案。
  return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}

/**
 * 根据当前筛选条件返回任务。
 */
function getVisibleTasks() {
  if (currentFilter === "active") {
    return tasks.filter((task) => !task.completed);
  }

  if (currentFilter === "completed") {
    return tasks.filter((task) => task.completed);
  }

  return tasks;
}

/**
 * 创建一条安全的任务 DOM。
 * 任务文本通过 textContent 设置,不会被当作 HTML 执行。
 */
function createTaskElement(task) {
  const item = document.createElement("li");
  item.className = "task-item";

  if (task.completed) {
    item.classList.add("is-completed");
  }

  const checkbox = document.createElement("input");
  checkbox.type = "checkbox";
  checkbox.className = "task-checkbox";
  checkbox.checked = task.completed;
  checkbox.setAttribute(
    "aria-label",
    task.completed
      ? `将“${task.text}”标记为未完成`
      : `将“${task.text}”标记为已完成`
  );

  checkbox.addEventListener("change", () => {
    toggleTask(task.id);
  });

  const text = document.createElement("span");
  text.className = "task-text";

  // 不使用 innerHTML,避免输入内容被解析为标签或脚本。
  text.textContent = task.text;

  const deleteButton = document.createElement("button");
  deleteButton.type = "button";
  deleteButton.className = "delete-button";
  deleteButton.textContent = "删除";
  deleteButton.setAttribute("aria-label", `删除任务“${task.text}”`);

  deleteButton.addEventListener("click", () => {
    deleteTask(task.id);
  });

  item.append(checkbox, text, deleteButton);

  return item;
}

/**
 * 重新渲染任务列表。
 */
function renderTasks() {
  const visibleTasks = getVisibleTasks();

  // 删除旧节点,防止重复渲染。
  taskList.replaceChildren();

  visibleTasks.forEach((task) => {
    taskList.appendChild(createTaskElement(task));
  });

  const activeCount = tasks.filter((task) => !task.completed).length;

  taskCount.textContent = `${activeCount} 个未完成`;

  if (visibleTasks.length === 0) {
    emptyState.hidden = false;

    if (currentFilter === "active" && tasks.length > 0) {
      emptyState.textContent = "没有进行中的任务。";
    } else if (currentFilter === "completed" && tasks.length > 0) {
      emptyState.textContent = "还没有已完成的任务。";
    } else {
      emptyState.textContent = "还没有任务,先添加一件今天最重要的事。";
    }
  } else {
    emptyState.hidden = true;
  }
}

/**
 * 添加任务。
 */
function addTask(text) {
  const cleanText = text.trim();

  if (!cleanText) {
    formMessage.textContent = "请输入任务内容。";
    taskInput.focus();
    return;
  }

  const newTask = {
    id: createTaskId(),
    text: cleanText,
    completed: false,
    createdAt: Date.now()
  };

  // 新任务放在最前面。
  tasks.unshift(newTask);

  saveTasks();
  renderTasks();

  formMessage.textContent = "";
  taskForm.reset();
  taskInput.focus();
}

/**
 * 切换任务完成状态。
 */
function toggleTask(taskId) {
  tasks = tasks.map((task) => {
    if (task.id === taskId) {
      return {
        ...task,
        completed: !task.completed
      };
    }

    return task;
  });

  saveTasks();
  renderTasks();
}

/**
 * 删除指定任务。
 */
function deleteTask(taskId) {
  tasks = tasks.filter((task) => task.id !== taskId);

  saveTasks();
  renderTasks();
}

/**
 * 清除所有已完成任务。
 */
function clearCompletedTasks() {
  const hasCompletedTask = tasks.some((task) => task.completed);

  if (!hasCompletedTask) {
    formMessage.textContent = "目前没有可清除的已完成任务。";
    return;
  }

  tasks = tasks.filter((task) => !task.completed);

  saveTasks();
  renderTasks();

  formMessage.textContent = "";
}

/* ---------- 计时器功能 ---------- */

/**
 * 把秒数转换为 mm:ss。
 */
function formatTime(totalSeconds) {
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;

  return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}

/**
 * 更新计时器界面。
 */
function renderTimer() {
  const formattedTime = formatTime(remainingSeconds);

  timerDisplay.textContent = formattedTime;
  timerStatus.textContent = timerMessage;

  timerToggleButton.textContent = timerId ? "暂停" : "开始";

  // 在浏览器标签页上显示剩余时间。
  document.title = timerId
    ? `${formattedTime}|Focus Board`
    : "Focus Board|番茄任务板";
}

/**
 * 开始或暂停计时。
 */
function toggleTimer() {
  if (timerId) {
    clearInterval(timerId);
    timerId = null;
    timerMessage = "已暂停";
    renderTimer();
    return;
  }

  timerMessage = "专注中";

  timerId = window.setInterval(() => {
    remainingSeconds -= 1;

    if (remainingSeconds <= 0) {
      remainingSeconds = 0;

      clearInterval(timerId);
      timerId = null;

      timerMessage = "本轮专注完成,休息一下吧。";
    }

    renderTimer();
  }, 1000);

  renderTimer();
}

/**
 * 重置计时器。
 */
function resetTimer() {
  if (timerId) {
    clearInterval(timerId);
    timerId = null;
  }

  remainingSeconds = FOCUS_DURATION;
  timerMessage = "准备开始";

  renderTimer();
}

/* ---------- 事件绑定 ---------- */

taskForm.addEventListener("submit", (event) => {
  // 阻止表单刷新页面。
  event.preventDefault();

  addTask(taskInput.value);
});

clearCompletedButton.addEventListener("click", clearCompletedTasks);

filterButtons.forEach((button) => {
  button.addEventListener("click", () => {
    currentFilter = button.dataset.filter;

    filterButtons.forEach((item) => {
      item.classList.toggle("is-active", item === button);
    });

    renderTasks();
  });
});

timerToggleButton.addEventListener("click", toggleTimer);
timerResetButton.addEventListener("click", resetTimer);

/* ---------- 首次渲染 ---------- */

renderTasks();
renderTimer();

第六步:在本地运行 Web App

方法一:直接打开

双击 index.html,浏览器一般就能运行这个项目。

但不同浏览器对本地文件的处理方式可能略有差异,更推荐使用本地服务器。

方法二:使用 Python 启动静态服务器

如果电脑已安装 Python,在 Cursor 终端中执行:

python -m http.server 8000

部分系统可能需要:

python3 -m http.server 8000

然后在浏览器中打开:

http://localhost:8000

停止服务器时,在终端中按:

Ctrl + C

这不是一个对外开放的生产服务器,只用于本地预览。


第七步:不要让 AI 自测,自己按清单验收

按以下顺序手动测试。

任务功能

  • [ ] 不输入内容,点击“添加任务”,出现提示
  • [ ] 输入普通文字,可以成功添加
  • [ ] 输入包含 <script> 的文字,只显示文本,不执行代码
  • [ ] 点击复选框,任务变成完成状态
  • [ ] 再次点击复选框,可以恢复为未完成
  • [ ] “全部”筛选正确
  • [ ] “进行中”筛选正确
  • [ ] “已完成”筛选正确
  • [ ] 删除按钮能够删除对应任务
  • [ ] 清除已完成不会删除未完成任务
  • [ ] 刷新页面后任务仍然存在

计时器功能

  • [ ] 点击开始后,每秒减少一次
  • [ ] 点击暂停后不再减少
  • [ ] 再次开始后从当前时间继续
  • [ ] 点击重置后回到 25:00
  • [ ] 计时期间浏览器标签显示剩余时间

响应式测试

打开浏览器开发者工具,切换到手机尺寸,检查:

  • [ ] 页面没有横向滚动条
  • [ ] 输入框和按钮没有溢出
  • [ ] 任务过长时能够换行
  • [ ] 删除按钮仍然可点击
  • [ ] 计时数字没有超出卡片

第八步:让 Claude 做一次受控代码审查

不要直接说“优化所有代码”。使用明确的审查提示词:

请审查当前项目的 index.html、styles.css 和 app.js。

审查重点:
1. 是否存在 JavaScript 运行错误
2. localStorage 是否有容错
3. 用户输入是否可能被当作 HTML 执行
4. 是否存在重复绑定事件
5. 计时器是否可能同时启动多个 setInterval
6. 手机宽度下是否可能横向溢出
7. label、按钮和动态状态是否满足基本无障碍要求

规则:
- 先输出问题清单
- 按严重程度排序
- 不要直接修改代码
- 不要引入框架、依赖、后端或新功能
- 如果没有发现问题,请明确说明,不要为了修改而修改

看完问题后,再让它逐项修复:

只修复刚才确认的第 1、2 项问题。

要求:
- 不重构无关代码
- 不改变现有视觉设计
- 修改后说明改了哪些行和原因
- 给出对应的手动测试方法

这就是比较可靠的 Cursor 工作方式:先审查,再确认,再小范围修改。


第九步:用 Git 保存可回退版本

确认功能正常后,在 Cursor 终端执行:

git add .
git commit -m "完成 Focus Board MVP"

如果 Git 提示没有配置身份,可以按照终端提示配置名称和邮箱。邮箱是否公开取决于你的 Git 配置和代码托管平台设置,不要随意填写他人的信息。

后续每完成一组功能就提交一次,例如:

git add .
git commit -m "修复移动端布局"
git add .
git commit -m "完善任务存储容错"

AI 改坏后查看变化

git diff

查看提交记录:

git log --oneline

如果还没有提交,不要轻易运行你不理解的删除或回退命令。可以先把项目文件夹复制一份作为备份,再让 Cursor 解释每条 Git 命令的影响。


第十步:部署到 GitHub Pages

该项目是纯静态页面,可以使用 GitHub Pages。

1. 在 GitHub 创建空仓库

例如仓库名:

focus-board

创建时注意不要把真实密钥、私人文件或敏感数据上传到公开仓库。

2. 关联远程仓库

GitHub 会在仓库页面提供对应命令。通常类似:

git branch -M main
git remote add origin 你的仓库地址
git push -u origin main

请使用 GitHub 为你的仓库实际生成的地址,不要照抄占位文字。

3. 开启 GitHub Pages

进入仓库设置中的 Pages 部分,根据当前界面选择从分支部署:

  • 分支选择 main
  • 目录选择仓库根目录
  • 保存设置

GitHub 界面和部署选项可能调整,以仓库当前设置页面为准。部署完成后,Pages 页面会显示实际访问地址。

4. 更新网站

以后修改代码后执行:

git add .
git commit -m "更新网站"
git push

GitHub Pages 会根据仓库中的新版本重新部署。


一天的推荐时间安排

时间段 工作内容 结果
09:00—10:00 明确目标和功能边界 一页需求与验收标准
10:00—12:00 创建 HTML、CSS、JavaScript 可打开的基础页面
13:00—15:00 实现任务管理和本地存储 核心功能可用
15:00—16:00 实现专注计时器 计时功能可用
16:00—17:00 手机适配和可访问性检查 页面可正常操作
17:00—18:00 手动测试、修复错误 MVP 稳定
18:00—19:00 Git 提交与部署 获得公开访问地址

如果某个问题超过 30 分钟仍未解决,先降低需求,不要不断增加功能。


Cursor 提示词的正确写法

差的提示词

页面不好看,帮我高级一点。

问题在于“高级”没有客观标准,AI 可能大范围重写样式。

更好的提示词

只修改 styles.css,不修改 HTML 和 JavaScript。

目标:
1. 保留当前绿色主色
2. 卡片边框更轻
3. 桌面端内容最大宽度保持不变
4. 手机端左右间距不小于 10px
5. 不引用外部字体
6. 不增加动画

完成后列出修改的 CSS 选择器。

差的报错方式

不能用了,修一下。

更好的报错方式

问题现象:
点击“添加任务”后页面刷新,任务没有显示。

复现步骤:
1. 打开首页
2. 输入“测试任务”
3. 点击“添加任务”
4. 页面刷新

浏览器控制台错误:
请粘贴实际错误信息

请先分析最可能的原因,不要直接重写文件。
只修改与该问题相关的代码,并告诉我如何验证。

一个有效的 AI编程问题,最好包括:

  • 预期结果
  • 实际结果
  • 复现步骤
  • 控制台错误
  • 允许修改的文件
  • 禁止改变的部分

常见问题排查

1. 页面只有文字,没有样式

检查 index.html 中是否存在:

<link rel="stylesheet" href="styles.css">

同时确认文件名完全一致:

styles.css

在区分大小写的部署环境中,下面两个名字不是同一个文件:

Styles.css
styles.css

还可以打开浏览器开发者工具的 Network 面板,查看 styles.css 是否返回 404。


2. 点击按钮完全没有反应

检查是否正确引入 JavaScript:

<script src="app.js" defer></script>

打开浏览器开发者工具:

  • Windows/Linux 常用 F12
  • macOS 可从浏览器菜单打开开发者工具

查看 Console 是否出现红色错误。

常见原因包括:

  • app.js 文件名写错
  • JavaScript 中少了括号
  • HTML 中的 ID 与 JavaScript 选择器不一致
  • AI 修改 HTML 后没有同步修改 JS

把第一条红色错误及其行号发给 Cursor,比只说“按钮坏了”更有效。


3. 刷新后任务消失

先在浏览器控制台执行:

localStorage.getItem("focus-board-tasks-v1")

如果返回 null,说明没有成功保存。

可能原因:

  • 浏览器限制了站点存储
  • 使用了隐私模式
  • 手动清除了站点数据
  • 存储键名被 AI 修改
  • saveTasks() 没有被调用
  • JSON 序列化前代码已经报错

本项目的数据只保存在当前浏览器,不会自动同步到手机或另一台电脑。


4. 任务出现两次

常见原因是:

  • renderTasks() 没有先清空旧节点
  • 同一个事件监听器被绑定了两次
  • 同时存在内联事件和 addEventListener
  • app.js 被引入两次

本教程使用:

taskList.replaceChildren();

在每次渲染前清除旧任务节点。


5. 计时器速度越来越快

这通常意味着重复创建了多个 setInterval。

启动新计时器前,必须确认旧计时器是否已存在:

if (timerId) {
  clearInterval(timerId);
  timerId = null;
}

本教程的 toggleTimer() 会在已经运行时执行暂停,而不是创建第二个计时器。


6. 计时器切换到后台后不完全准确

浏览器可能限制后台标签页中的定时器频率。这是浏览器节能策略造成的,不一定是代码语法错误。

如果后续需要更高精度,可以改成记录目标结束时间,再使用当前时间计算剩余秒数。让 Cursor 修改时可以使用:

请只重构计时器的时间计算方式。

目标:
- 点击开始时记录目标结束时间
- 页面切换到后台再回来时,根据 Date.now() 重新计算剩余时间
- 保留开始、暂停、继续、重置行为
- 不修改任务功能和页面样式

先说明状态设计,再修改 app.js。

对于一天完成的 MVP,当前版本已经能够满足基本演示和个人使用。


7. 手机页面出现横向滚动

重点检查:

  • 是否有固定宽度,如 width: 900px
  • 输入框是否缺少 width: 100%
  • Grid 子元素是否缺少 min-width: 0
  • 长文本是否无法换行
  • 是否有过大的左右内边距

本教程使用了:

.task-text {
  min-width: 0;
  overflow-wrap: anywhere;
}

用于防止长任务文本撑破容器。


8. Cursor 一次修改了太多文件

立即停止继续生成,并告诉它:

撤销与当前任务无关的修改。

本次只允许修改 app.js。
不要修改 index.html 和 styles.css。
不要重命名变量,不要重构其他函数。

更安全的做法是每次修改前先提交 Git:

git add .
git commit -m "修改前的稳定版本"

9. Cursor 中找不到 Claude 3.5

可能原因包括:

  • Cursor 当前已调整模型列表
  • 账户权限不同
  • 模型名称或分组发生变化
  • 旧模型已停止提供
  • 临时服务状态异常

不要安装非官方破解插件,也不要把账号令牌交给第三方。

直接选择当前模型列表中可用的 Claude 编程模型,并继续使用本文提示词。决定成果质量的关键往往是:

  1. 需求是否清楚
  2. 修改范围是否受控
  3. 是否提供真实错误信息
  4. 是否进行手动测试
  5. 是否有 Git 回退点

而不是只看模型名称。


10. GitHub Pages 打开后显示 404

依次检查:

  • 仓库中是否存在 index.html
  • index.html 是否位于已选择的部署目录
  • Pages 是否选择了正确分支
  • 最新代码是否已 push
  • 文件名大小写是否正确
  • Pages 设置页是否显示部署错误

不要在代码里猜测部署地址。以 GitHub Pages 设置页面给出的实际地址为准。


零基础用户必须知道的安全边界

不要把密钥写进前端

下面这种代码不能用于真实项目:

const API_KEY = "你的秘密密钥";

浏览器中的 JavaScript 可以被访问者查看。凡是需要保密的 API 密钥,都不应直接放入公开前端代码。

不要上传真实隐私数据

本项目使用 localStorage,适合普通个人任务,不适合保存:

  • 密码
  • 身份证件
  • 银行信息
  • 医疗记录
  • 企业机密
  • 访问令牌

不要盲目执行终端命令

Cursor 给出终端命令后,先让它解释:

请逐段解释这条命令会读取、修改或删除哪些文件。
如果具有不可逆风险,请提供更安全的替代方式。

特别是带有递归删除、强制覆盖、远程脚本执行等行为的命令,必须确认后再运行。


下一步可以增加什么

完成并稳定运行 MVP 后,可以每次只增加一个功能。

推荐顺序:

  1. 任务编辑
  2. 自定义专注时长
  3. 深色模式
  4. 任务导出为 JSON
  5. 从 JSON 恢复任务
  6. 使用结束时间提高后台计时精度
  7. 增加自动化测试
  8. 再评估是否真的需要登录和云同步

例如,增加“导出任务”时,可以这样问 Cursor:

在现有项目中增加“导出任务为 JSON 文件”的功能。

限制:
1. 不使用第三方库
2. 不修改现有 localStorage 数据结构
3. 导出文件只包含任务数据
4. 在工具栏增加一个导出按钮
5. 对空任务列表给出提示
6. 不增加导入功能
7. 先给出修改计划,等我确认后再改代码

一次只做一个小功能,远比要求 AI “升级成完整 SaaS”可靠。


总结

用 Cursor + Claude 3.5 做 Web App,真正需要掌握的不是背诵 JavaScript,而是一套可控流程:

  1. 先缩小产品范围
  2. 先写验收标准,再生成代码
  3. 让 AI 每次只修改少量文件
  4. 使用浏览器控制台提供真实错误
  5. 亲自测试每一个按钮
  6. 用 Git 保存稳定版本
  7. 不把密钥和敏感数据放进前端
  8. 先完成可用 MVP,再考虑复杂功能

普通人一天内确实可以借助 AI编程完成一个小型 Web App,但前提不是“完全不思考”,而是把大问题拆成可以验证的小步骤。

Cursor 负责提高编辑和排错效率,Claude 负责生成方案、代码与解释;而你负责定义目标、确认修改、验证结果和决定产品边界。做到这一点,即使不懂代码,也能从一个空文件夹,走到一个真正可以访问和使用的 Web App。

⚡ 极客核心要点提炼 可供 AI 智能体与搜索引擎引用检索

本文主题:Cursor + Claude 3.5 编程实战:不懂代码的普通人如何一天写出一个 Web App

引用出处:https://isoziyuan.com/p/100100/(作者:Isoziyuan · 发布于爱搜资源网)