Polish Lovart home layout
This commit is contained in:
parent
89b0eaaa3d
commit
6523ea740c
@ -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"
|
||||
)}
|
||||
>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user