优化新版首页打字特效并支持固定前缀配置
This commit is contained in:
parent
b7a9c30e70
commit
a6f55c7c21
@ -176,6 +176,7 @@ type SiteSettings = {
|
||||
canvasEnabled: boolean;
|
||||
canvasDefaultModelId: string | null;
|
||||
canvasAutoModeDefaultModelId: string | null;
|
||||
lovartComposerPrefix: string;
|
||||
lovartComposerPreviewTexts: string[];
|
||||
lovartPromoText: string;
|
||||
lovartPromoActionText: string;
|
||||
@ -408,8 +409,9 @@ export function AdminSettingsClient() {
|
||||
canvasEnabled: true,
|
||||
canvasDefaultModelId: null,
|
||||
canvasAutoModeDefaultModelId: null,
|
||||
lovartComposerPrefix: "让Buyi-ai",
|
||||
lovartComposerPreviewTexts: [
|
||||
"让 Lovart 打造引人注目的社交媒体视觉",
|
||||
"打造引人注目的社交媒体视觉",
|
||||
"帮我把这张产品图做成高级电商海报",
|
||||
"基于参考图延展 3 套同风格方案",
|
||||
],
|
||||
@ -462,12 +464,15 @@ export function AdminSettingsClient() {
|
||||
canvasEnabled: siteSettingsData.canvasEnabled ?? true,
|
||||
canvasDefaultModelId: siteSettingsData.canvasDefaultModelId || null,
|
||||
canvasAutoModeDefaultModelId: siteSettingsData.canvasAutoModeDefaultModelId || null,
|
||||
lovartComposerPrefix:
|
||||
String((siteSettingsData as any).lovartComposerPrefix || "").trim() ||
|
||||
"让Buyi-ai",
|
||||
lovartComposerPreviewTexts: Array.isArray(siteSettingsData.lovartComposerPreviewTexts)
|
||||
? siteSettingsData.lovartComposerPreviewTexts
|
||||
.map((item: any) => String(item).trim())
|
||||
.filter(Boolean)
|
||||
: [
|
||||
"让 Lovart 打造引人注目的社交媒体视觉",
|
||||
"打造引人注目的社交媒体视觉",
|
||||
"帮我把这张产品图做成高级电商海报",
|
||||
"基于参考图延展 3 套同风格方案",
|
||||
],
|
||||
@ -2713,6 +2718,23 @@ export function AdminSettingsClient() {
|
||||
</div>
|
||||
<div className="grid gap-4 rounded-lg border border-dashed p-4">
|
||||
<div className="text-sm font-medium">新版首页文案配置</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lovartComposerPrefix">对话框固定前缀文案</Label>
|
||||
<Input
|
||||
id="lovartComposerPrefix"
|
||||
value={siteSettings.lovartComposerPrefix}
|
||||
onChange={(e) =>
|
||||
setSiteSettings({
|
||||
...siteSettings,
|
||||
lovartComposerPrefix: e.target.value.slice(0, 40),
|
||||
})
|
||||
}
|
||||
placeholder="让Buyi-ai"
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
该前缀会固定显示在打字文案前面,切换文案时不删除。
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lovartComposerPreviewTexts">
|
||||
对话框预览文案(每行一条,按顺序轮播打字显示)
|
||||
@ -2733,17 +2755,17 @@ export function AdminSettingsClient() {
|
||||
values.length > 0
|
||||
? values
|
||||
: [
|
||||
"让 Lovart 打造引人注目的社交媒体视觉",
|
||||
"打造引人注目的社交媒体视觉",
|
||||
"帮我把这张产品图做成高级电商海报",
|
||||
"基于参考图延展 3 套同风格方案",
|
||||
],
|
||||
});
|
||||
}}
|
||||
rows={5}
|
||||
placeholder={"让 Lovart 打造引人注目的社交媒体视觉\n帮我把这张产品图做成高级电商海报"}
|
||||
placeholder={"打造引人注目的社交媒体视觉\n帮我把这张产品图做成高级电商海报"}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
前端输入框将按此顺序循环打字显示,支持最多 20 条。
|
||||
仅配置动态尾文案;前端会固定显示前缀后再进行打字与删字切换。支持最多 20 条。
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@ -15,6 +15,7 @@ const HOME_VARIANT_KEY = "home_variant";
|
||||
// Historically stored as "home_surprise_categories" but used for the featured gallery categories.
|
||||
const HOME_SURPRISE_CATEGORIES_KEY = "home_surprise_categories";
|
||||
const LOVART_COMPOSER_PREVIEW_TEXTS_KEY = "lovart_composer_preview_texts";
|
||||
const LOVART_COMPOSER_PREFIX_KEY = "lovart_composer_prefix";
|
||||
const MEMBERSHIP_CONFIG_KEY = "membership_config";
|
||||
const MEMBERSHIP_USER_OVERRIDES_KEY = "membership_user_plan_overrides";
|
||||
|
||||
@ -48,7 +49,7 @@ function normalizeSurpriseCategories(value: unknown): string[] {
|
||||
|
||||
function normalizeLovartComposerPreviewTexts(value: unknown): string[] {
|
||||
const defaults = [
|
||||
"让 Lovart 打造引人注目的社交媒体视觉",
|
||||
"打造引人注目的社交媒体视觉",
|
||||
"帮我把这张产品图做成高级电商海报",
|
||||
"基于参考图延展 3 套同风格方案",
|
||||
];
|
||||
@ -65,6 +66,14 @@ function normalizeLovartComposerPreviewTexts(value: unknown): string[] {
|
||||
return normalized.length > 0 ? normalized : defaults;
|
||||
}
|
||||
|
||||
function normalizeLovartComposerPrefix(value: unknown): string {
|
||||
const normalized = String(value || "").trim();
|
||||
if (!normalized) {
|
||||
return "让Buyi-ai";
|
||||
}
|
||||
return normalized.slice(0, 40);
|
||||
}
|
||||
|
||||
// GET /api/site-settings - 获取网站设置 (公开)
|
||||
export async function GET() {
|
||||
try {
|
||||
@ -74,6 +83,7 @@ export async function GET() {
|
||||
homeVariantSetting,
|
||||
surpriseCategoriesSetting,
|
||||
lovartComposerPreviewTextsSetting,
|
||||
lovartComposerPrefixSetting,
|
||||
membershipConfigSetting,
|
||||
membershipOverridesSetting,
|
||||
] = await Promise.all([
|
||||
@ -102,6 +112,12 @@ export async function GET() {
|
||||
.where(eq(systemSettings.key, LOVART_COMPOSER_PREVIEW_TEXTS_KEY))
|
||||
.limit(1)
|
||||
.then(rows => rows[0]),
|
||||
db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
.where(eq(systemSettings.key, LOVART_COMPOSER_PREFIX_KEY))
|
||||
.limit(1)
|
||||
.then(rows => rows[0]),
|
||||
db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
@ -122,6 +138,9 @@ export async function GET() {
|
||||
const lovartComposerPreviewTexts = normalizeLovartComposerPreviewTexts(
|
||||
lovartComposerPreviewTextsSetting?.value
|
||||
);
|
||||
const lovartComposerPrefix = normalizeLovartComposerPrefix(
|
||||
lovartComposerPrefixSetting?.value
|
||||
);
|
||||
const membershipConfig = normalizeMembershipConfig(membershipConfigSetting?.value);
|
||||
const membershipUserOverrides = normalizeMembershipUserOverrides(
|
||||
membershipOverridesSetting?.value
|
||||
@ -158,6 +177,7 @@ export async function GET() {
|
||||
lovartHeroSubtitle: "懂你的设计代理,帮你搞定一切",
|
||||
homeVariant,
|
||||
surpriseCategories,
|
||||
lovartComposerPrefix,
|
||||
lovartComposerPreviewTexts,
|
||||
membershipConfig,
|
||||
membershipUserOverrides,
|
||||
@ -174,6 +194,7 @@ export async function GET() {
|
||||
notification,
|
||||
homeVariant,
|
||||
surpriseCategories,
|
||||
lovartComposerPrefix,
|
||||
lovartComposerPreviewTexts,
|
||||
membershipConfig,
|
||||
membershipUserOverrides,
|
||||
@ -224,6 +245,7 @@ const updateSchema = z.object({
|
||||
lovartHeroSubtitle: z.string().max(300).optional(),
|
||||
homeVariant: z.enum(["classic", "lovart"]).optional(),
|
||||
surpriseCategories: z.array(z.string().trim().min(1).max(20)).max(12).optional(),
|
||||
lovartComposerPrefix: z.string().trim().min(1).max(40).optional(),
|
||||
lovartComposerPreviewTexts: z.array(z.string().trim().min(1).max(120)).max(20).optional(),
|
||||
membershipConfig: z.unknown().optional(),
|
||||
membershipUserOverrides: z.unknown().optional(),
|
||||
@ -247,6 +269,7 @@ export async function PUT(request: NextRequest) {
|
||||
const {
|
||||
homeVariant,
|
||||
surpriseCategories,
|
||||
lovartComposerPrefix,
|
||||
lovartComposerPreviewTexts,
|
||||
membershipConfig,
|
||||
membershipUserOverrides,
|
||||
@ -327,6 +350,26 @@ export async function PUT(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
if (lovartComposerPrefix !== undefined) {
|
||||
const normalized = normalizeLovartComposerPrefix(lovartComposerPrefix);
|
||||
await db
|
||||
.insert(systemSettings)
|
||||
.values({
|
||||
key: LOVART_COMPOSER_PREFIX_KEY,
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: systemSettings.key,
|
||||
set: {
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (membershipConfig !== undefined) {
|
||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||
await db
|
||||
@ -374,6 +417,10 @@ export async function PUT(request: NextRequest) {
|
||||
surpriseCategories !== undefined
|
||||
? normalizeSurpriseCategories(surpriseCategories)
|
||||
: normalizeSurpriseCategories(undefined),
|
||||
lovartComposerPrefix:
|
||||
lovartComposerPrefix !== undefined
|
||||
? normalizeLovartComposerPrefix(lovartComposerPrefix)
|
||||
: normalizeLovartComposerPrefix(undefined),
|
||||
lovartComposerPreviewTexts:
|
||||
lovartComposerPreviewTexts !== undefined
|
||||
? normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts)
|
||||
@ -484,6 +531,26 @@ export async function PUT(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
if (lovartComposerPrefix !== undefined) {
|
||||
const normalized = normalizeLovartComposerPrefix(lovartComposerPrefix);
|
||||
await db
|
||||
.insert(systemSettings)
|
||||
.values({
|
||||
key: LOVART_COMPOSER_PREFIX_KEY,
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: systemSettings.key,
|
||||
set: {
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (membershipConfig !== undefined) {
|
||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||
await db
|
||||
@ -531,6 +598,10 @@ export async function PUT(request: NextRequest) {
|
||||
surpriseCategories !== undefined
|
||||
? normalizeSurpriseCategories(surpriseCategories)
|
||||
: normalizeSurpriseCategories(undefined),
|
||||
lovartComposerPrefix:
|
||||
lovartComposerPrefix !== undefined
|
||||
? normalizeLovartComposerPrefix(lovartComposerPrefix)
|
||||
: normalizeLovartComposerPrefix(undefined),
|
||||
lovartComposerPreviewTexts:
|
||||
lovartComposerPreviewTexts !== undefined
|
||||
? normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts)
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
ChevronDown,
|
||||
Clapperboard,
|
||||
Folder,
|
||||
Heart,
|
||||
Home,
|
||||
ImageIcon,
|
||||
Info,
|
||||
@ -162,6 +163,27 @@ const PROMPT_TAGS = [
|
||||
|
||||
const MASONRY_HEIGHTS = [420, 290, 340, 470, 300, 360, 430, 320];
|
||||
const FEATURED_PAGE_SIZE = 24;
|
||||
const COMPOSER_PREFIX_DEFAULT = "让Buyi-ai";
|
||||
|
||||
function normalizeComposerPreviewTail(text: string, fixedPrefix: string) {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return "";
|
||||
|
||||
const sanitizedPrefix = fixedPrefix.trim();
|
||||
if (sanitizedPrefix && trimmed.startsWith(sanitizedPrefix)) {
|
||||
return trimmed.slice(sanitizedPrefix.length).trimStart();
|
||||
}
|
||||
|
||||
const legacyPrefixRemoved = trimmed
|
||||
.replace(/^让\s*buyi[-_ ]?ai\s*/i, "")
|
||||
.replace(/^让\s*lovart\s*/i, "")
|
||||
.trimStart();
|
||||
if (legacyPrefixRemoved !== trimmed) {
|
||||
return legacyPrefixRemoved;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function getUserInitials(user: HomeUser | null) {
|
||||
const source = (user?.name || user?.email || "mu").trim();
|
||||
@ -280,6 +302,7 @@ export function LovartHome({
|
||||
|
||||
const { data: siteSettingsData } = useSWR<{
|
||||
canvasDefaultModelId?: string | null;
|
||||
lovartComposerPrefix?: string | null;
|
||||
lovartComposerPreviewTexts?: string[];
|
||||
}>("/api/site-settings", fetcher);
|
||||
|
||||
@ -358,12 +381,25 @@ export function LovartHome({
|
||||
}
|
||||
|
||||
return [
|
||||
"让 Lovart 打造引人注目的社交媒体视觉",
|
||||
"打造引人注目的社交媒体视觉",
|
||||
"帮我把这张产品图做成高级电商海报",
|
||||
"基于参考图延展 3 套同风格方案",
|
||||
];
|
||||
}, [siteSettingsData?.lovartComposerPreviewTexts]);
|
||||
|
||||
const composerFixedPrefix = useMemo(() => {
|
||||
return (
|
||||
String(siteSettingsData?.lovartComposerPrefix || "").trim() ||
|
||||
COMPOSER_PREFIX_DEFAULT
|
||||
);
|
||||
}, [siteSettingsData?.lovartComposerPrefix]);
|
||||
|
||||
const composerPreviewTails = useMemo(() => {
|
||||
return composerPreviewTexts
|
||||
.map((text) => normalizeComposerPreviewTail(text, composerFixedPrefix))
|
||||
.filter(Boolean);
|
||||
}, [composerFixedPrefix, composerPreviewTexts]);
|
||||
|
||||
const loadedFeaturedImages = useMemo(() => {
|
||||
return (featuredPages || []).flatMap((page) => page.images || []);
|
||||
}, [featuredPages]);
|
||||
@ -474,44 +510,58 @@ export function LovartHome({
|
||||
let disposed = false;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
if (composerPreviewTexts.length === 0) {
|
||||
if (composerPreviewTails.length === 0) {
|
||||
setTypedPreviewText("");
|
||||
return () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
};
|
||||
}
|
||||
|
||||
const runTyping = (textIndex: number) => {
|
||||
let textIndex = 0;
|
||||
let charIndex = 0;
|
||||
|
||||
const typeForward = () => {
|
||||
if (disposed) return;
|
||||
const currentText = composerPreviewTexts[textIndex] || "";
|
||||
let charIndex = 0;
|
||||
const currentText = composerPreviewTails[textIndex] || "";
|
||||
|
||||
const typeNextChar = () => {
|
||||
if (disposed) return;
|
||||
|
||||
setTypedPreviewText(currentText.slice(0, charIndex));
|
||||
if (charIndex < currentText.length) {
|
||||
charIndex += 1;
|
||||
setTypedPreviewText(currentText.slice(0, charIndex));
|
||||
timer = setTimeout(typeForward, 55);
|
||||
return;
|
||||
}
|
||||
|
||||
if (charIndex <= currentText.length) {
|
||||
timer = setTimeout(typeNextChar, 55);
|
||||
return;
|
||||
}
|
||||
if (composerPreviewTails.length === 1) {
|
||||
setTypedPreviewText(currentText);
|
||||
return;
|
||||
}
|
||||
|
||||
timer = setTimeout(() => {
|
||||
runTyping((textIndex + 1) % composerPreviewTexts.length);
|
||||
}, 1300);
|
||||
};
|
||||
|
||||
typeNextChar();
|
||||
timer = setTimeout(typeBackward, 1200);
|
||||
};
|
||||
|
||||
runTyping(0);
|
||||
const typeBackward = () => {
|
||||
if (disposed) return;
|
||||
const currentText = composerPreviewTails[textIndex] || "";
|
||||
|
||||
if (charIndex > 0) {
|
||||
charIndex -= 1;
|
||||
setTypedPreviewText(currentText.slice(0, charIndex));
|
||||
timer = setTimeout(typeBackward, 35);
|
||||
return;
|
||||
}
|
||||
|
||||
textIndex = (textIndex + 1) % composerPreviewTails.length;
|
||||
timer = setTimeout(typeForward, 260);
|
||||
};
|
||||
|
||||
setTypedPreviewText("");
|
||||
timer = setTimeout(typeForward, 260);
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
if (timer) clearTimeout(timer);
|
||||
};
|
||||
}, [composerPreviewTexts]);
|
||||
}, [composerPreviewTails]);
|
||||
|
||||
useEffect(() => {
|
||||
const blinkTimer = setInterval(() => {
|
||||
@ -708,7 +758,7 @@ export function LovartHome({
|
||||
projectSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
};
|
||||
|
||||
const composerPlaceholder = `${typedPreviewText || composerPreviewTexts[0] || "描述你想创作的内容"}${
|
||||
const composerPlaceholder = `${composerFixedPrefix}${typedPreviewText}${
|
||||
previewCursorVisible ? " |" : ""
|
||||
}`;
|
||||
|
||||
@ -1111,7 +1161,7 @@ export function LovartHome({
|
||||
<button
|
||||
type="button"
|
||||
onClick={openNewCanvas}
|
||||
className="rounded-[22px] border border-black/8 bg-white p-3 text-left shadow-sm transition hover:-translate-y-0.5"
|
||||
className="cursor-pointer rounded-[22px] border border-black/8 bg-white p-3 text-left shadow-sm transition hover:-translate-y-0.5"
|
||||
>
|
||||
<div className="flex aspect-[4/3] items-center justify-center rounded-2xl bg-[#efefef]">
|
||||
<div className="text-center">
|
||||
@ -1126,14 +1176,14 @@ export function LovartHome({
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => router.push(`/canvas/${item.id}`)}
|
||||
className="group rounded-[22px] border border-black/8 bg-white p-3 text-left shadow-sm transition hover:-translate-y-0.5"
|
||||
className="group cursor-pointer rounded-[22px] border border-black/8 bg-white p-3 text-left shadow-sm transition hover:-translate-y-0.5"
|
||||
>
|
||||
<div className="aspect-[4/3] overflow-hidden rounded-2xl bg-[#efefef]">
|
||||
{item.thumbnail ? (
|
||||
<img
|
||||
src={item.thumbnail}
|
||||
alt={item.title}
|
||||
className="h-full w-full object-cover transition-transform duration-300 ease-out group-hover:scale-[1.03]"
|
||||
className="h-full w-full object-cover transition-transform duration-300 ease-out will-change-transform group-hover:scale-[1.045]"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center text-black/35">Untitled</div>
|
||||
@ -1183,7 +1233,7 @@ export function LovartHome({
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => window.open(item.originalUrl || item.thumbnailUrl, "_blank")}
|
||||
className="group mb-4 block w-full break-inside-avoid rounded-2xl border border-black/8 bg-white text-left shadow-sm"
|
||||
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"
|
||||
>
|
||||
<div
|
||||
className="overflow-hidden rounded-t-2xl bg-[#e8e8e8]"
|
||||
@ -1192,7 +1242,7 @@ export function LovartHome({
|
||||
<img
|
||||
src={item.thumbnailUrl || item.originalUrl}
|
||||
alt={item.prompt || "灵感图"}
|
||||
className="h-full w-full object-cover transition-transform duration-300 ease-out group-hover:scale-[1.02]"
|
||||
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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user