Polish Lovart home layout

This commit is contained in:
134 2026-06-11 16:58:28 +08:00
parent 89b0eaaa3d
commit 6523ea740c
8 changed files with 204 additions and 185 deletions

View File

@ -8037,7 +8037,7 @@ export function CanvasChatPanel({
<span className="flex min-w-0 items-center gap-2">
<span
className={cn(
"h-4 w-4 overflow-hidden rounded-full",
"flex h-5 w-5 shrink-0 items-center justify-center overflow-hidden rounded-full leading-none",
isVideoWorkspace
? "border border-border/60 bg-background"
: "border border-transparent bg-white dark:border-white/20 dark:bg-white"
@ -8047,7 +8047,7 @@ export function CanvasChatPanel({
<img
src={buildPreviewImageUrl(model.avatarUrl, 40)}
alt={model.displayName}
className="h-full w-full object-cover"
className="block h-full w-full object-contain"
loading="lazy"
decoding="async"
onError={(event) => {
@ -8059,7 +8059,7 @@ export function CanvasChatPanel({
) : (
<span
className={cn(
"flex h-full w-full items-center justify-center text-[9px] font-medium",
"flex h-full w-full items-center justify-center text-[10px] font-medium leading-none",
isVideoWorkspace ? "text-muted-foreground" : "text-[#6b7280]"
)}
>
@ -10799,21 +10799,17 @@ export function CanvasChatPanel({
>
<span
className={cn(
"flex h-4 w-4 items-center justify-center overflow-hidden rounded-full",
isVideoWorkspace ? "bg-background dark:bg-transparent" : "bg-white dark:bg-transparent"
"flex h-5 w-5 shrink-0 items-center justify-center overflow-hidden rounded-full leading-none",
isVideoWorkspace
? "bg-background dark:bg-transparent"
: "bg-white dark:bg-transparent"
)}
>
{getToolbarItem("chatModelSelector").iconUrl ? (
<CanvasToolbarIcon
className="h-3 w-3"
fallback={Bot}
item={getToolbarItem("chatModelSelector")}
/>
) : selectDisplayModel?.avatarUrl ? (
{selectDisplayModel?.avatarUrl ? (
<img
src={buildPreviewImageUrl(selectDisplayModel.avatarUrl, 64)}
alt={selectDisplayModel.displayName || "模型头像"}
className="h-full w-full object-cover"
className="block h-full w-full object-contain"
loading="lazy"
decoding="async"
onError={(event) => {
@ -10822,10 +10818,16 @@ export function CanvasChatPanel({
}
}}
/>
) : getToolbarItem("chatModelSelector").iconUrl ? (
<CanvasToolbarIcon
className="h-4 w-4"
fallback={Bot}
item={getToolbarItem("chatModelSelector")}
/>
) : (
<span
className={cn(
"flex h-full w-full items-center justify-center text-[9px] font-medium",
"flex h-full w-full items-center justify-center text-[10px] font-medium leading-none",
isVideoWorkspace ? "text-foreground/70 dark:text-zinc-100" : "text-[#4b5563] dark:text-zinc-100"
)}
>

View File

@ -21,18 +21,18 @@ export function CanvasLogoLoadingView({
return (
<div className="flex h-dvh w-screen items-center justify-center bg-[#f4f4f5] text-[#20242a] dark:bg-[#08090b] dark:text-zinc-100">
<div className="flex flex-col items-center gap-5">
<div className="relative flex h-28 w-28 items-center justify-center">
<div className="flex flex-col items-center gap-3.5">
<div className="relative flex h-[78px] w-[78px] items-center justify-center">
<div className="absolute inset-0 animate-spin rounded-full bg-[conic-gradient(from_0deg,#ff7a00,#ffe04b,#48e6a5,#35a7ff,#b15cff,#ff4aa2,#ff7a00)] opacity-90 blur-[1px]" />
<div className="absolute inset-2 animate-ping rounded-full bg-[conic-gradient(from_180deg,#ff7a00,#35a7ff,#b15cff,#48e6a5,#ff7a00)] opacity-25" />
<div className="absolute inset-[6px] rounded-full bg-[#f4f4f5] dark:bg-[#08090b]" />
<div className="relative flex h-20 w-20 items-center justify-center overflow-hidden rounded-full border border-black/8 bg-white shadow-[0_18px_54px_rgba(15,23,42,0.16)] dark:border-white/10 dark:bg-zinc-950 dark:shadow-[0_24px_70px_rgba(0,0,0,0.45)]">
<div className="absolute inset-[6px] animate-ping rounded-full bg-[conic-gradient(from_180deg,#ff7a00,#35a7ff,#b15cff,#48e6a5,#ff7a00)] opacity-25" />
<div className="absolute inset-1 rounded-full bg-[#f4f4f5] dark:bg-[#08090b]" />
<div className="relative flex h-14 w-14 items-center justify-center overflow-hidden rounded-full border border-black/8 bg-white shadow-[0_13px_38px_rgba(15,23,42,0.16)] dark:border-white/10 dark:bg-zinc-950 dark:shadow-[0_17px_49px_rgba(0,0,0,0.45)]">
{lightLogo ? (
<>
<img
alt={resolvedSiteName}
className={cn(
"h-12 w-12 object-contain",
"h-[34px] w-[34px] object-contain",
hasSeparateDarkLogo && "dark:hidden"
)}
src={lightLogo}
@ -40,17 +40,17 @@ export function CanvasLogoLoadingView({
{hasSeparateDarkLogo ? (
<img
alt={resolvedSiteName}
className="hidden h-12 w-12 object-contain dark:block"
className="hidden h-[34px] w-[34px] object-contain dark:block"
src={darkLogo}
/>
) : null}
</>
) : (
<Sparkles className="h-8 w-8 text-[#20242a] dark:text-white" />
<Sparkles className="h-[22px] w-[22px] text-[#20242a] dark:text-white" />
)}
</div>
</div>
<div className="text-[13px] font-medium tracking-[0.18em] text-[#6b7280] dark:text-zinc-400">
<div className="text-[12px] font-medium tracking-[0.16em] text-[#6b7280] dark:text-zinc-400">
{label}
</div>
</div>

View File

@ -428,7 +428,6 @@ export function LovartHome({
const [files, setFiles] = useState<UploadFileItem[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [activeCategory, setActiveCategory] = useState("全部");
const [showAllProjects, setShowAllProjects] = useState(false);
const [isProjectsOnlyView, setIsProjectsOnlyView] = useState(false);
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
const [accountCenterOpen, setAccountCenterOpen] = useState(false);
@ -463,9 +462,6 @@ export function LovartHome({
const [deletingProjectId, setDeletingProjectId] = useState<string | null>(null);
const [typedPreviewText, setTypedPreviewText] = useState("");
const [previewCursorVisible, setPreviewCursorVisible] = useState(true);
const [routeTransitionLabel, setRouteTransitionLabel] = useState<string | null>(
null
);
const [isSecondaryDataReady, setIsSecondaryDataReady] = useState(false);
const [isFeaturedDataReady, setIsFeaturedDataReady] = useState(false);
const projectSectionRef = useRef<HTMLDivElement | null>(null);
@ -716,18 +712,15 @@ export function LovartHome({
.map((item) => String(item || "").trim())
.filter(Boolean)
// legacy typo compatibility: treat "惊喜" as "精选"
.map((item) => (item === "惊喜" ? "精选" : item));
.map((item) => (item === "惊喜" ? "精选" : item))
.filter((item) => item !== "精选");
const uniqueConfigured = Array.from(new Set(configured));
const hasRealConfig =
uniqueConfigured.length > 0 &&
!(uniqueConfigured.length === 1 && uniqueConfigured[0] === "全部");
const fallback = ["全部", "精选", ...DISCOVERY_DEFAULT_TABS];
const base = hasRealConfig ? uniqueConfigured : fallback;
const withoutAll = base.filter((item) => item !== "全部");
const merged = ["全部", ...withoutAll];
const merged = [
"全部",
...uniqueConfigured.filter((item) => item !== "全部"),
...DISCOVERY_DEFAULT_TABS,
];
return Array.from(new Set(merged)).slice(0, 12);
}, [surpriseCategories]);
@ -825,12 +818,10 @@ export function LovartHome({
);
}, [loadedFeaturedImages, activeCategory]);
const shouldShowAllProjects = isProjectsOnlyView || showAllProjects;
const recentProjects = useMemo(() => {
const projects = canvasData?.data || [];
return shouldShowAllProjects ? projects : projects.slice(0, 4);
}, [canvasData?.data, shouldShowAllProjects]);
return isProjectsOnlyView ? projects : projects.slice(0, 4);
}, [canvasData?.data, isProjectsOnlyView]);
const hasMoreProjects = useMemo(() => {
return (canvasData?.data || []).length > 4;
@ -1446,7 +1437,6 @@ export function LovartHome({
return;
}
setIsProjectsOnlyView(true);
setShowAllProjects(true);
projectSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
};
@ -1460,11 +1450,6 @@ export function LovartHome({
}
if (value.startsWith("/")) {
setRouteTransitionLabel(
value.startsWith("/canvas?mode=agent")
? "正在进入风格化工作台..."
: "正在打开页面..."
);
router.push(value);
return;
}
@ -1548,20 +1533,12 @@ export function LovartHome({
return (
<div className="min-h-svh overflow-x-hidden bg-[#f4f4f4] text-[#171717] dark:bg-[#0a0a0c] dark:text-zinc-100 [&_button]:cursor-pointer [&_input]:cursor-pointer [&_textarea]:cursor-pointer [&_img]:cursor-pointer [&_a]:cursor-pointer [&_[role=button]]:cursor-pointer">
{routeTransitionLabel ? (
<div className="fixed inset-0 z-[120] flex items-center justify-center bg-[#050607] text-white/70">
<div className="flex flex-col items-center gap-4 rounded-[32px] border border-white/10 bg-white/[0.04] px-8 py-7 shadow-[0_24px_90px_rgba(0,0,0,0.42)] backdrop-blur-xl">
<Loader2 className="h-5 w-5 animate-spin text-emerald-300" />
<div className="text-sm tracking-[0.08em]">
{routeTransitionLabel}
</div>
</div>
</div>
) : null}
<BrandTopBar
contentClassName="pl-[98px]"
logo={displayLogo}
onLogoClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
shouldInvertLogo={shouldInvertLogo}
showBrand={false}
siteName={siteName || "Lovart"}
>
<button className="flex h-8 w-8 items-center justify-center rounded-lg bg-black/5 text-black/75 hover:bg-black/10 dark:bg-white/6 dark:text-white/70 dark:hover:bg-white/10">
@ -1574,10 +1551,10 @@ export function LovartHome({
<Button
variant="ghost"
onClick={() => setUpgradeDialogOpen(true)}
className="h-8 rounded-[14px] bg-[#1c1c1e] px-2 text-[14px] text-white hover:bg-black hover:text-white"
className="h-8 rounded-[14px] border border-black/10 bg-white px-2 text-[14px] text-black/72 shadow-sm hover:bg-black/5 hover:text-black dark:border-white/10 dark:bg-white/6 dark:text-white/72 dark:hover:bg-white/10 dark:hover:text-white"
>
<div className="inline-flex items-center gap-1 rounded-[9px] bg-white/10 px-1.5 py-0.5 text-xs text-white">
<Zap className="h-3 w-3 text-amber-300" />
<div className="inline-flex items-center gap-1 rounded-[9px] px-1.5 py-0.5 text-xs">
<Zap className="h-3 w-3 text-amber-500" />
{membershipData?.summary?.currentCredits ?? 0}
</div>
</Button>
@ -1810,6 +1787,9 @@ export function LovartHome({
? "/settings/agent-workflow"
: null
}
shouldInvertLogo={shouldInvertLogo}
siteLogo={displayLogo}
siteName={siteName || "Lovart"}
toolbox1Enabled={siteSettingsData?.toolbox1Enabled !== false}
toolbox1Links={toolbox1Links}
toolbox2Enabled={siteSettingsData?.toolbox2Enabled === true}
@ -1820,7 +1800,7 @@ export function LovartHome({
tone={resolvedTheme === "dark" ? "dark" : "light"}
/>
<main className="mx-auto max-w-[1240px] px-6 pb-20 pt-[84px]">
<main className="ml-[118px] mr-[56px] w-auto max-w-none pb-20 pt-[84px] max-lg:mx-6">
{!isProjectsOnlyView && (
<LovartHeroComposer
composerPlaceholder={composerPlaceholder}
@ -1857,10 +1837,9 @@ export function LovartHome({
onCreateProject={openNewCanvas}
onDeleteProject={handleDeleteProject}
onOpenProject={(projectId) => router.push(`/canvas/${projectId}`)}
onViewAllProjects={openAllProjectsInPage}
projects={recentProjects}
projectSectionRef={projectSectionRef}
setShowAllProjects={setShowAllProjects}
showAllProjects={showAllProjects}
/>
{!isProjectsOnlyView && (

View File

@ -1,7 +1,6 @@
import { Heart, Loader2, Sparkles } from "lucide-react";
import { Gem, Heart, Loader2, PlayCircle } from "lucide-react";
import type { Ref } from "react";
import type { FeaturedImage } from "./types";
import { MASONRY_HEIGHTS } from "./utils";
type LovartFeaturedGalleryProps = {
activeCategory: string;
@ -25,19 +24,21 @@ export function LovartFeaturedGallery({
setActiveCategory,
}: LovartFeaturedGalleryProps) {
return (
<section ref={featuredSectionRef} className="mt-16">
<h2 className="text-[18px] font-semibold tracking-tight">灵感发现</h2>
<section ref={featuredSectionRef} className="mt-[166px]">
<h2 className="text-[18px] font-semibold leading-none tracking-[-0.01em] text-[#1f1f1f] dark:text-white">
灵感发现
</h2>
<div className="mt-3 flex flex-wrap gap-1.5">
<div className="mt-[20px] flex flex-wrap items-center gap-x-[28px] gap-y-2">
{categories.map((category) => (
<button
key={category}
type="button"
onClick={() => setActiveCategory(category)}
className={`rounded-xl px-3 py-1.5 text-[12px] ${
className={`h-[34px] rounded-[8px] px-[16px] text-[14px] font-normal leading-none transition ${
activeCategory === category
? "bg-[#ececec] text-black dark:bg-[#24242a] dark:text-white"
: "text-black/55 hover:bg-white dark:text-white/55 dark:hover:bg-[#17171b]"
? "bg-[#f2f2f2] text-[#202020] dark:bg-[#24242a] dark:text-white"
: "text-[#5f5f5f] hover:bg-[#f7f7f7] hover:text-[#202020] dark:text-white/55 dark:hover:bg-[#17171b]"
}`}
>
{category}
@ -46,41 +47,42 @@ export function LovartFeaturedGallery({
</div>
{featuredItems.length === 0 ? (
<div className="mt-5 rounded-xl border border-dashed border-black/15 bg-white p-10 text-center text-black/35 dark:border-white/12 dark:bg-[#141416] dark:text-white/35">
<div className="mt-[22px] rounded-[10px] border border-dashed border-black/15 bg-[#f7f7f7] p-10 text-center text-[14px] text-black/35 dark:border-white/12 dark:bg-[#141416] dark:text-white/35">
{isLoadingFeatured ? "灵感内容加载中..." : "当前没有可展示的灵感数据。"}
</div>
) : (
<div className="mt-5 columns-1 gap-4 md:columns-2 xl:columns-4">
<div className="mt-[14px] columns-1 gap-[10px] md:columns-2 lg:columns-5">
{featuredItems.map((item, index) => (
<button
key={item.id}
type="button"
onClick={() => window.open(item.originalUrl || item.thumbnailUrl, "_blank")}
className="group mb-4 block w-full cursor-pointer break-inside-avoid rounded-2xl border border-black/8 bg-white text-left shadow-sm transition hover:-translate-y-0.5 dark:border-white/10 dark:bg-[#141416]"
className="group mb-[24px] block w-full cursor-pointer break-inside-avoid text-left"
>
<div
className="overflow-hidden rounded-t-2xl bg-[#e8e8e8] dark:bg-[#1b1c20]"
style={{ height: `${MASONRY_HEIGHTS[index % MASONRY_HEIGHTS.length]}px` }}
>
<img
src={item.thumbnailUrl || item.originalUrl}
alt={item.prompt || "灵感图"}
className="h-full w-full object-cover transition-transform duration-300 ease-out will-change-transform group-hover:scale-[1.04]"
/>
</div>
<div className="flex items-center justify-between px-3 py-2 text-[12px] text-black/60 dark:text-white/55">
<div className="line-clamp-1 font-medium text-black/85 dark:text-white/85">
{item.user?.name || "创作者"}
<img
src={item.thumbnailUrl || item.originalUrl}
alt={item.prompt || "灵感图"}
className="block h-auto w-full rounded-[6px] bg-[#e8e8e8] object-cover transition-transform duration-300 ease-out will-change-transform group-hover:scale-[1.01] dark:bg-[#1b1c20]"
/>
<div className="mt-[8px] flex h-[24px] items-center justify-between text-[12px] font-normal leading-none text-[#b8b8b8] dark:text-white/45">
<div className="flex min-w-0 items-center gap-[7px] pr-2">
<span className="flex h-[18px] w-[18px] shrink-0 items-center justify-center overflow-hidden rounded-full bg-[#d8d8d8] text-[9px] font-medium text-white">
{(item.user?.name || "C").slice(0, 1).toUpperCase()}
</span>
<span className="line-clamp-1 text-[16px] font-normal leading-none text-[#4b4b4b] dark:text-white/80">
{item.user?.name || "创作者"}
</span>
</div>
<div className="inline-flex items-center gap-2 text-[11px]">
<span className="inline-flex items-center gap-1">
<Sparkles className="h-2.5 w-2.5" />
<div className="inline-flex shrink-0 items-center gap-[8px] text-[12px] leading-none">
<span className="inline-flex items-center gap-[4px]">
<PlayCircle className="h-[12px] w-[12px]" strokeWidth={2} />
{(1200 + index * 37).toLocaleString()}
</span>
<span className="inline-flex items-center gap-1">
<Heart className="h-2.5 w-2.5" />
<span className="inline-flex items-center gap-[4px]">
<Heart className="h-[12px] w-[12px]" strokeWidth={2} />
{(20 + (index % 9) * 11).toLocaleString()}
</span>
<Gem className="h-[12px] w-[12px]" strokeWidth={2} />
</div>
</div>
</button>

View File

@ -1,4 +1,4 @@
import { ArrowRight, Loader2, Trash2 } from "lucide-react";
import { ChevronRight, Loader2, Trash2 } from "lucide-react";
import type { Ref } from "react";
import type { CanvasProject } from "./types";
import { formatRelativeTime } from "./utils";
@ -10,10 +10,9 @@ type LovartProjectsSectionProps = {
onCreateProject: () => void;
onDeleteProject: (projectId: string, projectTitle?: string) => void;
onOpenProject: (projectId: string) => void;
onViewAllProjects: () => void;
projects: CanvasProject[];
projectSectionRef: Ref<HTMLElement>;
setShowAllProjects: (updater: (value: boolean) => boolean) => void;
showAllProjects: boolean;
};
export function LovartProjectsSection({
@ -23,75 +22,70 @@ export function LovartProjectsSection({
onCreateProject,
onDeleteProject,
onOpenProject,
onViewAllProjects,
projects,
projectSectionRef,
setShowAllProjects,
showAllProjects,
}: LovartProjectsSectionProps) {
return (
<section
ref={projectSectionRef}
className={isProjectsOnlyView ? "mt-6" : "mt-20"}
className={isProjectsOnlyView ? "mt-6" : "mt-[46px]"}
>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-[18px] font-semibold tracking-tight">
<div className="mb-[32px] flex items-center justify-between">
<h2 className="text-[26px] font-semibold leading-none tracking-[-0.01em] text-[#1f1f1f] dark:text-white">
{isProjectsOnlyView ? "所有项目" : "最近项目"}
</h2>
{!isProjectsOnlyView && hasMoreProjects ? (
<button
type="button"
className="inline-flex items-center gap-1 text-[13px] text-black/45 hover:text-black dark:text-white/45 dark:hover:text-white"
onClick={() => setShowAllProjects((value) => !value)}
className="inline-flex items-center gap-[8px] text-[22px] font-normal leading-none text-[#8b8b8b] transition hover:text-[#1f1f1f] dark:text-white/45 dark:hover:text-white"
onClick={onViewAllProjects}
>
{showAllProjects ? "收起" : "查看更多"}
<ArrowRight
className={`h-3.5 w-3.5 transition-transform ${showAllProjects ? "rotate-90" : ""}`}
/>
查看全部
<ChevronRight className="h-[22px] w-[22px]" strokeWidth={1.8} />
</button>
) : null}
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<div className="grid grid-cols-1 gap-x-[16px] gap-y-[36px] md:grid-cols-2 lg:grid-cols-5">
<button
type="button"
onClick={onCreateProject}
className="cursor-pointer rounded-[22px] border border-black/8 bg-white p-3 text-left shadow-sm transition hover:-translate-y-0.5 dark:border-white/10 dark:bg-[#141416]"
className="group cursor-pointer text-left"
>
<div className="flex aspect-[4/3] items-center justify-center rounded-2xl bg-[#efefef] dark:bg-[#1b1c20]">
<div className="text-center">
<div className="mb-2 text-3xl">+</div>
<div className="text-[14px]">新建项目</div>
</div>
<div className="flex aspect-[16/9] items-center justify-center rounded-[10px] border border-dashed border-[#d4d4d4] bg-[#f7f7f7] transition group-hover:border-[#bdbdbd] group-hover:bg-[#f3f3f3] dark:border-white/15 dark:bg-[#18191d]">
<span className="text-[34px] font-light leading-none text-[#8e8e8e]">+</span>
</div>
<div className="mt-[22px] text-[22px] font-normal leading-none tracking-[-0.01em] text-[#202020] dark:text-white">
新建项目
</div>
</button>
{projects.map((item) => (
<div
key={item.id}
className="group relative rounded-[22px] border border-black/8 bg-white p-3 text-left shadow-sm transition hover:-translate-y-0.5 dark:border-white/10 dark:bg-[#141416]"
className="group relative text-left"
>
<button
type="button"
onClick={() => onOpenProject(item.id)}
className="block w-full cursor-pointer text-left"
>
<div className="aspect-[4/3] overflow-hidden rounded-2xl bg-[#efefef] dark:bg-[#1b1c20]">
<div className="aspect-[16/9] overflow-hidden rounded-[10px] border border-[#dfdfdf] bg-[#f5f5f5] dark:border-white/10 dark:bg-[#1b1c20]">
{item.thumbnail ? (
<img
src={item.thumbnail}
alt={item.title}
className="h-full w-full object-cover transition-transform duration-300 ease-out will-change-transform group-hover:scale-[1.045]"
className="h-full w-full object-cover transition-transform duration-300 ease-out will-change-transform group-hover:scale-[1.025]"
/>
) : (
<div className="flex h-full w-full items-center justify-center text-black/35 dark:text-white/35">
Untitled
</div>
<div className="h-full w-full" />
)}
</div>
<div className="mt-3 text-[14px] font-medium">
<div className="mt-[22px] truncate text-[22px] font-normal leading-none tracking-[-0.01em] text-[#202020] dark:text-white">
{item.title || "Untitled"}
</div>
<div className="mt-1 text-xs text-black/35 dark:text-white/35">
<div className="mt-[13px] text-[16px] font-normal leading-none text-[#8e8e8e] dark:text-white/35">
{formatRelativeTime(item.updatedAt || item.createdAt)}
</div>
</button>
@ -99,7 +93,7 @@ export function LovartProjectsSection({
type="button"
onClick={() => onDeleteProject(item.id, item.title)}
disabled={deletingProjectId === item.id}
className="absolute right-5 top-5 z-20 inline-flex h-7 w-7 items-center justify-center rounded-full border border-black/10 bg-white/90 text-black/45 shadow-sm transition hover:border-[#ffb4b4] hover:text-[#ef4444] disabled:cursor-not-allowed disabled:opacity-80 dark:border-white/10 dark:bg-[#18181c]/90 dark:text-white/45"
className="absolute right-3 top-3 z-20 inline-flex h-8 w-8 items-center justify-center rounded-full border border-black/10 bg-white/90 text-black/45 opacity-0 shadow-sm transition hover:border-[#ffb4b4] hover:text-[#ef4444] group-hover:opacity-100 disabled:cursor-not-allowed disabled:opacity-80 dark:border-white/10 dark:bg-[#18181c]/90 dark:text-white/45"
aria-label={`删除${item.title || "未命名画布"}`}
>
{deletingProjectId === item.id ? (
@ -110,12 +104,6 @@ export function LovartProjectsSection({
</button>
</div>
))}
{projects.length === 0 && (
<div className="rounded-[22px] border border-dashed border-black/15 bg-white p-6 text-center text-black/40 md:col-span-3 dark:border-white/12 dark:bg-[#141416] dark:text-white/40">
还没有项目,点击“新建项目”开始。
</div>
)}
</div>
</section>
);

View File

@ -1,6 +1,5 @@
import { ArrowRight, PlayCircle } from "lucide-react";
import type { ReactNode } from "react";
import { FloatingSidebarRail } from "@/components/home/floating-sidebar-shell";
import {
HoverCard,
HoverCardContent,
@ -43,6 +42,9 @@ type LovartSidebarProps = {
}
) => ReactNode;
settingsHref: string | null;
shouldInvertLogo?: boolean;
siteLogo?: string | null;
siteName: string;
toolbox1Enabled: boolean;
toolbox1Links: NavLink[];
toolbox2Enabled: boolean;
@ -69,6 +71,9 @@ export function LovartSidebar({
productPlanningEnabled,
renderIcon,
settingsHref,
shouldInvertLogo = false,
siteLogo,
siteName,
toolbox1Enabled,
toolbox1Links,
toolbox2Enabled,
@ -78,13 +83,14 @@ export function LovartSidebar({
videoCanvasEnabled,
tone = "light",
}: LovartSidebarProps) {
const isDark = tone === "dark";
const sidebarPopupBaseClass =
"rounded-[24px] border border-black/8 bg-white/96 p-2.5 text-[#111827] shadow-[0_24px_64px_rgba(15,23,42,0.12)] backdrop-blur-xl dark:border-white/10 dark:bg-[#141416]/96 dark:text-white dark:shadow-[0_24px_64px_rgba(0,0,0,0.42)]";
const sidebarButtonClass = (active = false) =>
[
"group flex h-[34px] w-[34px] shrink-0 items-center justify-center rounded-[10px] text-[#6b7280] transition-all duration-200 hover:bg-black/[0.04] hover:text-[#111827] dark:text-white/62 dark:hover:bg-white/8 dark:hover:text-white",
"group flex h-11 w-11 shrink-0 items-center justify-center rounded-[11px] text-gray-800 transition-colors hover:bg-black/5 dark:text-white dark:hover:bg-white/10",
active
? "bg-[#f1f2f4] text-[#111827] shadow-[inset_0_0_0_1px_rgba(15,23,42,0.04)] dark:bg-white/12 dark:text-white"
? "bg-black/5 text-gray-950 dark:bg-white/10 dark:text-white"
: "",
]
.filter(Boolean)
@ -106,7 +112,7 @@ export function LovartSidebar({
>
{renderIcon(key, {
active: options?.active,
sizeClassName: "h-[15px] w-[15px]",
sizeClassName: "h-[18px] w-[18px]",
})}
</button>
</TooltipTrigger>
@ -127,7 +133,7 @@ export function LovartSidebar({
className={sidebarButtonClass()}
title={getItem(key).label}
>
{renderIcon(key, { sizeClassName: "h-[15px] w-[15px]" })}
{renderIcon(key, { sizeClassName: "h-[18px] w-[18px]" })}
</button>
</HoverCardTrigger>
<HoverCardContent
@ -156,31 +162,65 @@ export function LovartSidebar({
) : null;
return (
<aside className="fixed left-5 top-[88px] z-30 hidden flex-col items-center gap-2.5 lg:flex">
<aside
className={[
"fixed bottom-0 left-0 top-0 z-[100] hidden w-[62px] flex-col items-center border-r shadow-sm backdrop-blur-sm lg:flex",
isDark
? "border-white/10 bg-black/90"
: "border-gray-200 bg-white/90",
].join(" ")}
>
<TooltipProvider delayDuration={120}>
{isEnabled("newProject") ? (
<div className="flex h-full w-full flex-col items-center overflow-y-auto overflow-x-hidden px-2 pb-4 pt-[22px] [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onNewCanvas}
className="flex h-[48px] w-[48px] shrink-0 items-center justify-center rounded-[17px] border border-black/8 bg-white/96 shadow-[0_18px_40px_rgba(15,23,42,0.11)] backdrop-blur-xl transition-transform duration-200 hover:-translate-y-0.5 hover:bg-white dark:border-white/10 dark:bg-[#17181d]/94 dark:shadow-[0_22px_60px_rgba(6,8,14,0.34)] dark:hover:bg-[#202127]"
title={getItem("newProject").label}
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full transition-opacity hover:opacity-80"
onClick={onHome}
title={siteName}
>
<div className="flex h-[31px] w-[31px] items-center justify-center rounded-[10px] bg-[#f1f2f4] text-[#111827] shadow-[inset_0_0_0_1px_rgba(15,23,42,0.04)] dark:bg-white/12 dark:text-white">
{renderIcon("newProject", {
sizeClassName: "h-3.5 w-3.5",
})}
</div>
{siteLogo ? (
<img
src={siteLogo}
alt={siteName}
className={[
"h-9 w-9 rounded-full object-cover",
shouldInvertLogo ? "brightness-0 invert" : "",
]
.filter(Boolean)
.join(" ")}
/>
) : (
<span className="flex h-9 w-9 items-center justify-center rounded-full bg-black text-[14px] font-semibold text-white dark:bg-white dark:text-black">
{siteName.slice(0, 1).toUpperCase()}
</span>
)}
</button>
</TooltipTrigger>
<TooltipContent side="right">{getItem("newProject").label}</TooltipContent>
<TooltipContent side="right">{siteName}</TooltipContent>
</Tooltip>
) : null}
<FloatingSidebarRail
className="max-h-[calc(100vh-160px)] !w-[48px] !max-w-[48px] gap-1 !rounded-[20px] !px-[7px] !py-[7px]"
tone={tone}
>
<div className="mt-8 flex w-full flex-col items-center gap-1">
{isEnabled("newProject") ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onNewCanvas}
className={sidebarButtonClass()}
title={getItem("newProject").label}
>
{renderIcon("newProject", {
sizeClassName: "h-[18px] w-[18px]",
})}
</button>
</TooltipTrigger>
<TooltipContent side="right">
{getItem("newProject").label}
</TooltipContent>
</Tooltip>
) : null}
{tooltipButton("home", onHome, { active: !isProjectsOnlyView })}
{tooltipButton("projects", onAllProjects, { active: isProjectsOnlyView })}
{tooltipButton("gallery", onGallery)}
@ -192,7 +232,7 @@ export function LovartSidebar({
className={sidebarButtonClass()}
title={getItem("tutorial").label}
>
{renderIcon("tutorial", { sizeClassName: "h-[15px] w-[15px]" })}
{renderIcon("tutorial", { sizeClassName: "h-[18px] w-[18px]" })}
</button>
</HoverCardTrigger>
<HoverCardContent
@ -254,8 +294,12 @@ export function LovartSidebar({
: null}
{toolbox("toolbox1", toolbox1Links, toolbox1Enabled)}
{toolbox("toolbox2", toolbox2Links, toolbox2Enabled)}
{settingsHref ? tooltipButton("settings", () => onRoute(settingsHref)) : null}
</FloatingSidebarRail>
</div>
<div className="mt-auto flex w-full flex-col items-center gap-1 pt-4">
{settingsHref ? tooltipButton("settings", () => onRoute(settingsHref)) : null}
</div>
</div>
</TooltipProvider>
</aside>
);

View File

@ -127,16 +127,15 @@ export function getUserInitials(user: HomeUser | null) {
}
export function formatRelativeTime(value?: string) {
if (!value) return "更新于今天";
if (!value) return "更新于 Jun 11, 2026";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "更新于今天";
const diffMs = Date.now() - date.getTime();
const minute = 60 * 1000;
const hour = 60 * minute;
const day = 24 * hour;
if (diffMs < hour) return `更新于 ${Math.max(1, Math.round(diffMs / minute))} 分钟前`;
if (diffMs < day) return `更新于 ${Math.round(diffMs / hour)} 小时前`;
return `更新于 ${Math.round(diffMs / day)} 天前`;
if (Number.isNaN(date.getTime())) return "更新于 Jun 11, 2026";
const formatted = date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
return `更新于 ${formatted}`;
}
export function formatPrice(currency: string, value: number) {

View File

@ -13,6 +13,7 @@ type BrandTopBarProps = {
onLogoClick?: () => void;
position?: "fixed" | "sticky" | "static";
shouldInvertLogo?: boolean;
showBrand?: boolean;
siteName: string;
};
@ -25,6 +26,7 @@ export function BrandTopBar({
onLogoClick,
position = "fixed",
shouldInvertLogo = false,
showBrand = true,
siteName,
}: BrandTopBarProps) {
const resolvedSiteName = String(siteName || "").trim() || "由壹点AI";
@ -45,33 +47,36 @@ export function BrandTopBar({
>
<div
className={cn(
"flex h-full items-center justify-between px-7",
"flex h-full items-center px-7",
showBrand ? "justify-between" : "justify-end",
contentClassName
)}
>
<button
className="flex items-center gap-2 transition-opacity hover:opacity-80"
onClick={onLogoClick}
type="button"
>
{logo ? (
<img
alt={resolvedSiteName}
className={cn(
"h-8 w-8 rounded-full object-cover",
shouldInvertLogo && "brightness-0 invert"
)}
src={logo}
/>
) : (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-black text-white dark:bg-white dark:text-black">
<Sparkles className="h-4 w-4" />
</div>
)}
<span className="text-[20px] font-semibold leading-none tracking-tight dark:text-zinc-100">
{resolvedSiteName}
</span>
</button>
{showBrand ? (
<button
className="flex items-center gap-2 transition-opacity hover:opacity-80"
onClick={onLogoClick}
type="button"
>
{logo ? (
<img
alt={resolvedSiteName}
className={cn(
"h-8 w-8 rounded-full object-cover",
shouldInvertLogo && "brightness-0 invert"
)}
src={logo}
/>
) : (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-black text-white dark:bg-white dark:text-black">
<Sparkles className="h-4 w-4" />
</div>
)}
<span className="text-[20px] font-semibold leading-none tracking-tight dark:text-zinc-100">
{resolvedSiteName}
</span>
</button>
) : null}
<div className="flex items-center gap-2">{actions || children}</div>
</div>