diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 124370630..fcbfff74a 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -198,6 +198,8 @@ type SiteSettings = { canvasAutoModeDefaultModelId: string | null; lovartComposerPrefix: string; lovartComposerPreviewTexts: string[]; + lovartPromptTagVisibility: LovartPromptTagVisibility; + contactConfig: ContactConfig; pointsSystemConfig: PointsSystemConfig; pointsPackages: PointsPackage[]; dulupayConfig: DulupayConfig; @@ -211,6 +213,46 @@ type SiteSettings = { membershipUserOverrides: ReturnType; }; +type LovartPromptTagVisibility = { + nanoBananaPro: boolean; + design: boolean; + branding: boolean; + illustration: boolean; + ecommerce: boolean; + video: boolean; +}; + +type ContactConfig = { + phone: string; + wechatQrUrl: string; +}; + +const defaultLovartPromptTagVisibility: LovartPromptTagVisibility = { + nanoBananaPro: true, + design: true, + branding: true, + illustration: true, + ecommerce: true, + video: true, +}; + +const defaultContactConfig: ContactConfig = { + phone: "", + wechatQrUrl: "", +}; + +const lovartPromptTagOptions: Array<{ + key: keyof LovartPromptTagVisibility; + label: string; +}> = [ + { key: "nanoBananaPro", label: "Nano Banana Pro" }, + { key: "design", label: "Design" }, + { key: "branding", label: "Branding" }, + { key: "illustration", label: "Illustration" }, + { key: "ecommerce", label: "E-Commerce" }, + { key: "video", label: "Video" }, +]; + type NavLink = { id: string; name: string; @@ -442,6 +484,8 @@ export function AdminSettingsClient() { "帮我把这张产品图做成高级电商海报", "基于参考图延展 3 套同风格方案", ], + lovartPromptTagVisibility: defaultLovartPromptTagVisibility, + contactConfig: defaultContactConfig, pointsSystemConfig: defaultPointsSystemConfig, pointsPackages: defaultPointsPackages, dulupayConfig: defaultDulupayConfig, @@ -513,6 +557,20 @@ export function AdminSettingsClient() { "帮我把这张产品图做成高级电商海报", "基于参考图延展 3 套同风格方案", ], + lovartPromptTagVisibility: { + nanoBananaPro: + (siteSettingsData as any).lovartPromptTagVisibility?.nanoBananaPro !== false, + design: (siteSettingsData as any).lovartPromptTagVisibility?.design !== false, + branding: (siteSettingsData as any).lovartPromptTagVisibility?.branding !== false, + illustration: + (siteSettingsData as any).lovartPromptTagVisibility?.illustration !== false, + ecommerce: (siteSettingsData as any).lovartPromptTagVisibility?.ecommerce !== false, + video: (siteSettingsData as any).lovartPromptTagVisibility?.video !== false, + }, + contactConfig: { + phone: String((siteSettingsData as any).contactConfig?.phone || "").trim(), + wechatQrUrl: String((siteSettingsData as any).contactConfig?.wechatQrUrl || "").trim(), + }, pointsSystemConfig: normalizedPointsSystemConfig, pointsPackages: normalizedPointsPackages, dulupayConfig: normalizedDulupayConfig, @@ -932,6 +990,7 @@ export function AdminSettingsClient() { const [showModelDialog, setShowModelDialog] = useState(false); const [editingModel, setEditingModel] = useState(null); const [modelAvatarUploading, setModelAvatarUploading] = useState(false); + const [contactQrUploading, setContactQrUploading] = useState(false); // 🆕 火山引擎视频多模型表单(动态从API加载) const [volcanoVideoModels, setVolcanoVideoModels] = useState { + if (!file.type.startsWith("image/")) { + toast.error("请上传图片文件"); + return; + } + + try { + setContactQrUploading(true); + const formData = new FormData(); + formData.append("file", file); + + const response = await fetch("/api/files/upload", { + method: "POST", + body: formData, + }); + const result = await response.json(); + + if (!response.ok || !result?.url) { + throw new Error(result?.error || "上传失败"); + } + + setSiteSettings((prev) => ({ + ...prev, + contactConfig: { + ...prev.contactConfig, + wechatQrUrl: result.url, + }, + })); + toast.success("微信二维码上传成功"); + } catch (error: any) { + toast.error(error?.message || "微信二维码上传失败"); + } finally { + setContactQrUploading(false); + } + }; + const handleSaveModel = async () => { try { // 🆕 检查是否是火山引擎视频Provider @@ -3168,6 +3263,113 @@ export function AdminSettingsClient() { 仅配置动态尾文案;前端会固定显示前缀后再进行打字与删字切换。支持最多 20 条。 +
+
对话框下方标签按钮显示
+
+ {lovartPromptTagOptions.map((item) => ( +
+ + + setSiteSettings({ + ...siteSettings, + lovartPromptTagVisibility: { + ...siteSettings.lovartPromptTagVisibility, + [item.key]: checked, + }, + }) + } + /> +
+ ))} +
+
+ 关闭后对应标签按钮会在新版首页对话框下方隐藏。 +
+
+
+
联系我们配置
+
+ + + setSiteSettings({ + ...siteSettings, + contactConfig: { + ...siteSettings.contactConfig, + phone: e.target.value.slice(0, 64), + }, + }) + } + placeholder="例如:400-888-8888" + /> +
+
+ + + setSiteSettings({ + ...siteSettings, + contactConfig: { + ...siteSettings.contactConfig, + wechatQrUrl: e.target.value, + }, + }) + } + placeholder="https://example.com/wechat-qr.png" + /> +
+ + { + const file = event.target.files?.[0]; + if (file) { + await handleContactQrUpload(file); + } + event.target.value = ""; + }} + /> +
+ {siteSettings.contactConfig.wechatQrUrl ? ( +
+ 微信二维码预览 +
+ ) : null} +
+
+ 新版首页用户菜单点击“联系我们”后会弹出该二维码和联系电话。 +
+
) + : {}; + + return { + nanoBananaPro: raw.nanoBananaPro !== false, + design: raw.design !== false, + branding: raw.branding !== false, + illustration: raw.illustration !== false, + ecommerce: raw.ecommerce !== false, + video: raw.video !== false, + }; +} + +function normalizeContactConfig(value: unknown): { + phone: string; + wechatQrUrl: string; +} { + const raw = + value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; + + const phone = String(raw.phone || "").trim().slice(0, 64); + const wechatQrUrl = String(raw.wechatQrUrl || "").trim().slice(0, 1000); + + return { + phone, + wechatQrUrl, + }; +} + // GET /api/site-settings - 获取网站设置 (公开) export async function GET() { try { @@ -102,6 +145,8 @@ export async function GET() { aiDetailGeneratorEnabledSetting, lovartComposerPreviewTextsSetting, lovartComposerPrefixSetting, + lovartPromptTagVisibilitySetting, + contactConfigSetting, membershipConfigSetting, membershipOverridesSetting, pointsSystemConfigSetting, @@ -145,6 +190,18 @@ export async function GET() { .where(eq(systemSettings.key, LOVART_COMPOSER_PREFIX_KEY)) .limit(1) .then(rows => rows[0]), + db + .select() + .from(systemSettings) + .where(eq(systemSettings.key, LOVART_PROMPT_TAG_VISIBILITY_KEY)) + .limit(1) + .then(rows => rows[0]), + db + .select() + .from(systemSettings) + .where(eq(systemSettings.key, CONTACT_CONFIG_KEY)) + .limit(1) + .then(rows => rows[0]), db .select() .from(systemSettings) @@ -187,6 +244,10 @@ export async function GET() { const lovartComposerPrefix = normalizeLovartComposerPrefix( lovartComposerPrefixSetting?.value ); + const lovartPromptTagVisibility = normalizeLovartPromptTagVisibility( + lovartPromptTagVisibilitySetting?.value + ); + const contactConfig = normalizeContactConfig(contactConfigSetting?.value); const membershipConfig = normalizeMembershipConfig(membershipConfigSetting?.value); const membershipUserOverrides = normalizeMembershipUserOverrides( membershipOverridesSetting?.value @@ -233,6 +294,8 @@ export async function GET() { aiDetailGenerationEnabled, lovartComposerPrefix, lovartComposerPreviewTexts, + lovartPromptTagVisibility, + contactConfig, membershipConfig, membershipUserOverrides, pointsSystemConfig, @@ -254,6 +317,8 @@ export async function GET() { aiDetailGenerationEnabled, lovartComposerPrefix, lovartComposerPreviewTexts, + lovartPromptTagVisibility, + contactConfig, membershipConfig, membershipUserOverrides, pointsSystemConfig, @@ -309,6 +374,22 @@ const updateSchema = z.object({ 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(), + lovartPromptTagVisibility: z + .object({ + nanoBananaPro: z.boolean().optional(), + design: z.boolean().optional(), + branding: z.boolean().optional(), + illustration: z.boolean().optional(), + ecommerce: z.boolean().optional(), + video: z.boolean().optional(), + }) + .optional(), + contactConfig: z + .object({ + phone: z.string().max(64).optional(), + wechatQrUrl: z.string().max(1000).optional(), + }) + .optional(), membershipConfig: z.unknown().optional(), membershipUserOverrides: z.unknown().optional(), pointsSystemConfig: z.unknown().optional(), @@ -337,6 +418,8 @@ export async function PUT(request: NextRequest) { aiDetailGenerationEnabled, lovartComposerPrefix, lovartComposerPreviewTexts, + lovartPromptTagVisibility, + contactConfig, membershipConfig, membershipUserOverrides, pointsSystemConfig, @@ -458,6 +541,46 @@ export async function PUT(request: NextRequest) { }); } + if (lovartPromptTagVisibility !== undefined) { + const normalized = normalizeLovartPromptTagVisibility(lovartPromptTagVisibility); + await db + .insert(systemSettings) + .values({ + key: LOVART_PROMPT_TAG_VISIBILITY_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 (contactConfig !== undefined) { + const normalized = normalizeContactConfig(contactConfig); + await db + .insert(systemSettings) + .values({ + key: CONTACT_CONFIG_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 @@ -574,6 +697,14 @@ export async function PUT(request: NextRequest) { lovartComposerPreviewTexts !== undefined ? normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts) : normalizeLovartComposerPreviewTexts(undefined), + lovartPromptTagVisibility: + lovartPromptTagVisibility !== undefined + ? normalizeLovartPromptTagVisibility(lovartPromptTagVisibility) + : normalizeLovartPromptTagVisibility(undefined), + contactConfig: + contactConfig !== undefined + ? normalizeContactConfig(contactConfig) + : normalizeContactConfig(undefined), membershipConfig: membershipConfig !== undefined ? normalizeMembershipConfig(membershipConfig) @@ -731,6 +862,46 @@ export async function PUT(request: NextRequest) { }); } + if (lovartPromptTagVisibility !== undefined) { + const normalized = normalizeLovartPromptTagVisibility(lovartPromptTagVisibility); + await db + .insert(systemSettings) + .values({ + key: LOVART_PROMPT_TAG_VISIBILITY_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 (contactConfig !== undefined) { + const normalized = normalizeContactConfig(contactConfig); + await db + .insert(systemSettings) + .values({ + key: CONTACT_CONFIG_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 @@ -847,6 +1018,14 @@ export async function PUT(request: NextRequest) { lovartComposerPreviewTexts !== undefined ? normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts) : normalizeLovartComposerPreviewTexts(undefined), + lovartPromptTagVisibility: + lovartPromptTagVisibility !== undefined + ? normalizeLovartPromptTagVisibility(lovartPromptTagVisibility) + : normalizeLovartPromptTagVisibility(undefined), + contactConfig: + contactConfig !== undefined + ? normalizeContactConfig(contactConfig) + : normalizeContactConfig(undefined), membershipConfig: membershipConfig !== undefined ? normalizeMembershipConfig(membershipConfig) diff --git a/components/home/lovart-home.tsx b/components/home/lovart-home.tsx index e77979f6d..ddc5c56e0 100644 --- a/components/home/lovart-home.tsx +++ b/components/home/lovart-home.tsx @@ -123,6 +123,11 @@ type PublicAuthSettings = { isGoogleConfigured?: boolean; }; +type ContactConfig = { + phone?: string; + wechatQrUrl?: string; +}; + type MembershipSummaryResponse = { config: MembershipConfig; currentPlanId: string; @@ -213,13 +218,16 @@ const DISCOVERY_DEFAULT_TABS = [ ]; const PROMPT_TAGS = [ - { label: "Nano Banana Pro", icon: Sparkles, highlighted: true }, - { label: "Design", icon: ImageIcon, highlighted: false }, - { label: "Branding", icon: Star, highlighted: false }, - { label: "Illustration", icon: Palette, highlighted: false }, - { label: "E-Commerce", icon: Monitor, highlighted: false }, - { label: "Video", icon: Clapperboard, highlighted: false }, -]; + { key: "nanoBananaPro", label: "Nano Banana Pro", icon: Sparkles, highlighted: true }, + { key: "design", label: "Design", icon: ImageIcon, highlighted: false }, + { key: "branding", label: "Branding", icon: Star, highlighted: false }, + { key: "illustration", label: "Illustration", icon: Palette, highlighted: false }, + { key: "ecommerce", label: "E-Commerce", icon: Monitor, highlighted: false }, + { key: "video", label: "Video", icon: Clapperboard, highlighted: false }, +] as const; + +type LovartPromptTagKey = (typeof PROMPT_TAGS)[number]["key"]; +type LovartPromptTagVisibility = Record; const MASONRY_HEIGHTS = [420, 290, 340, 470, 300, 360, 430, 320]; const FEATURED_PAGE_SIZE = 24; @@ -338,6 +346,7 @@ export function LovartHome({ const [editorImageUrl, setEditorImageUrl] = useState(""); const [tutorialDialogOpen, setTutorialDialogOpen] = useState(false); const [currentTutorialVideo, setCurrentTutorialVideo] = useState(null); + const [contactDialogOpen, setContactDialogOpen] = useState(false); const [typedPreviewText, setTypedPreviewText] = useState(""); const [previewCursorVisible, setPreviewCursorVisible] = useState(true); const projectSectionRef = useRef(null); @@ -380,6 +389,12 @@ export function LovartHome({ const { data: modelsData } = useSWR<{ models: ModelOption[]; + agentModel?: { + id: string; + displayName: string; + avatarUrl?: string | null; + providerType: string; + } | null; }>(isAuthenticated ? "/api/models" : null, fetcher); const { data: navLinksData } = useSWR<{ links: NavLink[] }>( @@ -391,6 +406,8 @@ export function LovartHome({ canvasDefaultModelId?: string | null; lovartComposerPrefix?: string | null; lovartComposerPreviewTexts?: string[]; + lovartPromptTagVisibility?: Partial; + contactConfig?: ContactConfig; membershipConfig?: { enabled?: boolean; }; @@ -437,18 +454,21 @@ export function LovartHome({ [modelsData?.models] ); + const autoComposerModel = useMemo( + () => ({ + id: "auto", + modelId: "auto", + displayName: "Agent模式", + providerType: "auto", + supportsImageGen: true, + avatarUrl: modelsData?.agentModel?.avatarUrl || null, + }), + [modelsData?.agentModel?.avatarUrl] + ); + const allComposerModels = useMemo( - () => [ - { - id: "auto", - modelId: "auto", - displayName: "Agent模式", - providerType: "auto", - supportsImageGen: true, - }, - ...imageModels, - ], - [imageModels] + () => [autoComposerModel, ...imageModels], + [autoComposerModel, imageModels] ); const composerModelValue = useMemo(() => { @@ -516,6 +536,31 @@ export function LovartHome({ .filter(Boolean); }, [composerFixedPrefix, composerPreviewTexts]); + const lovartPromptTagVisibility = useMemo(() => { + const raw = siteSettingsData?.lovartPromptTagVisibility || {}; + return { + nanoBananaPro: raw.nanoBananaPro !== false, + design: raw.design !== false, + branding: raw.branding !== false, + illustration: raw.illustration !== false, + ecommerce: raw.ecommerce !== false, + video: raw.video !== false, + }; + }, [siteSettingsData?.lovartPromptTagVisibility]); + + const visiblePromptTags = useMemo( + () => PROMPT_TAGS.filter((tag) => lovartPromptTagVisibility[tag.key]), + [lovartPromptTagVisibility] + ); + + const contactConfig = useMemo( + () => ({ + phone: String(siteSettingsData?.contactConfig?.phone || "").trim(), + wechatQrUrl: String(siteSettingsData?.contactConfig?.wechatQrUrl || "").trim(), + }), + [siteSettingsData?.contactConfig?.phone, siteSettingsData?.contactConfig?.wechatQrUrl] + ); + const loadedFeaturedImages = useMemo(() => { return (featuredPages || []).flatMap((page) => page.images || []); }, [featuredPages]); @@ -1276,7 +1321,7 @@ export function LovartHome({ toast.info("联系我们后续开放")} + onClick={() => setContactDialogOpen(true)} />
- {[{ id: "auto", displayName: "Agent模式", avatarUrl: null }, ...imageModels].map((model) => ( + {allComposerModels.map((model) => (
@@ -1672,7 +1718,7 @@ export function LovartHome({
- {PROMPT_TAGS.map((tag) => ( + {visiblePromptTags.map((tag) => ( - - - {imageCountOptions.map((count) => ( - updateParam("imageCount", count)} - className="focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0" - > - - {count}张 - - ))} - - + {!hideImageCount && ( + + + + + + {imageCountOptions.map((count) => ( + updateParam("imageCount", count)} + className="focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0" + > + + {count}张 + + ))} + + + )} {/* 🆕 阿里生图专用:提示词优化 & 反向提示词 */} {(showPromptExtend || showNegativePrompt) && (