feat: native project flows on mobile

This commit is contained in:
kris
2026-03-30 11:45:06 +08:00
parent 31ebe0431e
commit 28863b208e
2 changed files with 103 additions and 16 deletions

View File

@@ -3138,11 +3138,53 @@ function openDashboardProjectSwitcher() {
setScreen("intake");
return;
}
const selectedProject = getSelectedProject();
const projects = safeArray(appState.dashboard?.projects);
const projectCards = projects.map((project) => {
const stats = getProjectStats(project.id);
const reviewCount = getProjectReviews(project.id).length;
const isActive = project.id === selectedProject?.id;
return `
<div class="task-item compact" style="${isActive ? "border-color:rgba(59, 130, 246, 0.32); background:linear-gradient(180deg, rgba(239, 246, 255, 0.98) 0%, rgba(255,255,255,0.98) 100%);" : ""}">
<h4>${escapeHtml(project.name || "未命名项目")}</h4>
<p>${escapeHtml(project.description || "还没有补项目说明,适合先切过去继续完善。")}</p>
<div class="task-meta">
<span class="tag ${isActive ? "green" : "blue"}">${escapeHtml(isActive ? "当前项目" : "可切换")}</span>
<span class="tag">最近任务 ${escapeHtml(formatNumber(stats.jobs.length))}</span>
<span class="tag">Agent ${escapeHtml(formatNumber(stats.assistants.length))}</span>
<span class="tag">复盘 ${escapeHtml(formatNumber(reviewCount))}</span>
</div>
</div>
`;
}).join("");
openActionModal({
title: "切换当前项目",
description: "首页上下文、今日动作和项目概览都会跟着当前项目一起切换。",
description: "首页上下文、今日动作和项目概览都会跟着当前项目一起切换。手机端会优先让你先扫一眼当前项目和最近任务。",
submitLabel: "切换项目",
fields: [
{
name: "projectSummary",
label: "项目速览",
type: "html",
html: `
<div class="task-item compact">
<h4>${escapeHtml(selectedProject?.name || "当前还没有项目")}</h4>
<p>${escapeHtml(selectedProject?.description || "切换项目后首页、OneLiner 和工作台会一起同步到对应上下文。")}</p>
<div class="task-meta">
<span class="tag green">当前项目</span>
<span class="tag">最近任务 ${escapeHtml(formatNumber(getProjectStats(selectedProject?.id || "").jobs.length))}</span>
<span class="tag">切换到这个项目后会同步总台、Agent 和生产中心</span>
</div>
</div>
<div class="list" style="margin-top:12px;">
<div class="task-item compact">
<h4>最近任务</h4>
<p>优先切到你现在真正要推进的项目,再从首页和主 Agent 继续往下走。</p>
</div>
${projectCards}
</div>
`
},
{ name: "projectId", label: "当前项目", type: "select", value: getSelectedProject()?.id || "", options }
],
onSubmit: async (payload) => {
@@ -3161,6 +3203,7 @@ function openDashboardProjectSwitcher() {
} finally {
setBusy(false, "");
}
rememberAction("当前项目已切换", `已切换到「${getSelectedProject()?.name || "所选项目"}你现在看到的首页、Agent 和任务都会跟随更新。`, "green");
renderAll();
}
});
@@ -7096,21 +7139,54 @@ async function createProject() {
openAuthModal();
return;
}
const name = window.prompt("输入项目名称");
if (!name) return;
const description = window.prompt("输入项目说明(可选)") || "";
setBusy(true, "正在创建项目...");
try {
await storyforgeFetch("/v2/projects", {
method: "POST",
body: { name, description }
});
await bootstrap();
} catch (error) {
alert("创建项目失败: " + error.message);
} finally {
setBusy(false, "");
}
const existingProjects = safeArray(appState.dashboard?.projects);
openActionModal({
title: "新建项目",
description: "先把项目建起来,首页动作、主 Agent 和记忆都会跟着这个项目继续往下走。",
submitLabel: "创建并进入项目",
fields: [
{
name: "projectGuide",
label: "创建提示",
type: "html",
html: `
<div class="task-item compact">
<h4>先建一个可继续推进的项目</h4>
<p>推荐直接按你现在的业务目标来命名,例如某个平台增长、某个账号矩阵或某个内容专题。</p>
<div class="task-meta">
<span class="tag blue">当前已有 ${escapeHtml(formatNumber(existingProjects.length))} 个项目</span>
<span class="tag">创建后会自动切过去</span>
<span class="tag">主 Agent 会沿用这个项目上下文</span>
</div>
</div>
`
},
{ name: "name", label: "项目名称", placeholder: "例如:创业 IP 增长实验室" },
{ name: "description", label: "项目说明", type: "textarea", rows: 4, placeholder: "写一句这个项目主要解决什么问题、接下来准备推进什么" }
],
onSubmit: async (values) => {
const name = String(values.name || "").trim();
if (!name) throw new Error("请填写项目名称");
setBusy(true, "正在创建项目...");
try {
const project = await storyforgeFetch("/v2/projects", {
method: "POST",
body: {
name,
description: String(values.description || "").trim()
}
});
appState.selectedProjectId = project.id || appState.selectedProjectId;
rememberAction("项目已创建", `已创建「${project.name || name}」,并切到这个项目继续推进。`, "green", project);
await bootstrap();
} catch (error) {
presentActionFailure(error, "创建项目失败");
throw error;
} finally {
setBusy(false, "");
}
}
});
}
function openPreferredModelAction() {

View File

@@ -103,6 +103,17 @@ test("mobile action sheets and oneliner runtime behave like bottom sheets", () =
assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.oneliner-composer\s*\{[\s\S]*position:\s*sticky/);
});
test("project creation and switching use in-app sheets instead of browser prompts", () => {
const createProject = extractBetween(APP, "async function createProject()", "function openPreferredModelAction()");
const projectSwitcher = extractBetween(APP, "function openDashboardProjectSwitcher()", "function openDashboardActionReasonAction(");
assert.match(createProject, /openActionModal\(\{/);
assert.doesNotMatch(createProject, /window\.prompt/);
assert.doesNotMatch(createProject, /alert\(/);
assert.match(projectSwitcher, /type: "html"/);
assert.match(projectSwitcher, /最近任务/);
assert.match(projectSwitcher, /切换到这个项目/);
});
test("mobile touch targets raise tappable buttons, tabs, and action tags closer to native sizes", () => {
assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.btn,\s*[\s\S]*\.tab,\s*[\s\S]*\.tag\.clickable-tag\s*\{[\s\S]*min-height:\s*44px/);
assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.btn,\s*[\s\S]*\.tab,\s*[\s\S]*\.tag\.clickable-tag\s*\{[\s\S]*display:\s*inline-flex/);