// 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: () => "

Dashboard

placeholder -- real content is PH3 (v_dashboard, U-025)

" }, "#/tasks": { render: () => "

Tasks

placeholder -- real content is PH3 (v_task_board, U-025)

" }, "#/settings": { render: () => "

Settings

placeholder -- real content is PH3 (U-030)

" }, "#/setup": { render: renderProjectSetup, mount: mountProjectSetup }, }; function render() { const hash = window.location.hash || "#/"; const route = routes[hash] || { render: () => "

Not found

" + hash + "

" }; 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 `
← 返回仪表盘

创建 / 切换项目

选择一个本地文件夹作为目标项目的代码空间
点击"选择文件夹"会由后端代为弹出系统原生对话框,不支持手动输入路径(除非返回 DIALOG_UNAVAILABLE)
`; } 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"; }); }