feat: add dynamic canvas placeholder with shared admin config

This commit is contained in:
Your Name 2026-03-04 01:53:20 +08:00
parent a4dca610aa
commit 0c44d498d1
2 changed files with 123 additions and 4 deletions

View File

@ -3781,9 +3781,9 @@ export function AdminSettingsClient() {
</div>
</div>
<div className="grid gap-4 rounded-lg border border-dashed p-4">
<div className="text-sm font-medium">新版首页文案配置</div>
<div className="text-sm font-medium">首页 / 无限画布对话框文案配置</div>
<div className="space-y-2">
<Label htmlFor="lovartComposerPrefix">对话框固定前缀文案</Label>
<Label htmlFor="lovartComposerPrefix">对话框固定前缀文案(首页与无限画布共用)</Label>
<Input
id="lovartComposerPrefix"
value={siteSettings.lovartComposerPrefix}
@ -3801,7 +3801,7 @@ export function AdminSettingsClient() {
</div>
<div className="space-y-2">
<Label htmlFor="lovartComposerPreviewTexts">
对话框预览文案(每行一条,按顺序轮播打字显示)
对话框预览文案(首页与无限画布共用,每行一条,按顺序轮播打字显示)
</Label>
<Textarea
id="lovartComposerPreviewTexts"

View File

@ -89,6 +89,32 @@ interface CanvasChatPanelProps {
// 建议卡片数据 - 已移除
const SUGGESTIONS: { title: string; desc: string; image: string }[] = [];
const CANVAS_COMPOSER_PREFIX_DEFAULT = "让Buyi-ai";
const CANVAS_COMPOSER_PREVIEW_TEXTS_DEFAULT = [
"打造引人注目的社交媒体视觉",
"帮我把这张产品图做成高级电商海报",
"基于参考图延展 3 套同风格方案",
];
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 getImageDimensions(url: string): Promise<{
@ -2248,6 +2274,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
bannerEnabled?: boolean;
bannerText?: string;
bannerLink?: string | null;
lovartComposerPrefix?: string | null;
lovartComposerPreviewTexts?: string[];
canvasEnabled?: boolean;
aiDetailGenerationEnabled?: boolean;
canvasDefaultModelId?: string | null;
@ -2292,6 +2320,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
}
return "";
}, [session?.user?.email, session?.user?.name]);
const [canvasTypedPreviewText, setCanvasTypedPreviewText] = useState("");
const [canvasPreviewCursorVisible, setCanvasPreviewCursorVisible] = useState(true);
const [greetingPeriodText, setGreetingPeriodText] = useState("中午好!");
useEffect(() => {
const hour = new Date().getHours();
@ -2309,6 +2339,95 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
}
setGreetingPeriodText("深夜好!");
}, []);
const canvasComposerPrefix = useMemo(
() =>
String(siteSettings?.lovartComposerPrefix || "").trim() ||
CANVAS_COMPOSER_PREFIX_DEFAULT,
[siteSettings?.lovartComposerPrefix]
);
const canvasComposerPreviewTexts = useMemo(() => {
const configured = (siteSettings?.lovartComposerPreviewTexts || [])
.map((item) => String(item || "").trim())
.filter(Boolean);
return configured.length > 0
? configured
: CANVAS_COMPOSER_PREVIEW_TEXTS_DEFAULT;
}, [siteSettings?.lovartComposerPreviewTexts]);
const canvasComposerPreviewTails = useMemo(
() =>
canvasComposerPreviewTexts
.map((text) => normalizeComposerPreviewTail(text, canvasComposerPrefix))
.filter(Boolean),
[canvasComposerPrefix, canvasComposerPreviewTexts]
);
useEffect(() => {
let disposed = false;
let timer: ReturnType<typeof setTimeout> | null = null;
if (canvasComposerPreviewTails.length === 0) {
setCanvasTypedPreviewText("");
return () => {
if (timer) clearTimeout(timer);
};
}
let textIndex = 0;
let charIndex = 0;
const typeForward = () => {
if (disposed) return;
const currentText = canvasComposerPreviewTails[textIndex] || "";
if (charIndex < currentText.length) {
charIndex += 1;
setCanvasTypedPreviewText(currentText.slice(0, charIndex));
timer = setTimeout(typeForward, 55);
return;
}
if (canvasComposerPreviewTails.length === 1) {
setCanvasTypedPreviewText(currentText);
return;
}
timer = setTimeout(typeBackward, 1200);
};
const typeBackward = () => {
if (disposed) return;
const currentText = canvasComposerPreviewTails[textIndex] || "";
if (charIndex > 0) {
charIndex -= 1;
setCanvasTypedPreviewText(currentText.slice(0, charIndex));
timer = setTimeout(typeBackward, 35);
return;
}
textIndex = (textIndex + 1) % canvasComposerPreviewTails.length;
timer = setTimeout(typeForward, 260);
};
setCanvasTypedPreviewText("");
timer = setTimeout(typeForward, 260);
return () => {
disposed = true;
if (timer) clearTimeout(timer);
};
}, [canvasComposerPreviewTails]);
useEffect(() => {
const blinkTimer = setInterval(() => {
setCanvasPreviewCursorVisible((value) => !value);
}, 450);
return () => clearInterval(blinkTimer);
}, []);
const canvasDynamicPlaceholder = `${canvasComposerPrefix}${canvasTypedPreviewText}${
canvasPreviewCursorVisible ? " |" : ""
}`;
const totalGeneratedImages = Number(userImageStats?.totalImages ?? NaN);
const aiDetailGenerationEnabled = siteSettings?.aiDetailGenerationEnabled === true;
const shouldShowCanvasBanner =
@ -6008,7 +6127,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
: selectedSkillTag?.placeholder?.trim() ||
(selectedSkillTag
? `请输入「${selectedSkillTag.name}」相关需求`
: "请输入你的设计需求,例如:绿色椅子 替换 Image");
: canvasDynamicPlaceholder || "请输入你的设计需求,例如:绿色椅子 替换 Image");
const detailProductCount = DETAIL_PRODUCT_SLOT_INDEXES.filter((index) => Boolean(detailSlots[index]?.url)).length;
const detailModelReady = DETAIL_MODEL_SLOT_INDEX >= 0 ? Boolean(detailSlots[DETAIL_MODEL_SLOT_INDEX]?.url) : false;
const detailBundleCount = DETAIL_BUNDLE_SLOT_INDEXES.filter((index) => Boolean(detailSlots[index]?.url)).length;