From 648c30983fa1e79547d8f87454a2229671165940 Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Fri, 18 Sep 2026 10:30:08 +0800 Subject: [PATCH] =?UTF-8?q?style(web):=20=E5=B7=A5=E4=BD=9C=E5=8F=B0?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=20-=20=E4=BE=A7=E6=A0=8F=E5=AE=B9=E9=87=8F?= =?UTF-8?q?=E5=8A=A0=E7=A1=AC=E7=9B=98=E5=9B=BE=E6=A0=87=EF=BC=8C=E7=A7=AF?= =?UTF-8?q?=E5=88=86=E5=85=A5=E5=8F=A3=E6=94=B9=E4=B8=BA=E7=A4=BC=E7=9B=92?= =?UTF-8?q?=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- docs/content/docs/overview/features.mdx | 2 +- docs/content/docs/progress/pending-test.mdx | 4 +- .../layout/workspace-credit-gift-mark.tsx | 44 +++++ .../workspace-sidebar-storage-meter.tsx | 59 +++++-- .../components/layout/workspace-top-bar.tsx | 5 +- .../pages/canvas/canvas-project-top-bar.tsx | 5 +- web/src/styles/globals.css | 159 +++++++++++++++--- web/src/styles/workspace-product.css | 2 +- web/test/workspace-credit-gift.test.ts | 21 +++ web/test/workspace-sidebar-storage.test.ts | 3 + 10 files changed, 254 insertions(+), 50 deletions(-) create mode 100644 web/src/components/layout/workspace-credit-gift-mark.tsx create mode 100644 web/test/workspace-credit-gift.test.ts diff --git a/docs/content/docs/overview/features.mdx b/docs/content/docs/overview/features.mdx index f0690077a..91c31956b 100644 --- a/docs/content/docs/overview/features.mdx +++ b/docs/content/docs/overview/features.mdx @@ -26,7 +26,7 @@ description: 当前项目已实现的核心工作台能力与使用入口 ## 工作区账号容量 -登录后,首页侧栏左下角用户头像上方显示账号文件容量进度:已用容量、剩余容量和总容量,细进度条表示已用比例。用量达到 70% 变为警告色,达到 90% 或已满变为危险色。点击进入素材库;资产页页头仍展示同一用量。收起侧栏时保留细进度条,悬停可看到已用、剩余与总容量。统计包含素材文件和 Agent 会话附件。 +登录后,首页侧栏左下角用户头像上方显示账号文件容量进度:左侧硬盘图标,右侧已用容量与总容量,进度条旁显示剩余容量。用量达到 70% 变为警告色,达到 90% 或已满变为危险色。点击进入素材库;资产页页头仍展示同一用量。收起侧栏时保留图标和细进度条,悬停可看到已用、剩余与总容量。统计包含素材文件和 Agent 会话附件。 ### 相关实现 diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index de9dfb233..90705a228 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -68,8 +68,8 @@ description: 当前项目已实现功能的手工验证清单与未确认边界 登录后首页侧栏左下角用户头像上方展示账号文件容量。待登录工作台后: -- [ ] 展开侧栏时能看到已用容量、剩余容量、总容量和进度条;数字为真实配额,不是占位。 -- [ ] 收起侧栏后头像上方仍有细进度条,悬停能看到剩余/总容量。 +- [ ] 展开侧栏时能看到硬盘图标、已用容量、剩余容量、总容量和进度条;数字为真实配额,不是占位。 +- [ ] 收起侧栏后头像上方仍有硬盘图标和细进度条,悬停能看到剩余/总容量。 - [ ] 用量到 70% 变警告色,到 90% 或已满变危险色;点进度条进入资产页,两边数字一致。 - [ ] 未登录不显示该进度;接口失败时可重试。 diff --git a/web/src/components/layout/workspace-credit-gift-mark.tsx b/web/src/components/layout/workspace-credit-gift-mark.tsx new file mode 100644 index 000000000..e5794bbe1 --- /dev/null +++ b/web/src/components/layout/workspace-credit-gift-mark.tsx @@ -0,0 +1,44 @@ +import { useId } from "react"; + +import { cn } from "@/lib/utils"; + +export function WorkspaceCreditGiftMark({ className }: { className?: string }) { + const uid = useId().replace(/:/g, ""); + const box = `${uid}-box`; + const lid = `${uid}-lid`; + const ribbon = `${uid}-ribbon`; + + return ( + + ); +} diff --git a/web/src/components/layout/workspace-sidebar-storage-meter.tsx b/web/src/components/layout/workspace-sidebar-storage-meter.tsx index a32a2bf9d..1f1e59bad 100644 --- a/web/src/components/layout/workspace-sidebar-storage-meter.tsx +++ b/web/src/components/layout/workspace-sidebar-storage-meter.tsx @@ -1,3 +1,4 @@ +import { HardDrive } from "lucide-react"; import { Link } from "react-router"; import { useAccountFileStorageUsage } from "@/hooks/use-account-file-storage-usage"; @@ -5,6 +6,14 @@ import { accountStorageMeter } from "@/lib/account-storage-usage"; import { cn } from "@/lib/utils"; import { preloadWorkspaceRoute } from "@/lib/workspace-route-modules"; +function StorageGlyph() { + return ( + + ); +} + export function WorkspaceSidebarStorageMeter({ collapsed }: { collapsed: boolean }) { const query = useAccountFileStorageUsage(); const meter = accountStorageMeter(query.data); @@ -13,13 +22,30 @@ export function WorkspaceSidebarStorageMeter({ collapsed }: { collapsed: boolean const totalText = query.data ? `共 ${meter.totalLabel}` : ""; const summary = query.data ? `${usedText},${remainingText},${totalText}` : usedText; + const track = ( + + + + ); + if (query.isError && !query.data) { return (
{collapsed ? ( - ) : ( <> + 容量暂不可用 : null} diff --git a/web/src/pages/canvas/canvas-project-top-bar.tsx b/web/src/pages/canvas/canvas-project-top-bar.tsx index 53efec4e1..9245178ed 100644 --- a/web/src/pages/canvas/canvas-project-top-bar.tsx +++ b/web/src/pages/canvas/canvas-project-top-bar.tsx @@ -1,8 +1,9 @@ import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react"; import { Link } from "react-router"; -import { Clapperboard, CloudDownload, CloudUpload, Coins, CopyPlus, Focus, FolderKanban, Gauge, Home, LayoutGrid, LoaderCircle, Menu, Pencil, Plus, Redo2, Save, Search, Share2, Trash2, Undo2, Upload } from "lucide-react"; +import { Clapperboard, CloudDownload, CloudUpload, CopyPlus, Focus, FolderKanban, Gauge, Home, LayoutGrid, LoaderCircle, Menu, Pencil, Plus, Redo2, Save, Search, Share2, Trash2, Undo2, Upload } from "lucide-react"; import { Button, Dropdown, Tooltip } from "antd"; +import { WorkspaceCreditGiftMark } from "@/components/layout/workspace-credit-gift-mark"; import { useWalletBalance } from "@/hooks/use-wallet-balance"; import { openWorkspaceWallet } from "@/lib/workspace-wallet"; import { canvasDockStyle } from "@/lib/canvas/canvas-aceternity-style"; @@ -241,7 +242,7 @@ export function CanvasTopBar({ aria-label="打开积分中心" onClick={() => openWorkspaceWallet()} > - {refreshing && availableMicrocredits === null ? : } + {refreshing && availableMicrocredits === null ? : } {availableMicrocredits === null ? "--" : (availableMicrocredits / 1_000_000).toLocaleString("zh-CN", { maximumFractionDigits: 3 })} diff --git a/web/src/styles/globals.css b/web/src/styles/globals.css index 7e5731989..ac24175ff 100644 --- a/web/src/styles/globals.css +++ b/web/src/styles/globals.css @@ -1863,6 +1863,10 @@ background: var(--dock-command-hover) !important; } + .canvas-topbar-action:hover:not(:disabled) .app-workspace-credit-gift { + transform: translateY(-1px) rotate(-8deg); + } + .canvas-topbar-project-cluster { width: max-content; max-width: min(360px, calc(50vw - 88px)); @@ -16099,11 +16103,12 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 .app-user-workspace .app-workspace-topbar-credit-pill { display: inline-flex; flex-shrink: 0; + overflow: visible; white-space: nowrap; height: 36px; align-items: center; - gap: 7px; - padding: 0 12px; + gap: 8px; + padding: 0 12px 0 8px; border: 1px solid rgba(0, 0, 0, .075); border-radius: 999px; background: #fff; @@ -16113,15 +16118,46 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 } .app-user-workspace .app-workspace-topbar-credit-pill:hover { - border-color: rgba(91, 78, 224, .26); - background: #fbfaff; + border-color: color-mix(in srgb, #ff8a28 34%, transparent); + background: #fffaf4; transform: translateY(-1px); } -.app-user-workspace .app-workspace-topbar-credit-pill svg { - width: 15px; - height: 15px; - color: #6a58e8; +.app-user-workspace .app-workspace-credit-gift { + display: grid; + width: 24px; + height: 24px; + flex: none; + place-items: center; + filter: drop-shadow(0 1px 1px rgba(92, 42, 0, .22)); + transform-origin: 50% 70%; + transition: transform .18s ease; +} + +.app-user-workspace .app-workspace-credit-gift svg { + width: 24px; + height: 24px; + overflow: visible; + color: unset; +} + +.app-user-workspace .app-workspace-credit-gift.is-compact, +.app-user-workspace .app-workspace-credit-gift.is-compact svg { + width: 18px; + height: 18px; +} + +.app-user-workspace .app-workspace-topbar-credit-pill:hover .app-workspace-credit-gift { + transform: translateY(-1px) rotate(-8deg); +} + +@media (prefers-reduced-motion: reduce) { + .app-user-workspace .app-workspace-topbar-credit-pill, + .app-user-workspace .app-workspace-credit-gift, + .app-user-workspace .app-workspace-topbar-credit-pill:hover .app-workspace-credit-gift { + transform: none; + transition: none; + } } .app-user-workspace .app-workspace-topbar-credit-pill strong { @@ -16268,9 +16304,9 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 .app-user-workspace .app-workspace-sidebar-storage { display: flex; min-width: 0; - flex-direction: column; - gap: 6px; - padding: 8px 8px 6px; + align-items: center; + gap: 8px; + padding: 8px; border-radius: 12px; color: inherit; text-decoration: none; @@ -16282,11 +16318,40 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 background: var(--user-surface-hover, rgba(72, 72, 73, .055)); } +.app-user-workspace .app-workspace-sidebar-storage:hover .app-workspace-sidebar-storage-icon, +.app-user-workspace .app-workspace-sidebar-storage:focus-visible .app-workspace-sidebar-storage-icon { + color: var(--user-ink, var(--foreground)); +} + .app-user-workspace .app-workspace-sidebar-storage:focus-visible { outline: 2px solid var(--control-focus-ring); outline-offset: 2px; } +.app-user-workspace .app-workspace-sidebar-storage-icon { + display: grid; + width: 32px; + height: 32px; + flex: none; + place-items: center; + border-radius: 9px; + background: color-mix(in srgb, var(--user-ink, var(--foreground)) 8%, transparent); + color: color-mix(in srgb, var(--user-ink, var(--foreground)) 78%, transparent); +} + +.app-user-workspace .app-workspace-sidebar-storage-icon svg { + width: 16px; + height: 16px; +} + +.app-user-workspace .app-workspace-sidebar-storage-body { + display: flex; + min-width: 0; + flex: 1 1 auto; + flex-direction: column; + gap: 6px; +} + .app-user-workspace .app-workspace-sidebar-storage-copy { display: flex; min-width: 0; @@ -16294,6 +16359,13 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 gap: 2px; } +.app-user-workspace .app-workspace-sidebar-storage-foot { + display: flex; + min-width: 0; + align-items: center; + gap: 8px; +} + .app-user-workspace .app-workspace-sidebar-storage-meta { display: flex; min-width: 0; @@ -16326,12 +16398,14 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 } .app-user-workspace .app-workspace-sidebar-storage-remain { - min-width: 0; + line-height: 1; } .app-user-workspace .app-workspace-sidebar-storage-track { display: block; - height: 5px; + flex: 1 1 auto; + min-width: 0; + height: 4px; overflow: hidden; border-radius: 999px; background: color-mix(in srgb, var(--user-ink, var(--foreground)) 10%, transparent); @@ -16353,18 +16427,28 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 background: color-mix(in srgb, var(--user-ink, var(--foreground)) 8%, transparent); } -.app-user-workspace .app-workspace-sidebar-storage.is-warn .app-workspace-sidebar-storage-used { +.app-user-workspace .app-workspace-sidebar-storage.is-warn .app-workspace-sidebar-storage-used, +.app-user-workspace .app-workspace-sidebar-storage.is-warn .app-workspace-sidebar-storage-icon { color: var(--status-warning); } +.app-user-workspace .app-workspace-sidebar-storage.is-warn .app-workspace-sidebar-storage-icon { + background: color-mix(in srgb, var(--status-warning) 14%, transparent); +} + .app-user-workspace .app-workspace-sidebar-storage.is-warn .app-workspace-sidebar-storage-track > span { background: var(--status-warning); } -.app-user-workspace .app-workspace-sidebar-storage.is-critical .app-workspace-sidebar-storage-used { +.app-user-workspace .app-workspace-sidebar-storage.is-critical .app-workspace-sidebar-storage-used, +.app-user-workspace .app-workspace-sidebar-storage.is-critical .app-workspace-sidebar-storage-icon { color: var(--status-error); } +.app-user-workspace .app-workspace-sidebar-storage.is-critical .app-workspace-sidebar-storage-icon { + background: color-mix(in srgb, var(--status-error) 14%, transparent); +} + .app-user-workspace .app-workspace-sidebar-storage.is-critical .app-workspace-sidebar-storage-track > span { background: var(--progress-bar-fill-error); } @@ -16379,6 +16463,11 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 font-size: var(--fs-tiny); } +.app-user-workspace .app-workspace-sidebar-storage.is-error .app-workspace-sidebar-storage-icon { + color: color-mix(in srgb, var(--status-error) 72%, transparent); + background: color-mix(in srgb, var(--status-error) 12%, transparent); +} + .app-user-workspace .app-workspace-sidebar-storage.is-error button { color: var(--user-ink, var(--foreground)); text-decoration: underline; @@ -16391,26 +16480,43 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 } .app-user-workspace .app-workspace-sidebar-storage.is-collapsed { - width: 36px; - padding: 8px 0 6px; + width: 40px; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 6px 0; margin-inline: auto; } +.app-user-workspace .app-workspace-sidebar-storage.is-collapsed .app-workspace-sidebar-storage-track { + flex: none; + width: 32px; +} + .app-user-workspace .app-workspace-sidebar-storage.is-collapsed.is-error { - width: 36px; + width: 40px; min-height: 0; - padding: 2px 0 4px; + padding: 2px 0; } .app-user-workspace .app-workspace-sidebar-storage.is-collapsed.is-error button { - display: block; - width: 36px; - height: 5px; - border-radius: 999px; - background: color-mix(in srgb, var(--status-error) 48%, transparent); + display: grid; + width: 32px; + height: 32px; + place-items: center; + border-radius: 9px; + background: color-mix(in srgb, var(--status-error) 14%, transparent); + color: var(--status-error); text-decoration: none; } +.app-user-workspace .app-workspace-sidebar-storage.is-collapsed.is-error .app-workspace-sidebar-storage-icon { + width: 32px; + height: 32px; + background: transparent; + color: inherit; +} + .app-user-workspace .app-workspace-sidebar-profile-row { display: flex; min-width: 0; @@ -18604,6 +18710,11 @@ html:not(.dark) .creation-home .creation-user-message-copy-wrap { box-shadow: 0 } .app-user-workspace .app-workspace-topbar-breadcrumb { color: var(--user-ink-muted) !important; } .app-user-workspace .app-workspace-topbar-credit-pill { border-color: var(--user-border) !important; background: var(--user-surface) !important; color: var(--user-ink-muted) !important; } +.app-user-workspace .app-workspace-topbar-credit-pill:hover, +.app-user-workspace .app-workspace-topbar-credit-pill:focus-visible { + border-color: color-mix(in srgb, #ff8a28 36%, var(--user-border)) !important; + background: color-mix(in srgb, #ff8a28 10%, var(--user-surface)) !important; +} .app-user-workspace .app-workspace-topbar-credit-pill strong { color: var(--user-ink) !important; } /* One scroll owner on the creation route. */ diff --git a/web/src/styles/workspace-product.css b/web/src/styles/workspace-product.css index b3be23f03..b2758925c 100644 --- a/web/src/styles/workspace-product.css +++ b/web/src/styles/workspace-product.css @@ -469,7 +469,7 @@ .app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-nav-link .app-workspace-nav-main > svg { width: 18px !important; height: 18px !important; flex: 0 0 18px; } .app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-account { width: 100%; align-items: center; } .app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-checkin { width: 48px; } -.app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-storage { width: 36px; } +.app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-storage { width: 40px; } .app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-profile-row { width: 100%; justify-content: center; } .app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-profile { width: 48px; min-width: 48px; flex: 0 0 48px; margin-inline: auto; } .app-user-workspace .app-workspace-sidebar-nav.is-collapsed .app-workspace-sidebar-notification-spacer { display: none; } diff --git a/web/test/workspace-credit-gift.test.ts b/web/test/workspace-credit-gift.test.ts new file mode 100644 index 000000000..d19bf449a --- /dev/null +++ b/web/test/workspace-credit-gift.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +describe("workspace credit gift mark", () => { + test("top-bar credit entries use a product gift instead of a lucide coin", () => { + const topBar = readFileSync(resolve(import.meta.dir, "../src/components/layout/workspace-top-bar.tsx"), "utf8"); + const canvas = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/canvas-project-top-bar.tsx"), "utf8"); + const mark = readFileSync(resolve(import.meta.dir, "../src/components/layout/workspace-credit-gift-mark.tsx"), "utf8"); + const css = readFileSync(resolve(import.meta.dir, "../src/styles/globals.css"), "utf8"); + + expect(topBar).toContain(""); + expect(topBar).not.toContain("Coins"); + expect(canvas).toContain(""); + expect(canvas).not.toContain("Coins"); + expect(mark).toContain("#FFB34A"); + expect(mark).toContain("#12B8A8"); + expect(css).toContain(".app-workspace-credit-gift"); + expect(css).toContain("rotate(-8deg)"); + }); +}); diff --git a/web/test/workspace-sidebar-storage.test.ts b/web/test/workspace-sidebar-storage.test.ts index a89108a43..5234921c9 100644 --- a/web/test/workspace-sidebar-storage.test.ts +++ b/web/test/workspace-sidebar-storage.test.ts @@ -55,10 +55,13 @@ describe("workspace sidebar storage meter", () => { expect(sidebar).toContain(""); expect(sidebar.indexOf("WorkspaceSidebarStorageMeter")).toBeLessThan(sidebar.indexOf("app-workspace-sidebar-profile-row")); + expect(meter).toContain("HardDrive"); + expect(meter).toContain("app-workspace-sidebar-storage-icon"); expect(meter).toContain("已用 ${meter.usedLabel}"); expect(meter).toContain("剩余 ${meter.remainingLabel}"); expect(meter).toContain("共 ${meter.totalLabel}"); expect(meter).toContain('to="/assets"'); + expect(css).toContain(".app-workspace-sidebar-storage-icon"); 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");