app.js 5.07 KB
// U-008: hash-based client-side router (no History API / server catch-all
// needed). U-022 adds the first real page, #/setup (v_project_setup, 02-V
// §2.1) -- everything else is still a PH3 placeholder.

const routes = {
  "#/": { render: () => "<h1>Dashboard</h1><p>placeholder -- real content is PH3 (v_dashboard, U-025)</p>" },
  "#/tasks": { render: () => "<h1>Tasks</h1><p>placeholder -- real content is PH3 (v_task_board, U-025)</p>" },
  "#/settings": { render: () => "<h1>Settings</h1><p>placeholder -- real content is PH3 (U-030)</p>" },
  "#/setup": { render: renderProjectSetup, mount: mountProjectSetup },
};

function render() {
  const hash = window.location.hash || "#/";
  const route = routes[hash] || { render: () => "<h1>Not found</h1><p>" + hash + "</p>" };
  document.getElementById("app").innerHTML = route.render();
  if (route.mount) route.mount();

  for (const a of document.querySelectorAll("#nav a")) {
    a.classList.toggle("active", a.getAttribute("href") === hash);
  }
}

window.addEventListener("hashchange", render);
window.addEventListener("DOMContentLoaded", render);

// ---- v_project_setup (02-V §2.1): create/switch project ----

function renderProjectSetup() {
  return `
    <div class="top-back"><a href="#/">← 返回仪表盘</a></div>
    <div class="page">
      <h1>创建 / 切换项目</h1>
      <div class="page-sub">选择一个本地文件夹作为目标项目的代码空间</div>
      <div class="panel">
        <div id="setup_error" class="error-banner" style="display:none;"></div>
        <div class="field">
          <label>目标文件夹(folder_path)</label>
          <input type="text" id="folder_path" readonly value="">
          <div class="hint" id="folder_path_hint">点击"选择文件夹"会由后端代为弹出系统原生对话框,不支持手动输入路径(除非返回 DIALOG_UNAVAILABLE)</div>
        </div>
        <button class="btn" id="btn_pick_folder" type="button">📁 选择文件夹</button>
        <div class="field" style="margin-top:20px;">
          <label>项目名称(project_name,非必填)</label>
          <input type="text" id="project_name" placeholder="留空则取文件夹名">
        </div>
        <div class="btn-row">
          <button class="btn btn-primary" id="btn_create" type="button">创建项目</button>
        </div>
        <div class="confirm-banner" id="confirm_banner" style="display:none;">
          该文件夹还不是 Git 仓库,是否代为初始化(<code>git init</code>)?确认前不写入任何文件。
          <div class="btn-row" style="margin-top:10px;">
            <button class="btn" id="btn_cancel_init" type="button">取消</button>
            <button class="btn btn-primary" id="btn_confirm_init" type="button">确认初始化并创建项目</button>
          </div>
        </div>
      </div>
    </div>
  `;
}

function mountProjectSetup() {
  const folderInput = document.getElementById("folder_path");
  const folderHint = document.getElementById("folder_path_hint");
  const nameInput = document.getElementById("project_name");
  const errorBox = document.getElementById("setup_error");
  const confirmBanner = document.getElementById("confirm_banner");

  function showError(message) {
    errorBox.textContent = message;
    errorBox.style.display = "block";
  }
  function clearError() {
    errorBox.style.display = "none";
  }

  document.getElementById("btn_pick_folder").addEventListener("click", async () => {
    clearError();
    const resp = await fetch("/api/system/pick-folder", { method: "POST", body: "{}" });
    const body = await resp.json();
    if (resp.status === 422 && body.error === "DIALOG_UNAVAILABLE") {
      folderInput.removeAttribute("readonly");
      folderHint.textContent = "当前环境不支持弹窗选择,请手动输入路径";
      return;
    }
    if (body.selected_path) {
      folderInput.value = body.selected_path;
    }
    // user canceled the native dialog -> folder_path stays as it was (02-V §2.1)
  });

  async function createProject(initGitIfMissing) {
    clearError();
    confirmBanner.style.display = "none";
    const folderPath = folderInput.value.trim();
    if (!folderPath) {
      showError("请先选择目标文件夹");
      return;
    }
    const resp = await fetch("/api/projects", {
      method: "POST",
      body: JSON.stringify({
        target_folder_path: folderPath,
        name: nameInput.value.trim() || undefined,
        init_git_if_missing: initGitIfMissing,
      }),
    });
    const body = await resp.json();
    if (resp.status === 200) {
      window.location.hash = "#/";
      return;
    }
    if (body.error === "GIT_INIT_DECLINED") {
      confirmBanner.style.display = "block";
      return;
    }
    showError(body.error + (body.detail ? ":" + body.detail : ""));
  }

  document.getElementById("btn_create").addEventListener("click", () => createProject(false));
  document.getElementById("btn_confirm_init").addEventListener("click", () => createProject(true));
  document.getElementById("btn_cancel_init").addEventListener("click", () => {
    confirmBanner.style.display = "none";
  });
}