app.js
5.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// 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";
});
}