feat: finish mobile task cards for thin pages

This commit is contained in:
kris
2026-03-30 12:46:04 +08:00
parent 1d2bbdf201
commit 794de0133e
3 changed files with 94 additions and 5 deletions

View File

@@ -618,6 +618,17 @@ function hasSessionBackendMismatch(expectedBackendUrl = DEFAULT_BACKEND_URL) {
return Boolean(expected && current && expected !== current);
}
function formatBackendDisplayLabel(value = DEFAULT_BACKEND_URL) {
const normalized = normalizeBackendUrlValue(value || DEFAULT_BACKEND_URL);
if (!normalized) return "未配置后端";
try {
const parsed = new URL(normalized);
return parsed.host || normalized;
} catch {
return normalized;
}
}
function compareDateDesc(leftValue, rightValue) {
return new Date(rightValue || 0).getTime() - new Date(leftValue || 0).getTime();
}
@@ -6930,11 +6941,45 @@ function renderCreditsScreen() {
const usage = appState.tenantUsage || quota?.usage || {};
const categories = usage?.categories || {};
const estimatedVideoUsage = (categories.ai_video?.quantity || 0) + (categories.real_cut?.quantity || 0);
const budgetAmount = (quota?.monthly_budget_cents || usage?.total_cost_cents || 0) / 100;
const usedAmount = (usage?.total_cost_cents || 0) / 100;
const quotaProtectionLabel = quota?.enabled === false ? "额度保护关闭" : "额度保护开启";
const riskLabel = quota?.storage_over_limit ? "存储超限" : "当前风险可控";
return screenShell(
"额度",
"在接真实计费前,先按任务量给出运营看板。",
`${button("刷新", "refresh-data")}`,
`
<div class="mobile-only mobile-flow-focus-card">
<div class="mobile-flow-focus-head">
<strong>当前额度任务</strong>
<span class="tag blue">${escapeHtml(quotaProtectionLabel)}</span>
</div>
<p>${escapeHtml(
quota
? `先确认预算 ${formatNumber(budgetAmount)} 元和已用 ${formatNumber(usedAmount)} 元,再决定是继续放量还是先收紧高成本动作。`
: "先看预算和高成本动作的预估消耗,再决定是否要继续做视频或剪辑任务。"
)}</p>
<div class="task-meta">
${actionTag("刷新额度", "refresh-data")}
${actionTag("去生产中心", "goto-production")}
${actionTag("交给主 Agent", "handoff-to-main-agent", buildMainAgentHandoffAttrs({
sourceScreen: "credits",
sourceActionKey: "credits-main-agent-handoff",
intentKey: "custom",
title: "评估当前额度和预算风险",
goal: "评估当前额度和预算风险",
summary: "让主 Agent 结合当前预算、已用额度和高成本动作,给出下一步建议。",
planSteps: ["读取当前额度看板", "判断预算与高成本动作风险", "给出下一步控制建议"]
}))}
</div>
</div>
<div class="mobile-only compact-summary-row" style="margin-bottom:14px;">
<span class="tag blue">预算 ${escapeHtml(formatNumber(budgetAmount))} 元</span>
<span class="tag ${quota?.storage_over_limit ? "orange" : "green"}">${escapeHtml(riskLabel)}</span>
<span class="tag">${escapeHtml(formatNumber(categories.copy?.quantity || jobs.filter((item) => item.line_type === "analysis").length))} 条文案</span>
<span class="tag">${escapeHtml(formatNumber(estimatedVideoUsage || jobs.filter((item) => item.line_type === "ai_video" || item.line_type === "real_cut").length))} 次视频</span>
</div>
<div class="layout-grid grid-3">
<div class="stat-card"><small>文案消耗预估</small><strong>${escapeHtml(formatNumber(categories.copy?.quantity || jobs.filter((item) => item.line_type === "analysis").length))}</strong><div class="stat-foot"><span>分析 / 生成链路</span><span class="positive">按任务量估算</span></div></div>
<div class="stat-card"><small>本周期预算</small><strong>${escapeHtml(formatNumber((quota?.monthly_budget_cents || usage?.total_cost_cents || 0) / 100))}</strong><div class="stat-foot"><span>元</span><span class="warn">已用 ${escapeHtml(formatNumber((usage?.total_cost_cents || 0) / 100))} 元</span></div></div>
@@ -6992,17 +7037,49 @@ function renderSettingsScreen() {
{ value: "display", label: "界面与帮助" }
];
const activeTab = getActiveDetailTab("settingsDetailTab", tabs);
const sessionConnected = Boolean(session);
const settingsHandoffAttrs = buildMainAgentHandoffAttrs({
sourceScreen: "settings",
sourceActionKey: "settings-main-agent-handoff",
intentKey: "custom",
title: "检查当前设置和工作区连接",
goal: "检查当前设置和工作区连接",
summary: "让主 Agent 读取当前连接状态、工作区和常用入口,再给出下一步建议。",
planSteps: ["读取当前连接与项目上下文", "确认当前工作区和入口状态", "生成下一步建议"]
});
return screenShell(
"设置",
"这里不放系统治理内容,只处理当前用户需要理解的连接、界面和帮助信息。",
`${button("连接状态", "open-auth")} ${isSuperAdmin() ? button("管理员配置台", "goto-admin-workbench", "primary") : button("刷新", "refresh-data", "primary")}`,
`
<div class="hero-card">
<div class="hero-card mobile-secondary-card">
<h3>设置与帮助</h3>
<p>把连接状态、当前工作区和使用说明放在一起,避免和管理员控制面混在同一页。</p>
</div>
<div class="panel pad" style="margin-top:18px;">
<div class="panel-head"><div><h3>当前设置</h3><div class="panel-subtitle">先看你现在接到了哪里,再决定是否要调整。</div></div></div>
<div class="mobile-only mobile-flow-focus-card">
<div class="mobile-flow-focus-head">
<strong>当前设置任务</strong>
<span class="tag ${sessionConnected ? "green" : "red"}">${escapeHtml(sessionConnected ? "已自动连接" : "等待连接")}</span>
</div>
<p>${escapeHtml(
sessionConnected
? `当前已经连到 ${session?.account?.display_name || session?.account?.username || "当前工作区"},先确认工作区和常用入口,再决定是否切项目或回到业务页。`
: "先确认当前站点是否已自动连接到工作区,再决定是重试连接还是回到业务页。"
)}</p>
<div class="task-meta">
${actionTag("打开连接状态", "open-auth")}
${project ? actionTag("去我的项目", "goto-intake") : ""}
${actionTag("交给主 Agent", "handoff-to-main-agent", settingsHandoffAttrs)}
</div>
</div>
<div class="mobile-only compact-summary-row" style="margin-bottom:14px;">
<span class="tag ${sessionConnected ? "green" : "red"}">${escapeHtml(sessionConnected ? "已自动连接" : "等待连接")}</span>
<span class="tag blue">${escapeHtml(project?.name || "未选项目")}</span>
<span class="tag">${escapeHtml(formatBackendDisplayLabel(session?.backendUrl || DEFAULT_BACKEND_URL))}</span>
<span class="tag">${escapeHtml(activeTab === "workspace" ? "连接与工作区" : "界面与帮助")}</span>
</div>
${renderDetailTabs("settingsDetailTab", tabs)}
${activeTab === "workspace" ? `
<div class="layout-grid grid-main">

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#eef4fb" />
<title>StoryForge Web V4 Prototype</title>
<title>StoryForge Workbench</title>
<link rel="icon" href="./assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="./assets/styles.css" />
</head>
@@ -1687,7 +1687,7 @@
</div>
</div>
</div>
<div class="footer-note">StoryForge Web V4 prototype · UI only · no live data binding</div>
<div class="footer-note">StoryForge 工作台 · 项目总台与执行入口会按当前工作区实时展开</div>
</section>
<section class="screen" data-screen="review">
@@ -1807,7 +1807,7 @@
</div>
</div>
</div>
<div class="footer-note">StoryForge Web V4 prototype · review loop separated from production</div>
<div class="footer-note">StoryForge 工作台 · 复盘链路与生产链路分层展示,避免同页堆叠</div>
</section>
<section class="screen" data-screen="credits">
@@ -1941,7 +1941,7 @@
</div>
</div>
</div>
<div class="footer-note">StoryForge Web V4 prototype · credit system expressed as user-facing quota pools</div>
<div class="footer-note">StoryForge 工作台 · 额度以用户可理解的预算与动作池方式表达</div>
</section>
<section class="screen" data-screen="strategy"></section>

View File

@@ -287,6 +287,8 @@ test("remaining mobile workbench screens expose focus cards and compact summarie
const automation = extractBetween(APP, "function renderAutomationScreen()", "function renderOwnedScreen()");
const owned = extractBetween(APP, "function renderOwnedScreen()", "function renderPlaybookScreen()");
const review = extractBetween(APP, "function renderReviewScreen()", "function renderStrategyScreen()");
const credits = extractBetween(APP, "function renderCreditsScreen()", "function renderSettingsScreen()");
const settings = extractBetween(APP, "function renderSettingsScreen()", "function renderTopbar()");
assert.match(projects, /mobile-only mobile-flow-focus-card/);
assert.match(projects, /当前项目任务/);
@@ -312,6 +314,16 @@ test("remaining mobile workbench screens expose focus cards and compact summarie
assert.match(review, /当前复盘任务/);
assert.match(review, /mobile-only compact-summary-row/);
assert.match(review, /已保存/);
assert.match(credits, /mobile-only mobile-flow-focus-card/);
assert.match(credits, /当前额度任务/);
assert.match(credits, /mobile-only compact-summary-row/);
assert.match(credits, /预算/);
assert.match(settings, /mobile-only mobile-flow-focus-card/);
assert.match(settings, /当前设置任务/);
assert.match(settings, /mobile-only compact-summary-row/);
assert.match(settings, /已自动连接/);
});
test("projects screen uses an adaptive project grid instead of a fixed three-column squeeze", () => {