新版首页文案可配置并部署侧边栏管理入口
This commit is contained in:
parent
f2c6002969
commit
bc5838f9e1
@ -176,6 +176,11 @@ type SiteSettings = {
|
|||||||
canvasEnabled: boolean;
|
canvasEnabled: boolean;
|
||||||
canvasDefaultModelId: string | null;
|
canvasDefaultModelId: string | null;
|
||||||
canvasAutoModeDefaultModelId: string | null;
|
canvasAutoModeDefaultModelId: string | null;
|
||||||
|
lovartComposerPreviewTexts: string[];
|
||||||
|
lovartPromoText: string;
|
||||||
|
lovartPromoActionText: string;
|
||||||
|
lovartHeroTitle: string;
|
||||||
|
lovartHeroSubtitle: string;
|
||||||
homeVariant: "classic" | "lovart";
|
homeVariant: "classic" | "lovart";
|
||||||
surpriseCategories: string[];
|
surpriseCategories: string[];
|
||||||
membershipConfig: ReturnType<typeof normalizeMembershipConfig>;
|
membershipConfig: ReturnType<typeof normalizeMembershipConfig>;
|
||||||
@ -399,6 +404,15 @@ export function AdminSettingsClient() {
|
|||||||
canvasEnabled: true,
|
canvasEnabled: true,
|
||||||
canvasDefaultModelId: null,
|
canvasDefaultModelId: null,
|
||||||
canvasAutoModeDefaultModelId: null,
|
canvasAutoModeDefaultModelId: null,
|
||||||
|
lovartComposerPreviewTexts: [
|
||||||
|
"让 Lovart 打造引人注目的社交媒体视觉",
|
||||||
|
"帮我把这张产品图做成高级电商海报",
|
||||||
|
"基于参考图延展 3 套同风格方案",
|
||||||
|
],
|
||||||
|
lovartPromoText: "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠",
|
||||||
|
lovartPromoActionText: "立即升级",
|
||||||
|
lovartHeroTitle: "由壹点AI 让设计更简单",
|
||||||
|
lovartHeroSubtitle: "懂你的设计代理,帮你搞定一切",
|
||||||
homeVariant: "classic",
|
homeVariant: "classic",
|
||||||
surpriseCategories: ["全部", "惊喜"],
|
surpriseCategories: ["全部", "惊喜"],
|
||||||
membershipConfig: defaultMembershipConfig,
|
membershipConfig: defaultMembershipConfig,
|
||||||
@ -444,6 +458,21 @@ export function AdminSettingsClient() {
|
|||||||
canvasEnabled: siteSettingsData.canvasEnabled ?? true,
|
canvasEnabled: siteSettingsData.canvasEnabled ?? true,
|
||||||
canvasDefaultModelId: siteSettingsData.canvasDefaultModelId || null,
|
canvasDefaultModelId: siteSettingsData.canvasDefaultModelId || null,
|
||||||
canvasAutoModeDefaultModelId: siteSettingsData.canvasAutoModeDefaultModelId || null,
|
canvasAutoModeDefaultModelId: siteSettingsData.canvasAutoModeDefaultModelId || null,
|
||||||
|
lovartComposerPreviewTexts: Array.isArray(siteSettingsData.lovartComposerPreviewTexts)
|
||||||
|
? siteSettingsData.lovartComposerPreviewTexts
|
||||||
|
.map((item: any) => String(item).trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [
|
||||||
|
"让 Lovart 打造引人注目的社交媒体视觉",
|
||||||
|
"帮我把这张产品图做成高级电商海报",
|
||||||
|
"基于参考图延展 3 套同风格方案",
|
||||||
|
],
|
||||||
|
lovartPromoText:
|
||||||
|
siteSettingsData.lovartPromoText ||
|
||||||
|
"现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠",
|
||||||
|
lovartPromoActionText: siteSettingsData.lovartPromoActionText || "立即升级",
|
||||||
|
lovartHeroTitle: siteSettingsData.lovartHeroTitle || "由壹点AI 让设计更简单",
|
||||||
|
lovartHeroSubtitle: siteSettingsData.lovartHeroSubtitle || "懂你的设计代理,帮你搞定一切",
|
||||||
homeVariant: siteSettingsData.homeVariant === "lovart" ? "lovart" : "classic",
|
homeVariant: siteSettingsData.homeVariant === "lovart" ? "lovart" : "classic",
|
||||||
surpriseCategories: Array.isArray(siteSettingsData.surpriseCategories)
|
surpriseCategories: Array.isArray(siteSettingsData.surpriseCategories)
|
||||||
? siteSettingsData.surpriseCategories.map((item: any) => String(item).trim()).filter(Boolean)
|
? siteSettingsData.surpriseCategories.map((item: any) => String(item).trim()).filter(Boolean)
|
||||||
@ -2668,6 +2697,86 @@ export function AdminSettingsClient() {
|
|||||||
新版首页“精选”模块会按这些分类显示筛选标签。
|
新版首页“精选”模块会按这些分类显示筛选标签。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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="lovartComposerPreviewTexts">
|
||||||
|
对话框预览文案(每行一条,按顺序轮播打字显示)
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="lovartComposerPreviewTexts"
|
||||||
|
value={siteSettings.lovartComposerPreviewTexts.join("\n")}
|
||||||
|
onChange={(e) => {
|
||||||
|
const values = e.target.value
|
||||||
|
.split("\n")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 20);
|
||||||
|
|
||||||
|
setSiteSettings({
|
||||||
|
...siteSettings,
|
||||||
|
lovartComposerPreviewTexts:
|
||||||
|
values.length > 0
|
||||||
|
? values
|
||||||
|
: [
|
||||||
|
"让 Lovart 打造引人注目的社交媒体视觉",
|
||||||
|
"帮我把这张产品图做成高级电商海报",
|
||||||
|
"基于参考图延展 3 套同风格方案",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
rows={5}
|
||||||
|
placeholder={"让 Lovart 打造引人注目的社交媒体视觉\n帮我把这张产品图做成高级电商海报"}
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
前端输入框将按此顺序循环打字显示,支持最多 20 条。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="lovartPromoText">顶部升级提示文案</Label>
|
||||||
|
<Input
|
||||||
|
id="lovartPromoText"
|
||||||
|
value={siteSettings.lovartPromoText}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSiteSettings({ ...siteSettings, lovartPromoText: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="lovartPromoActionText">顶部升级按钮文案</Label>
|
||||||
|
<Input
|
||||||
|
id="lovartPromoActionText"
|
||||||
|
value={siteSettings.lovartPromoActionText}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSiteSettings({ ...siteSettings, lovartPromoActionText: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="立即升级"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="lovartHeroTitle">首页主标题</Label>
|
||||||
|
<Input
|
||||||
|
id="lovartHeroTitle"
|
||||||
|
value={siteSettings.lovartHeroTitle}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSiteSettings({ ...siteSettings, lovartHeroTitle: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="由壹点AI 让设计更简单"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="lovartHeroSubtitle">首页副标题</Label>
|
||||||
|
<Input
|
||||||
|
id="lovartHeroSubtitle"
|
||||||
|
value={siteSettings.lovartHeroSubtitle}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSiteSettings({ ...siteSettings, lovartHeroSubtitle: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="懂你的设计代理,帮你搞定一切"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="border rounded-lg p-4 space-y-3">
|
<div className="border rounded-lg p-4 space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -53,10 +53,15 @@ async function NewChatPage() {
|
|||||||
id: session.user.id,
|
id: session.user.id,
|
||||||
email: session.user.email,
|
email: session.user.email,
|
||||||
name: session.user.name,
|
name: session.user.name,
|
||||||
|
type: session.user.type,
|
||||||
}}
|
}}
|
||||||
siteName={siteSettings.siteName || "AI Chatbot"}
|
siteName={siteSettings.siteName || "AI Chatbot"}
|
||||||
siteLogo={siteSettings.siteLogo || siteSettings.siteLogoDark || ""}
|
siteLogo={siteSettings.siteLogo || siteSettings.siteLogoDark || ""}
|
||||||
surpriseCategories={surpriseCategories}
|
surpriseCategories={surpriseCategories}
|
||||||
|
heroPromoText={siteSettings.lovartPromoText}
|
||||||
|
heroPromoActionText={siteSettings.lovartPromoActionText}
|
||||||
|
heroTitleText={siteSettings.lovartHeroTitle}
|
||||||
|
heroSubtitleText={siteSettings.lovartHeroSubtitle}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
|
|
||||||
const HOME_VARIANT_KEY = "home_variant";
|
const HOME_VARIANT_KEY = "home_variant";
|
||||||
const HOME_SURPRISE_CATEGORIES_KEY = "home_surprise_categories";
|
const HOME_SURPRISE_CATEGORIES_KEY = "home_surprise_categories";
|
||||||
|
const LOVART_COMPOSER_PREVIEW_TEXTS_KEY = "lovart_composer_preview_texts";
|
||||||
const MEMBERSHIP_CONFIG_KEY = "membership_config";
|
const MEMBERSHIP_CONFIG_KEY = "membership_config";
|
||||||
const MEMBERSHIP_USER_OVERRIDES_KEY = "membership_user_plan_overrides";
|
const MEMBERSHIP_USER_OVERRIDES_KEY = "membership_user_plan_overrides";
|
||||||
|
|
||||||
@ -42,6 +43,25 @@ function normalizeSurpriseCategories(value: unknown): string[] {
|
|||||||
return unique;
|
return unique;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeLovartComposerPreviewTexts(value: unknown): string[] {
|
||||||
|
const defaults = [
|
||||||
|
"让 Lovart 打造引人注目的社交媒体视觉",
|
||||||
|
"帮我把这张产品图做成高级电商海报",
|
||||||
|
"基于参考图延展 3 套同风格方案",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = value
|
||||||
|
.map((item) => String(item || "").trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 20);
|
||||||
|
|
||||||
|
return normalized.length > 0 ? normalized : defaults;
|
||||||
|
}
|
||||||
|
|
||||||
// GET /api/site-settings - 获取网站设置 (公开)
|
// GET /api/site-settings - 获取网站设置 (公开)
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@ -50,6 +70,7 @@ export async function GET() {
|
|||||||
notificationSetting,
|
notificationSetting,
|
||||||
homeVariantSetting,
|
homeVariantSetting,
|
||||||
surpriseCategoriesSetting,
|
surpriseCategoriesSetting,
|
||||||
|
lovartComposerPreviewTextsSetting,
|
||||||
membershipConfigSetting,
|
membershipConfigSetting,
|
||||||
membershipOverridesSetting,
|
membershipOverridesSetting,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
@ -72,6 +93,12 @@ export async function GET() {
|
|||||||
.where(eq(systemSettings.key, HOME_SURPRISE_CATEGORIES_KEY))
|
.where(eq(systemSettings.key, HOME_SURPRISE_CATEGORIES_KEY))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.then(rows => rows[0]),
|
.then(rows => rows[0]),
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(systemSettings)
|
||||||
|
.where(eq(systemSettings.key, LOVART_COMPOSER_PREVIEW_TEXTS_KEY))
|
||||||
|
.limit(1)
|
||||||
|
.then(rows => rows[0]),
|
||||||
db
|
db
|
||||||
.select()
|
.select()
|
||||||
.from(systemSettings)
|
.from(systemSettings)
|
||||||
@ -89,6 +116,9 @@ export async function GET() {
|
|||||||
const notification = notificationSetting?.value || { enabled: false, title: "", content: "" };
|
const notification = notificationSetting?.value || { enabled: false, title: "", content: "" };
|
||||||
const homeVariant = normalizeHomeVariant(homeVariantSetting?.value);
|
const homeVariant = normalizeHomeVariant(homeVariantSetting?.value);
|
||||||
const surpriseCategories = normalizeSurpriseCategories(surpriseCategoriesSetting?.value);
|
const surpriseCategories = normalizeSurpriseCategories(surpriseCategoriesSetting?.value);
|
||||||
|
const lovartComposerPreviewTexts = normalizeLovartComposerPreviewTexts(
|
||||||
|
lovartComposerPreviewTextsSetting?.value
|
||||||
|
);
|
||||||
const membershipConfig = normalizeMembershipConfig(membershipConfigSetting?.value);
|
const membershipConfig = normalizeMembershipConfig(membershipConfigSetting?.value);
|
||||||
const membershipUserOverrides = normalizeMembershipUserOverrides(
|
const membershipUserOverrides = normalizeMembershipUserOverrides(
|
||||||
membershipOverridesSetting?.value
|
membershipOverridesSetting?.value
|
||||||
@ -119,17 +149,29 @@ export async function GET() {
|
|||||||
canvasEnabled: true,
|
canvasEnabled: true,
|
||||||
canvasDefaultModelId: null,
|
canvasDefaultModelId: null,
|
||||||
canvasAutoModeDefaultModelId: null,
|
canvasAutoModeDefaultModelId: null,
|
||||||
|
lovartPromoText: "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠",
|
||||||
|
lovartPromoActionText: "立即升级",
|
||||||
|
lovartHeroTitle: "由壹点AI 让设计更简单",
|
||||||
|
lovartHeroSubtitle: "懂你的设计代理,帮你搞定一切",
|
||||||
homeVariant,
|
homeVariant,
|
||||||
surpriseCategories,
|
surpriseCategories,
|
||||||
|
lovartComposerPreviewTexts,
|
||||||
membershipConfig,
|
membershipConfig,
|
||||||
membershipUserOverrides,
|
membershipUserOverrides,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = NextResponse.json({
|
const response = NextResponse.json({
|
||||||
...baseSettings,
|
...baseSettings,
|
||||||
|
lovartPromoText:
|
||||||
|
baseSettings.lovartPromoText ||
|
||||||
|
"现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠",
|
||||||
|
lovartPromoActionText: baseSettings.lovartPromoActionText || "立即升级",
|
||||||
|
lovartHeroTitle: baseSettings.lovartHeroTitle || "由壹点AI 让设计更简单",
|
||||||
|
lovartHeroSubtitle: baseSettings.lovartHeroSubtitle || "懂你的设计代理,帮你搞定一切",
|
||||||
notification,
|
notification,
|
||||||
homeVariant,
|
homeVariant,
|
||||||
surpriseCategories,
|
surpriseCategories,
|
||||||
|
lovartComposerPreviewTexts,
|
||||||
membershipConfig,
|
membershipConfig,
|
||||||
membershipUserOverrides,
|
membershipUserOverrides,
|
||||||
});
|
});
|
||||||
@ -173,8 +215,13 @@ const updateSchema = z.object({
|
|||||||
canvasEnabled: z.boolean().optional(),
|
canvasEnabled: z.boolean().optional(),
|
||||||
canvasDefaultModelId: z.string().nullable().optional(),
|
canvasDefaultModelId: z.string().nullable().optional(),
|
||||||
canvasAutoModeDefaultModelId: z.string().nullable().optional(),
|
canvasAutoModeDefaultModelId: z.string().nullable().optional(),
|
||||||
|
lovartPromoText: z.string().max(300).optional(),
|
||||||
|
lovartPromoActionText: z.string().max(32).optional(),
|
||||||
|
lovartHeroTitle: z.string().max(300).optional(),
|
||||||
|
lovartHeroSubtitle: z.string().max(300).optional(),
|
||||||
homeVariant: z.enum(["classic", "lovart"]).optional(),
|
homeVariant: z.enum(["classic", "lovart"]).optional(),
|
||||||
surpriseCategories: z.array(z.string().trim().min(1).max(20)).max(12).optional(),
|
surpriseCategories: z.array(z.string().trim().min(1).max(20)).max(12).optional(),
|
||||||
|
lovartComposerPreviewTexts: z.array(z.string().trim().min(1).max(120)).max(20).optional(),
|
||||||
membershipConfig: z.unknown().optional(),
|
membershipConfig: z.unknown().optional(),
|
||||||
membershipUserOverrides: z.unknown().optional(),
|
membershipUserOverrides: z.unknown().optional(),
|
||||||
});
|
});
|
||||||
@ -197,6 +244,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
const {
|
const {
|
||||||
homeVariant,
|
homeVariant,
|
||||||
surpriseCategories,
|
surpriseCategories,
|
||||||
|
lovartComposerPreviewTexts,
|
||||||
membershipConfig,
|
membershipConfig,
|
||||||
membershipUserOverrides,
|
membershipUserOverrides,
|
||||||
...siteSettingsPayload
|
...siteSettingsPayload
|
||||||
@ -256,6 +304,26 @@ export async function PUT(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lovartComposerPreviewTexts !== undefined) {
|
||||||
|
const normalized = normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts);
|
||||||
|
await db
|
||||||
|
.insert(systemSettings)
|
||||||
|
.values({
|
||||||
|
key: LOVART_COMPOSER_PREVIEW_TEXTS_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) {
|
if (membershipConfig !== undefined) {
|
||||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||||
await db
|
await db
|
||||||
@ -303,6 +371,10 @@ export async function PUT(request: NextRequest) {
|
|||||||
surpriseCategories !== undefined
|
surpriseCategories !== undefined
|
||||||
? normalizeSurpriseCategories(surpriseCategories)
|
? normalizeSurpriseCategories(surpriseCategories)
|
||||||
: normalizeSurpriseCategories(undefined),
|
: normalizeSurpriseCategories(undefined),
|
||||||
|
lovartComposerPreviewTexts:
|
||||||
|
lovartComposerPreviewTexts !== undefined
|
||||||
|
? normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts)
|
||||||
|
: normalizeLovartComposerPreviewTexts(undefined),
|
||||||
membershipConfig:
|
membershipConfig:
|
||||||
membershipConfig !== undefined
|
membershipConfig !== undefined
|
||||||
? normalizeMembershipConfig(membershipConfig)
|
? normalizeMembershipConfig(membershipConfig)
|
||||||
@ -340,6 +412,12 @@ export async function PUT(request: NextRequest) {
|
|||||||
canvasEnabled: siteSettingsPayload.canvasEnabled ?? true,
|
canvasEnabled: siteSettingsPayload.canvasEnabled ?? true,
|
||||||
canvasDefaultModelId: siteSettingsPayload.canvasDefaultModelId,
|
canvasDefaultModelId: siteSettingsPayload.canvasDefaultModelId,
|
||||||
canvasAutoModeDefaultModelId: siteSettingsPayload.canvasAutoModeDefaultModelId,
|
canvasAutoModeDefaultModelId: siteSettingsPayload.canvasAutoModeDefaultModelId,
|
||||||
|
lovartPromoText:
|
||||||
|
siteSettingsPayload.lovartPromoText ??
|
||||||
|
"现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠",
|
||||||
|
lovartPromoActionText: siteSettingsPayload.lovartPromoActionText ?? "立即升级",
|
||||||
|
lovartHeroTitle: siteSettingsPayload.lovartHeroTitle ?? "由壹点AI 让设计更简单",
|
||||||
|
lovartHeroSubtitle: siteSettingsPayload.lovartHeroSubtitle ?? "懂你的设计代理,帮你搞定一切",
|
||||||
updatedBy: session.user.id,
|
updatedBy: session.user.id,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
@ -383,6 +461,26 @@ export async function PUT(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lovartComposerPreviewTexts !== undefined) {
|
||||||
|
const normalized = normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts);
|
||||||
|
await db
|
||||||
|
.insert(systemSettings)
|
||||||
|
.values({
|
||||||
|
key: LOVART_COMPOSER_PREVIEW_TEXTS_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) {
|
if (membershipConfig !== undefined) {
|
||||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||||
await db
|
await db
|
||||||
@ -430,6 +528,10 @@ export async function PUT(request: NextRequest) {
|
|||||||
surpriseCategories !== undefined
|
surpriseCategories !== undefined
|
||||||
? normalizeSurpriseCategories(surpriseCategories)
|
? normalizeSurpriseCategories(surpriseCategories)
|
||||||
: normalizeSurpriseCategories(undefined),
|
: normalizeSurpriseCategories(undefined),
|
||||||
|
lovartComposerPreviewTexts:
|
||||||
|
lovartComposerPreviewTexts !== undefined
|
||||||
|
? normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts)
|
||||||
|
: normalizeLovartComposerPreviewTexts(undefined),
|
||||||
membershipConfig:
|
membershipConfig:
|
||||||
membershipConfig !== undefined
|
membershipConfig !== undefined
|
||||||
? normalizeMembershipConfig(membershipConfig)
|
? normalizeMembershipConfig(membershipConfig)
|
||||||
|
|||||||
@ -479,6 +479,17 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
setAttachments(normalizedAttachments as PickedObjectAttachment[]);
|
setAttachments(normalizedAttachments as PickedObjectAttachment[]);
|
||||||
setInput(normalizedPrompt);
|
setInput(normalizedPrompt);
|
||||||
|
|
||||||
|
if (parsed.selectedModelId) {
|
||||||
|
setSelectedModelId(String(parsed.selectedModelId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.imageParams && typeof parsed.imageParams === "object") {
|
||||||
|
setImageParams((prev) => ({
|
||||||
|
...prev,
|
||||||
|
...parsed.imageParams,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
normalizedAttachments.forEach((attachment, index) => {
|
normalizedAttachments.forEach((attachment, index) => {
|
||||||
insertImageTagInEditor(attachment.url, attachment.name, index);
|
insertImageTagInEditor(attachment.url, attachment.name, index);
|
||||||
@ -491,6 +502,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
attachments: normalizedAttachments,
|
attachments: normalizedAttachments,
|
||||||
autoSend: true,
|
autoSend: true,
|
||||||
createdAt: parsed.createdAt,
|
createdAt: parsed.createdAt,
|
||||||
|
selectedModelId: parsed.selectedModelId,
|
||||||
|
imageParams: parsed.imageParams,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import useSWRInfinite from "swr/infinite";
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
ArrowUp,
|
ArrowUp,
|
||||||
@ -10,17 +11,16 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
Clapperboard,
|
Clapperboard,
|
||||||
Folder,
|
Folder,
|
||||||
Globe,
|
|
||||||
Heart,
|
Heart,
|
||||||
Hexagon,
|
|
||||||
Home,
|
Home,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
Info,
|
Info,
|
||||||
Lightbulb,
|
Loader2,
|
||||||
Monitor,
|
Monitor,
|
||||||
Palette,
|
Palette,
|
||||||
Plus,
|
Plus,
|
||||||
Send,
|
Send,
|
||||||
|
Settings,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Star,
|
Star,
|
||||||
Upload,
|
Upload,
|
||||||
@ -31,6 +31,8 @@ import { toast } from "sonner";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
||||||
|
import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { fetcher } from "@/lib/utils";
|
import { fetcher } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
@ -44,6 +46,7 @@ type HomeUser = {
|
|||||||
email?: string | null;
|
email?: string | null;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
image?: string | null;
|
image?: string | null;
|
||||||
|
type?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CanvasProject = {
|
type CanvasProject = {
|
||||||
@ -66,6 +69,24 @@ type FeaturedImage = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FeaturedResponse = {
|
||||||
|
images: FeaturedImage[];
|
||||||
|
pagination?: {
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModelOption = {
|
||||||
|
id: string;
|
||||||
|
modelId: string;
|
||||||
|
displayName: string;
|
||||||
|
providerType: string;
|
||||||
|
supportsImageGen?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type MembershipSummaryResponse = {
|
type MembershipSummaryResponse = {
|
||||||
config: MembershipConfig;
|
config: MembershipConfig;
|
||||||
currentPlanId: string;
|
currentPlanId: string;
|
||||||
@ -95,6 +116,10 @@ type LovartHomeProps = {
|
|||||||
siteName: string;
|
siteName: string;
|
||||||
siteLogo?: string | null;
|
siteLogo?: string | null;
|
||||||
surpriseCategories: string[];
|
surpriseCategories: string[];
|
||||||
|
heroPromoText?: string | null;
|
||||||
|
heroPromoActionText?: string | null;
|
||||||
|
heroTitleText?: string | null;
|
||||||
|
heroSubtitleText?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UploadFileItem = {
|
type UploadFileItem = {
|
||||||
@ -124,6 +149,7 @@ const PROMPT_TAGS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const MASONRY_HEIGHTS = [420, 290, 340, 470, 300, 360, 430, 320];
|
const MASONRY_HEIGHTS = [420, 290, 340, 470, 300, 360, 430, 320];
|
||||||
|
const FEATURED_PAGE_SIZE = 24;
|
||||||
|
|
||||||
function formatRelativeTime(value?: string) {
|
function formatRelativeTime(value?: string) {
|
||||||
if (!value) return "更新于今天";
|
if (!value) return "更新于今天";
|
||||||
@ -148,6 +174,10 @@ export function LovartHome({
|
|||||||
siteName,
|
siteName,
|
||||||
siteLogo,
|
siteLogo,
|
||||||
surpriseCategories,
|
surpriseCategories,
|
||||||
|
heroPromoText,
|
||||||
|
heroPromoActionText,
|
||||||
|
heroTitleText,
|
||||||
|
heroSubtitleText,
|
||||||
}: LovartHomeProps) {
|
}: LovartHomeProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [prompt, setPrompt] = useState("");
|
const [prompt, setPrompt] = useState("");
|
||||||
@ -157,7 +187,16 @@ export function LovartHome({
|
|||||||
const [showAllProjects, setShowAllProjects] = useState(false);
|
const [showAllProjects, setShowAllProjects] = useState(false);
|
||||||
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
|
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
|
||||||
const [billingCycle, setBillingCycle] = useState<"monthly" | "annual">("monthly");
|
const [billingCycle, setBillingCycle] = useState<"monthly" | "annual">("monthly");
|
||||||
|
const [selectedModelId, setSelectedModelId] = useState<string>("");
|
||||||
|
const [imageParams, setImageParams] = useState<ImageGenerationParams>({
|
||||||
|
aspectRatio: "auto",
|
||||||
|
imageSize: "2K",
|
||||||
|
imageCount: 1,
|
||||||
|
});
|
||||||
|
const [typedPreviewText, setTypedPreviewText] = useState("");
|
||||||
|
const [previewCursorVisible, setPreviewCursorVisible] = useState(true);
|
||||||
const projectSectionRef = useRef<HTMLDivElement | null>(null);
|
const projectSectionRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const featuredLoadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||||
const filesRef = useRef<UploadFileItem[]>([]);
|
const filesRef = useRef<UploadFileItem[]>([]);
|
||||||
|
|
||||||
const { data: canvasData, mutate: mutateCanvas } = useSWR<{
|
const { data: canvasData, mutate: mutateCanvas } = useSWR<{
|
||||||
@ -165,15 +204,39 @@ export function LovartHome({
|
|||||||
data: CanvasProject[];
|
data: CanvasProject[];
|
||||||
}>("/api/canvas", fetcher);
|
}>("/api/canvas", fetcher);
|
||||||
|
|
||||||
const { data: featuredData } = useSWR<{
|
const { data: featuredPages, size: featuredPageCount, setSize: setFeaturedPageCount, isValidating: isFeaturedValidating } =
|
||||||
images: FeaturedImage[];
|
useSWRInfinite<FeaturedResponse>(
|
||||||
}>("/api/featured?page=1&pageSize=40", fetcher);
|
(pageIndex, previousPageData) => {
|
||||||
|
if (
|
||||||
|
previousPageData?.pagination &&
|
||||||
|
previousPageData.pagination.page >= previousPageData.pagination.totalPages
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/api/featured?page=${pageIndex + 1}&pageSize=${FEATURED_PAGE_SIZE}`;
|
||||||
|
},
|
||||||
|
fetcher,
|
||||||
|
{
|
||||||
|
revalidateFirstPage: false,
|
||||||
|
parallel: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const { data: membershipData } = useSWR<MembershipSummaryResponse>(
|
const { data: membershipData } = useSWR<MembershipSummaryResponse>(
|
||||||
"/api/user/membership-summary",
|
"/api/user/membership-summary",
|
||||||
fetcher
|
fetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: modelsData } = useSWR<{
|
||||||
|
models: ModelOption[];
|
||||||
|
}>("/api/models", fetcher);
|
||||||
|
|
||||||
|
const { data: siteSettingsData } = useSWR<{
|
||||||
|
canvasDefaultModelId?: string | null;
|
||||||
|
lovartComposerPreviewTexts?: string[];
|
||||||
|
}>("/api/site-settings", fetcher);
|
||||||
|
|
||||||
const membershipConfig = membershipData?.config;
|
const membershipConfig = membershipData?.config;
|
||||||
const membershipPlans = membershipConfig?.plans || [];
|
const membershipPlans = membershipConfig?.plans || [];
|
||||||
const currentPlan = membershipData?.currentPlan;
|
const currentPlan = membershipData?.currentPlan;
|
||||||
@ -189,6 +252,30 @@ export function LovartHome({
|
|||||||
}, {});
|
}, {});
|
||||||
}, [membershipConfig?.modelRows]);
|
}, [membershipConfig?.modelRows]);
|
||||||
|
|
||||||
|
const imageModels = useMemo(
|
||||||
|
() => (modelsData?.models || []).filter((model) => model.supportsImageGen),
|
||||||
|
[modelsData?.models]
|
||||||
|
);
|
||||||
|
|
||||||
|
const allComposerModels = useMemo<ModelOption[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: "auto",
|
||||||
|
modelId: "auto",
|
||||||
|
displayName: "智能自动",
|
||||||
|
providerType: "auto",
|
||||||
|
supportsImageGen: true,
|
||||||
|
},
|
||||||
|
...imageModels,
|
||||||
|
],
|
||||||
|
[imageModels]
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentComposerModel = useMemo(
|
||||||
|
() => allComposerModels.find((model) => model.id === selectedModelId),
|
||||||
|
[allComposerModels, selectedModelId]
|
||||||
|
);
|
||||||
|
|
||||||
const discoveryTabs = useMemo(() => {
|
const discoveryTabs = useMemo(() => {
|
||||||
const merged = [
|
const merged = [
|
||||||
"全部",
|
"全部",
|
||||||
@ -198,8 +285,39 @@ export function LovartHome({
|
|||||||
return Array.from(new Set(merged)).slice(0, 12);
|
return Array.from(new Set(merged)).slice(0, 12);
|
||||||
}, [surpriseCategories]);
|
}, [surpriseCategories]);
|
||||||
|
|
||||||
|
const composerPreviewTexts = useMemo(() => {
|
||||||
|
const configured = (siteSettingsData?.lovartComposerPreviewTexts || [])
|
||||||
|
.map((item) => String(item || "").trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (configured.length > 0) {
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
"让 Lovart 打造引人注目的社交媒体视觉",
|
||||||
|
"帮我把这张产品图做成高级电商海报",
|
||||||
|
"基于参考图延展 3 套同风格方案",
|
||||||
|
];
|
||||||
|
}, [siteSettingsData?.lovartComposerPreviewTexts]);
|
||||||
|
|
||||||
|
const loadedFeaturedImages = useMemo(() => {
|
||||||
|
return (featuredPages || []).flatMap((page) => page.images || []);
|
||||||
|
}, [featuredPages]);
|
||||||
|
|
||||||
|
const featuredPagination = featuredPages?.[featuredPages.length - 1]?.pagination;
|
||||||
|
const hasMoreFeatured = Boolean(
|
||||||
|
featuredPagination && featuredPagination.page < featuredPagination.totalPages
|
||||||
|
);
|
||||||
|
const isLoadingFeatured = !featuredPages && isFeaturedValidating;
|
||||||
|
const isLoadingMoreFeatured =
|
||||||
|
isFeaturedValidating &&
|
||||||
|
!!featuredPages &&
|
||||||
|
featuredPageCount > 0 &&
|
||||||
|
featuredPageCount > featuredPages.length;
|
||||||
|
|
||||||
const featuredItems = useMemo(() => {
|
const featuredItems = useMemo(() => {
|
||||||
const images = featuredData?.images || [];
|
const images = loadedFeaturedImages;
|
||||||
if (activeCategory === "全部") {
|
if (activeCategory === "全部") {
|
||||||
return images;
|
return images;
|
||||||
}
|
}
|
||||||
@ -208,7 +326,7 @@ export function LovartHome({
|
|||||||
? String(item.featuredCategory) === activeCategory
|
? String(item.featuredCategory) === activeCategory
|
||||||
: String(item.prompt || "").toLowerCase().includes(activeCategory.toLowerCase())
|
: String(item.prompt || "").toLowerCase().includes(activeCategory.toLowerCase())
|
||||||
);
|
);
|
||||||
}, [featuredData?.images, activeCategory]);
|
}, [loadedFeaturedImages, activeCategory]);
|
||||||
|
|
||||||
const recentProjects = useMemo(() => {
|
const recentProjects = useMemo(() => {
|
||||||
const projects = canvasData?.data || [];
|
const projects = canvasData?.data || [];
|
||||||
@ -219,6 +337,109 @@ export function LovartHome({
|
|||||||
return (canvasData?.data || []).length > 4;
|
return (canvasData?.data || []).length > 4;
|
||||||
}, [canvasData?.data]);
|
}, [canvasData?.data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedModelId) return;
|
||||||
|
const defaultModelId = siteSettingsData?.canvasDefaultModelId || "auto";
|
||||||
|
setSelectedModelId(defaultModelId);
|
||||||
|
}, [selectedModelId, siteSettingsData?.canvasDefaultModelId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedModelId) return;
|
||||||
|
if (!modelsData) return;
|
||||||
|
if (allComposerModels.length === 0) return;
|
||||||
|
if (allComposerModels.some((model) => model.id === selectedModelId)) return;
|
||||||
|
setSelectedModelId("auto");
|
||||||
|
}, [allComposerModels, selectedModelId, modelsData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem("lovart-home-image-params");
|
||||||
|
if (!saved) return;
|
||||||
|
const parsed = JSON.parse(saved) as Partial<ImageGenerationParams>;
|
||||||
|
setImageParams((prev) => ({ ...prev, ...parsed }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[LovartHome] Failed to load image params:", error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleImageParamsChange = (nextParams: ImageGenerationParams) => {
|
||||||
|
setImageParams(nextParams);
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
try {
|
||||||
|
localStorage.setItem("lovart-home-image-params", JSON.stringify(nextParams));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[LovartHome] Failed to save image params:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!featuredLoadMoreRef.current || !hasMoreFeatured) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (!entries[0]?.isIntersecting || isLoadingMoreFeatured) return;
|
||||||
|
setFeaturedPageCount((current) => current + 1);
|
||||||
|
},
|
||||||
|
{ rootMargin: "1000px 0px 1200px 0px" }
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(featuredLoadMoreRef.current);
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [hasMoreFeatured, isLoadingMoreFeatured, setFeaturedPageCount]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let disposed = false;
|
||||||
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
if (composerPreviewTexts.length === 0) {
|
||||||
|
setTypedPreviewText("");
|
||||||
|
return () => {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const runTyping = (textIndex: number) => {
|
||||||
|
if (disposed) return;
|
||||||
|
const currentText = composerPreviewTexts[textIndex] || "";
|
||||||
|
let charIndex = 0;
|
||||||
|
|
||||||
|
const typeNextChar = () => {
|
||||||
|
if (disposed) return;
|
||||||
|
|
||||||
|
setTypedPreviewText(currentText.slice(0, charIndex));
|
||||||
|
charIndex += 1;
|
||||||
|
|
||||||
|
if (charIndex <= currentText.length) {
|
||||||
|
timer = setTimeout(typeNextChar, 55);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
runTyping((textIndex + 1) % composerPreviewTexts.length);
|
||||||
|
}, 1300);
|
||||||
|
};
|
||||||
|
|
||||||
|
typeNextChar();
|
||||||
|
};
|
||||||
|
|
||||||
|
runTyping(0);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposed = true;
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [composerPreviewTexts]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const blinkTimer = setInterval(() => {
|
||||||
|
setPreviewCursorVisible((value) => !value);
|
||||||
|
}, 450);
|
||||||
|
|
||||||
|
return () => clearInterval(blinkTimer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
filesRef.current = files;
|
filesRef.current = files;
|
||||||
}, [files]);
|
}, [files]);
|
||||||
@ -322,6 +543,8 @@ export function LovartHome({
|
|||||||
attachments: uploadedAttachments,
|
attachments: uploadedAttachments,
|
||||||
autoSend: true,
|
autoSend: true,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
|
selectedModelId: selectedModelId || "auto",
|
||||||
|
imageParams,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -342,9 +565,18 @@ export function LovartHome({
|
|||||||
router.push("/canvas?new=1");
|
router.push("/canvas?new=1");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openAllProjectsInPage = () => {
|
||||||
|
setShowAllProjects(true);
|
||||||
|
projectSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const composerPlaceholder = `${typedPreviewText || composerPreviewTexts[0] || "描述你想创作的内容"}${
|
||||||
|
previewCursorVisible ? " |" : ""
|
||||||
|
}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-svh bg-[#f4f4f4] text-[#171717]">
|
<div className="min-h-svh bg-[#f4f4f4] text-[#171717]">
|
||||||
<header className="fixed left-0 top-0 z-40 h-[68px] w-full border-b border-black/5 bg-[#f4f4f4]/90 backdrop-blur">
|
<header className="fixed left-0 top-0 z-40 h-[68px] w-full bg-[#f4f4f4]/90 backdrop-blur">
|
||||||
<div className="flex h-full items-center justify-between px-7">
|
<div className="flex h-full items-center justify-between px-7">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -364,11 +596,6 @@ export function LovartHome({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button className="inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[12px] text-black/80 hover:bg-black/5">
|
|
||||||
简体中文
|
|
||||||
<ChevronDown className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button className="flex h-8 w-8 items-center justify-center rounded-lg bg-black/5 text-black/75 hover:bg-black/10">
|
<button className="flex h-8 w-8 items-center justify-center rounded-lg bg-black/5 text-black/75 hover:bg-black/10">
|
||||||
<Bell className="h-3.5 w-3.5" />
|
<Bell className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@ -460,9 +687,7 @@ export function LovartHome({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-black/5"
|
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-black/5"
|
||||||
onClick={() =>
|
onClick={openAllProjectsInPage}
|
||||||
projectSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Folder className="h-[18px] w-[18px]" />
|
<Folder className="h-[18px] w-[18px]" />
|
||||||
</button>
|
</button>
|
||||||
@ -480,6 +705,15 @@ export function LovartHome({
|
|||||||
>
|
>
|
||||||
<Info className="h-[18px] w-[18px]" />
|
<Info className="h-[18px] w-[18px]" />
|
||||||
</button>
|
</button>
|
||||||
|
{user.type === "admin" ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-black/5"
|
||||||
|
onClick={() => router.push("/settings")}
|
||||||
|
>
|
||||||
|
<Settings className="h-[18px] w-[18px]" />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@ -491,8 +725,8 @@ export function LovartHome({
|
|||||||
className="inline-flex items-center gap-2 rounded-full bg-[#fde8bf] px-4 py-2 text-sm text-black/80"
|
className="inline-flex items-center gap-2 rounded-full bg-[#fde8bf] px-4 py-2 text-sm text-black/80"
|
||||||
>
|
>
|
||||||
<span className="rounded-full bg-[#ff9d3d] px-2 py-0.5 text-[10px] font-semibold text-white">NEW</span>
|
<span className="rounded-full bg-[#ff9d3d] px-2 py-0.5 text-[10px] font-semibold text-white">NEW</span>
|
||||||
现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠
|
{heroPromoText || "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠"}
|
||||||
<span className="text-[#ff7a00]">立即升级</span>
|
<span className="text-[#ff7a00]">{heroPromoActionText || "立即升级"}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="mt-8 flex items-center justify-center gap-2">
|
<div className="mt-8 flex items-center justify-center gap-2">
|
||||||
@ -504,16 +738,18 @@ export function LovartHome({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h1 className="text-[34px] font-semibold tracking-tight">
|
<h1 className="text-[34px] font-semibold tracking-tight">
|
||||||
{siteName || "Lovart"} 让设计更简单
|
{heroTitleText || `${siteName || "Lovart"} 让设计更简单`}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-[13px] text-black/35">懂你的设计代理,帮你搞定一切</p>
|
<p className="mt-2 text-[13px] text-black/35">
|
||||||
|
{heroSubtitleText || "懂你的设计代理,帮你搞定一切"}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="mx-auto mt-6 w-full max-w-[780px] rounded-[22px] border border-black/10 bg-white px-4 pb-3 pt-4 shadow-[0_10px_24px_rgba(0,0,0,0.05)]">
|
<div className="mx-auto mt-6 w-full max-w-[780px] rounded-[22px] border border-black/10 bg-white px-4 pb-3 pt-4 shadow-[0_10px_24px_rgba(0,0,0,0.05)]">
|
||||||
<Textarea
|
<Textarea
|
||||||
value={prompt}
|
value={prompt}
|
||||||
onChange={(event) => setPrompt(event.target.value)}
|
onChange={(event) => setPrompt(event.target.value)}
|
||||||
placeholder="让 Lovart 打造引人注目的社交媒体视觉"
|
placeholder={composerPlaceholder}
|
||||||
className="min-h-[76px] resize-none border-0 bg-transparent px-1 text-[12px] shadow-none focus-visible:ring-0"
|
className="min-h-[76px] resize-none border-0 bg-transparent px-1 text-[12px] shadow-none focus-visible:ring-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -535,39 +771,61 @@ export function LovartHome({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between border-t border-black/8 pt-3">
|
<div className="flex items-center justify-between border-t border-black/8 pt-3">
|
||||||
<label className="inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-full border border-black/10 text-black/60 hover:bg-black/5">
|
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
||||||
<Upload className="h-4 w-4" />
|
<label className="inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-full border border-black/10 text-black/60 hover:bg-black/5">
|
||||||
<input
|
<Upload className="h-4 w-4" />
|
||||||
type="file"
|
<input
|
||||||
accept="image/*"
|
type="file"
|
||||||
multiple
|
accept="image/*"
|
||||||
className="hidden"
|
multiple
|
||||||
onChange={(event) => {
|
className="hidden"
|
||||||
handleAddFiles(event.target.files);
|
onChange={(event) => {
|
||||||
event.target.value = "";
|
handleAddFiles(event.target.files);
|
||||||
}}
|
event.target.value = "";
|
||||||
/>
|
}}
|
||||||
</label>
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5">
|
<Select value={selectedModelId || "auto"} onValueChange={setSelectedModelId}>
|
||||||
<button className="flex h-8 w-8 items-center justify-center rounded-full border border-black/10 text-black/50 hover:bg-black/5">
|
<SelectTrigger className="h-8 w-auto min-w-[110px] rounded-full border border-blue-100 bg-blue-50 px-3 text-xs text-blue-600">
|
||||||
<Lightbulb className="h-3.5 w-3.5" />
|
<span className="truncate max-w-[100px]">
|
||||||
</button>
|
{currentComposerModel?.displayName || "智能自动"}
|
||||||
<button className="flex h-8 w-8 items-center justify-center rounded-full border border-black/10 text-black/50 hover:bg-black/5">
|
</span>
|
||||||
<Zap className="h-3.5 w-3.5" />
|
</SelectTrigger>
|
||||||
</button>
|
<SelectContent>
|
||||||
<button className="flex h-8 w-8 items-center justify-center rounded-full border border-black/10 text-black/50 hover:bg-black/5">
|
<SelectItem value="auto" className="text-xs">
|
||||||
<Globe className="h-3.5 w-3.5" />
|
智能自动
|
||||||
</button>
|
</SelectItem>
|
||||||
<button className="flex h-8 w-8 items-center justify-center rounded-full border border-[#72a3ff] text-[#3a78f2]">
|
{imageModels.map((model) => (
|
||||||
<Hexagon className="h-3.5 w-3.5" />
|
<SelectItem key={model.id} value={model.id} className="text-xs">
|
||||||
</button>
|
{model.displayName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1 overflow-hidden">
|
||||||
|
<ImageParamsBar
|
||||||
|
modelName={currentComposerModel?.displayName}
|
||||||
|
initialParams={imageParams}
|
||||||
|
onParamsChange={handleImageParamsChange}
|
||||||
|
inline
|
||||||
|
providerType={currentComposerModel?.providerType}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5 pl-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={createCanvasFromComposer}
|
onClick={createCanvasFromComposer}
|
||||||
disabled={isSubmitting || (!prompt.trim() && files.length === 0)}
|
disabled={isSubmitting || (!prompt.trim() && files.length === 0)}
|
||||||
className="h-8 w-8 rounded-full bg-[#9ca3af] p-0 text-white hover:bg-black"
|
className="h-8 w-8 rounded-full bg-[#9ca3af] p-0 text-white hover:bg-black"
|
||||||
>
|
>
|
||||||
<ArrowUp className="h-3.5 w-3.5" />
|
{isSubmitting ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowUp className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -672,7 +930,7 @@ export function LovartHome({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 columns-1 gap-4 md:columns-2 xl:columns-4">
|
<div className="mt-5 columns-1 gap-4 md:columns-2 xl:columns-4">
|
||||||
{featuredItems.slice(0, 20).map((item, index) => (
|
{featuredItems.map((item, index) => (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
type="button"
|
type="button"
|
||||||
@ -709,10 +967,18 @@ export function LovartHome({
|
|||||||
|
|
||||||
{featuredItems.length === 0 && (
|
{featuredItems.length === 0 && (
|
||||||
<div className="rounded-xl border border-dashed border-black/15 bg-white p-10 text-center text-black/35">
|
<div className="rounded-xl border border-dashed border-black/15 bg-white p-10 text-center text-black/35">
|
||||||
当前没有可展示的灵感数据。
|
{isLoadingFeatured ? "灵感内容加载中..." : "当前没有可展示的灵感数据。"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div ref={featuredLoadMoreRef} className="mt-3 flex h-10 items-center justify-center">
|
||||||
|
{isLoadingMoreFeatured ? (
|
||||||
|
<div className="inline-flex items-center gap-2 text-sm text-black/45">
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
正在加载更多灵感...
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@ -4,11 +4,24 @@ export interface CanvasLaunchAttachment {
|
|||||||
contentType: string;
|
contentType: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CanvasLaunchImageParams {
|
||||||
|
aspectRatio?: string;
|
||||||
|
imageSize?: string;
|
||||||
|
imageCount?: number;
|
||||||
|
size?: string;
|
||||||
|
negativePrompt?: string;
|
||||||
|
promptExtend?: boolean;
|
||||||
|
watermark?: boolean;
|
||||||
|
seed?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CanvasLaunchPayload {
|
export interface CanvasLaunchPayload {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
attachments: CanvasLaunchAttachment[];
|
attachments: CanvasLaunchAttachment[];
|
||||||
autoSend?: boolean;
|
autoSend?: boolean;
|
||||||
createdAt?: number;
|
createdAt?: number;
|
||||||
|
selectedModelId?: string;
|
||||||
|
imageParams?: CanvasLaunchImageParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCanvasLaunchStorageKey(canvasId: string): string {
|
export function getCanvasLaunchStorageKey(canvasId: string): string {
|
||||||
|
|||||||
5
lib/db/migrations/0029_add_lovart_home_copy_settings.sql
Normal file
5
lib/db/migrations/0029_add_lovart_home_copy_settings.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE "SiteSettings"
|
||||||
|
ADD COLUMN IF NOT EXISTS "lovartPromoText" text DEFAULT '现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠',
|
||||||
|
ADD COLUMN IF NOT EXISTS "lovartPromoActionText" varchar(32) DEFAULT '立即升级',
|
||||||
|
ADD COLUMN IF NOT EXISTS "lovartHeroTitle" text DEFAULT '由壹点AI 让设计更简单',
|
||||||
|
ADD COLUMN IF NOT EXISTS "lovartHeroSubtitle" text DEFAULT '懂你的设计代理,帮你搞定一切';
|
||||||
@ -106,6 +106,13 @@
|
|||||||
"when": 1769247486014,
|
"when": 1769247486014,
|
||||||
"tag": "0028_add_featured_category",
|
"tag": "0028_add_featured_category",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 15,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769251200000,
|
||||||
|
"tag": "0029_add_lovart_home_copy_settings",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1566,6 +1566,10 @@ export async function getSiteSettings() {
|
|||||||
tutorialVideoUrl: null,
|
tutorialVideoUrl: null,
|
||||||
tutorialEnabled: false,
|
tutorialEnabled: false,
|
||||||
rankingEnabled: true,
|
rankingEnabled: true,
|
||||||
|
lovartPromoText: "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠",
|
||||||
|
lovartPromoActionText: "立即升级",
|
||||||
|
lovartHeroTitle: "由壹点AI 让设计更简单",
|
||||||
|
lovartHeroSubtitle: "懂你的设计代理,帮你搞定一切",
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const [settings] = await db.select().from(siteSettings).limit(1);
|
const [settings] = await db.select().from(siteSettings).limit(1);
|
||||||
|
|||||||
@ -310,6 +310,12 @@ export const siteSettings = pgTable("SiteSettings", {
|
|||||||
canvasEnabled: boolean("canvasEnabled").notNull().default(true), // 是否启用无限画布功能
|
canvasEnabled: boolean("canvasEnabled").notNull().default(true), // 是否启用无限画布功能
|
||||||
canvasDefaultModelId: varchar("canvasDefaultModelId", { length: 128 }), // 画布默认选择的模型ID(null表示智能自动)
|
canvasDefaultModelId: varchar("canvasDefaultModelId", { length: 128 }), // 画布默认选择的模型ID(null表示智能自动)
|
||||||
canvasAutoModeDefaultModelId: varchar("canvasAutoModeDefaultModelId", { length: 128 }), // 智能自动模式下实际使用的默认生图模型ID
|
canvasAutoModeDefaultModelId: varchar("canvasAutoModeDefaultModelId", { length: 128 }), // 智能自动模式下实际使用的默认生图模型ID
|
||||||
|
lovartPromoText: text("lovartPromoText").default(
|
||||||
|
"现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠"
|
||||||
|
),
|
||||||
|
lovartPromoActionText: varchar("lovartPromoActionText", { length: 32 }).default("立即升级"),
|
||||||
|
lovartHeroTitle: text("lovartHeroTitle").default("由壹点AI 让设计更简单"),
|
||||||
|
lovartHeroSubtitle: text("lovartHeroSubtitle").default("懂你的设计代理,帮你搞定一切"),
|
||||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||||
updatedBy: uuid("updatedBy").references(() => user.id),
|
updatedBy: uuid("updatedBy").references(() => user.id),
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user