feat(web): 工作区侧栏 - 账号容量进度与签到加油站

头像上方展示已用/剩余/总容量,积分开启且未领取时固定显示品牌加油站卡片,当天领取后消失。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ddCat
2026-09-17 23:32:34 +08:00
parent 21e7dbb2b8
commit 67b53b45e4
9 changed files with 346 additions and 34 deletions

View File

@@ -0,0 +1,43 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { sidebarCheckinTitle, shouldShowSidebarCheckin } from "../src/lib/sidebar-checkin";
describe("sidebar checkin offer", () => {
test("shows only when credits and a checkin bonus are enabled and today is unclaimed", () => {
expect(shouldShowSidebarCheckin({ creditsEnabled: true, checkinBonusMicrocredits: 100_000_000, checkedInToday: false })).toBe(true);
expect(shouldShowSidebarCheckin({ creditsEnabled: false, checkinBonusMicrocredits: 100_000_000, checkedInToday: false })).toBe(false);
expect(shouldShowSidebarCheckin({ creditsEnabled: true, checkinBonusMicrocredits: 0, checkedInToday: false })).toBe(false);
expect(shouldShowSidebarCheckin({ creditsEnabled: true, checkinBonusMicrocredits: 100_000_000, checkedInToday: true })).toBe(false);
});
test("uses the configured brand name instead of Buddy", () => {
expect(sidebarCheckinTitle("影策")).toBe("影策加油站");
expect(sidebarCheckinTitle(" 本地工作室 ")).toBe("本地工作室加油站");
expect(sidebarCheckinTitle("")).toBe("影策加油站");
});
});
describe("workspace sidebar checkin card", () => {
test("sits above account storage and uses the claim card colors without a light-blue stage", () => {
const sidebar = readFileSync(resolve(import.meta.dir, "../src/components/layout/workspace-sidebar-nav.tsx"), "utf8");
const card = readFileSync(resolve(import.meta.dir, "../src/components/layout/workspace-sidebar-checkin.tsx"), "utf8");
const css = readFileSync(resolve(import.meta.dir, "../src/styles/globals.css"), "utf8");
const checkinIndex = sidebar.indexOf("<WorkspaceSidebarCheckin collapsed={collapsed} />");
const storageIndex = sidebar.indexOf("<WorkspaceSidebarStorageMeter collapsed={collapsed} />");
const start = css.indexOf(".app-user-workspace .app-workspace-sidebar-checkin {");
const end = css.indexOf(".app-user-workspace .app-workspace-sidebar-storage {", start);
const block = css.slice(start, end);
expect(checkinIndex).toBeGreaterThan(-1);
expect(storageIndex).toBeGreaterThan(checkinIndex);
expect(card).toContain("立即领取");
expect(card).toContain("今日可领");
expect(card).toContain("checkinCredits()");
expect(block).toContain("background: #fff;");
expect(block).toContain("background: #171717;");
expect(block).toContain("color: #12c8a0;");
expect(block).not.toMatch(/#(?:dbeafe|e8f1ff|eef4ff|e6f0ff|d6e8ff)/i);
});
});

View File

@@ -0,0 +1,66 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { accountStorageMeter } from "../src/lib/account-storage-usage";
const GB = 1024 ** 3;
describe("account storage meter", () => {
test("exposes remaining capacity and total capacity", () => {
const meter = accountStorageMeter({ usedBytes: 2 * GB, totalBytes: 10 * GB });
expect(meter.usedLabel).toBe("2.0 GB");
expect(meter.remainingLabel).toBe("8.0 GB");
expect(meter.totalLabel).toBe("10 GB");
expect(meter.percent).toBe(20);
expect(meter.tone).toBe("ok");
expect(meter.full).toBe(false);
});
test("warns when usage reaches seventy percent", () => {
const meter = accountStorageMeter({ usedBytes: 7 * GB, totalBytes: 10 * GB });
expect(meter.percent).toBe(70);
expect(meter.tone).toBe("warn");
expect(meter.remainingLabel).toBe("3.0 GB");
});
test("marks full accounts as critical with zero remaining", () => {
const meter = accountStorageMeter({ usedBytes: 10 * GB, totalBytes: 10 * GB });
expect(meter.full).toBe(true);
expect(meter.tone).toBe("critical");
expect(meter.remainingBytes).toBe(0);
expect(meter.remainingLabel).toBe("0 B");
expect(meter.percent).toBe(100);
});
test("treats missing usage as empty instead of overflowing", () => {
const meter = accountStorageMeter();
expect(meter.percent).toBe(0);
expect(meter.remainingLabel).toBe("0 B");
expect(meter.totalLabel).toBe("0 B");
expect(meter.tone).toBe("ok");
expect(meter.full).toBe(false);
});
});
describe("workspace sidebar storage meter", () => {
test("sits above the signed-in profile and shows used, remaining and total", () => {
const sidebar = readFileSync(resolve(import.meta.dir, "../src/components/layout/workspace-sidebar-nav.tsx"), "utf8");
const meter = readFileSync(resolve(import.meta.dir, "../src/components/layout/workspace-sidebar-storage-meter.tsx"), "utf8");
const css = readFileSync(resolve(import.meta.dir, "../src/styles/globals.css"), "utf8");
expect(sidebar).toContain("<WorkspaceSidebarStorageMeter collapsed={collapsed} />");
expect(sidebar.indexOf("WorkspaceSidebarStorageMeter")).toBeLessThan(sidebar.indexOf("app-workspace-sidebar-profile-row"));
expect(meter).toContain("已用 ${meter.usedLabel}");
expect(meter).toContain("剩余 ${meter.remainingLabel}");
expect(meter).toContain("共 ${meter.totalLabel}");
expect(meter).toContain('to="/assets"');
expect(css).toMatch(/\.app-workspace-sidebar-storage-used\s*\{[^}]*font-variant-numeric:\s*tabular-nums/s);
expect(css).toContain(".app-workspace-sidebar-storage.is-warn");
expect(css).toContain(".app-workspace-sidebar-storage.is-critical");
});
});