新版首页文案可配置并部署侧边栏管理入口
This commit is contained in:
parent
f2c6002969
commit
bc5838f9e1
@ -176,6 +176,11 @@ type SiteSettings = {
|
||||
canvasEnabled: boolean;
|
||||
canvasDefaultModelId: string | null;
|
||||
canvasAutoModeDefaultModelId: string | null;
|
||||
lovartComposerPreviewTexts: string[];
|
||||
lovartPromoText: string;
|
||||
lovartPromoActionText: string;
|
||||
lovartHeroTitle: string;
|
||||
lovartHeroSubtitle: string;
|
||||
homeVariant: "classic" | "lovart";
|
||||
surpriseCategories: string[];
|
||||
membershipConfig: ReturnType<typeof normalizeMembershipConfig>;
|
||||
@ -399,6 +404,15 @@ export function AdminSettingsClient() {
|
||||
canvasEnabled: true,
|
||||
canvasDefaultModelId: null,
|
||||
canvasAutoModeDefaultModelId: null,
|
||||
lovartComposerPreviewTexts: [
|
||||
"让 Lovart 打造引人注目的社交媒体视觉",
|
||||
"帮我把这张产品图做成高级电商海报",
|
||||
"基于参考图延展 3 套同风格方案",
|
||||
],
|
||||
lovartPromoText: "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠",
|
||||
lovartPromoActionText: "立即升级",
|
||||
lovartHeroTitle: "由壹点AI 让设计更简单",
|
||||
lovartHeroSubtitle: "懂你的设计代理,帮你搞定一切",
|
||||
homeVariant: "classic",
|
||||
surpriseCategories: ["全部", "惊喜"],
|
||||
membershipConfig: defaultMembershipConfig,
|
||||
@ -444,6 +458,21 @@ export function AdminSettingsClient() {
|
||||
canvasEnabled: siteSettingsData.canvasEnabled ?? true,
|
||||
canvasDefaultModelId: siteSettingsData.canvasDefaultModelId || 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",
|
||||
surpriseCategories: Array.isArray(siteSettingsData.surpriseCategories)
|
||||
? siteSettingsData.surpriseCategories.map((item: any) => String(item).trim()).filter(Boolean)
|
||||
@ -2668,6 +2697,86 @@ 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="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="flex items-center justify-between">
|
||||
<div>
|
||||
|
||||
@ -53,10 +53,15 @@ async function NewChatPage() {
|
||||
id: session.user.id,
|
||||
email: session.user.email,
|
||||
name: session.user.name,
|
||||
type: session.user.type,
|
||||
}}
|
||||
siteName={siteSettings.siteName || "AI Chatbot"}
|
||||
siteLogo={siteSettings.siteLogo || siteSettings.siteLogoDark || ""}
|
||||
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_SURPRISE_CATEGORIES_KEY = "home_surprise_categories";
|
||||
const LOVART_COMPOSER_PREVIEW_TEXTS_KEY = "lovart_composer_preview_texts";
|
||||
const MEMBERSHIP_CONFIG_KEY = "membership_config";
|
||||
const MEMBERSHIP_USER_OVERRIDES_KEY = "membership_user_plan_overrides";
|
||||
|
||||
@ -42,6 +43,25 @@ function normalizeSurpriseCategories(value: unknown): string[] {
|
||||
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 - 获取网站设置 (公开)
|
||||
export async function GET() {
|
||||
try {
|
||||
@ -50,6 +70,7 @@ export async function GET() {
|
||||
notificationSetting,
|
||||
homeVariantSetting,
|
||||
surpriseCategoriesSetting,
|
||||
lovartComposerPreviewTextsSetting,
|
||||
membershipConfigSetting,
|
||||
membershipOverridesSetting,
|
||||
] = await Promise.all([
|
||||
@ -72,6 +93,12 @@ export async function GET() {
|
||||
.where(eq(systemSettings.key, HOME_SURPRISE_CATEGORIES_KEY))
|
||||
.limit(1)
|
||||
.then(rows => rows[0]),
|
||||
db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
.where(eq(systemSettings.key, LOVART_COMPOSER_PREVIEW_TEXTS_KEY))
|
||||
.limit(1)
|
||||
.then(rows => rows[0]),
|
||||
db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
@ -89,6 +116,9 @@ export async function GET() {
|
||||
const notification = notificationSetting?.value || { enabled: false, title: "", content: "" };
|
||||
const homeVariant = normalizeHomeVariant(homeVariantSetting?.value);
|
||||
const surpriseCategories = normalizeSurpriseCategories(surpriseCategoriesSetting?.value);
|
||||
const lovartComposerPreviewTexts = normalizeLovartComposerPreviewTexts(
|
||||
lovartComposerPreviewTextsSetting?.value
|
||||
);
|
||||
const membershipConfig = normalizeMembershipConfig(membershipConfigSetting?.value);
|
||||
const membershipUserOverrides = normalizeMembershipUserOverrides(
|
||||
membershipOverridesSetting?.value
|
||||
@ -119,17 +149,29 @@ export async function GET() {
|
||||
canvasEnabled: true,
|
||||
canvasDefaultModelId: null,
|
||||
canvasAutoModeDefaultModelId: null,
|
||||
lovartPromoText: "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠",
|
||||
lovartPromoActionText: "立即升级",
|
||||
lovartHeroTitle: "由壹点AI 让设计更简单",
|
||||
lovartHeroSubtitle: "懂你的设计代理,帮你搞定一切",
|
||||
homeVariant,
|
||||
surpriseCategories,
|
||||
lovartComposerPreviewTexts,
|
||||
membershipConfig,
|
||||
membershipUserOverrides,
|
||||
};
|
||||
|
||||
const response = NextResponse.json({
|
||||
...baseSettings,
|
||||
lovartPromoText:
|
||||
baseSettings.lovartPromoText ||
|
||||
"现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠",
|
||||
lovartPromoActionText: baseSettings.lovartPromoActionText || "立即升级",
|
||||
lovartHeroTitle: baseSettings.lovartHeroTitle || "由壹点AI 让设计更简单",
|
||||
lovartHeroSubtitle: baseSettings.lovartHeroSubtitle || "懂你的设计代理,帮你搞定一切",
|
||||
notification,
|
||||
homeVariant,
|
||||
surpriseCategories,
|
||||
lovartComposerPreviewTexts,
|
||||
membershipConfig,
|
||||
membershipUserOverrides,
|
||||
});
|
||||
@ -173,8 +215,13 @@ const updateSchema = z.object({
|
||||
canvasEnabled: z.boolean().optional(),
|
||||
canvasDefaultModelId: 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(),
|
||||
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(),
|
||||
membershipUserOverrides: z.unknown().optional(),
|
||||
});
|
||||
@ -197,6 +244,7 @@ export async function PUT(request: NextRequest) {
|
||||
const {
|
||||
homeVariant,
|
||||
surpriseCategories,
|
||||
lovartComposerPreviewTexts,
|
||||
membershipConfig,
|
||||
membershipUserOverrides,
|
||||
...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) {
|
||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||
await db
|
||||
@ -303,6 +371,10 @@ export async function PUT(request: NextRequest) {
|
||||
surpriseCategories !== undefined
|
||||
? normalizeSurpriseCategories(surpriseCategories)
|
||||
: normalizeSurpriseCategories(undefined),
|
||||
lovartComposerPreviewTexts:
|
||||
lovartComposerPreviewTexts !== undefined
|
||||
? normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts)
|
||||
: normalizeLovartComposerPreviewTexts(undefined),
|
||||
membershipConfig:
|
||||
membershipConfig !== undefined
|
||||
? normalizeMembershipConfig(membershipConfig)
|
||||
@ -340,6 +412,12 @@ export async function PUT(request: NextRequest) {
|
||||
canvasEnabled: siteSettingsPayload.canvasEnabled ?? true,
|
||||
canvasDefaultModelId: siteSettingsPayload.canvasDefaultModelId,
|
||||
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,
|
||||
})
|
||||
.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) {
|
||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||
await db
|
||||
@ -430,6 +528,10 @@ export async function PUT(request: NextRequest) {
|
||||
surpriseCategories !== undefined
|
||||
? normalizeSurpriseCategories(surpriseCategories)
|
||||
: normalizeSurpriseCategories(undefined),
|
||||
lovartComposerPreviewTexts:
|
||||
lovartComposerPreviewTexts !== undefined
|
||||
? normalizeLovartComposerPreviewTexts(lovartComposerPreviewTexts)
|
||||
: normalizeLovartComposerPreviewTexts(undefined),
|
||||
membershipConfig:
|
||||
membershipConfig !== undefined
|
||||
? normalizeMembershipConfig(membershipConfig)
|
||||
|
||||
@ -479,6 +479,17 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
setAttachments(normalizedAttachments as PickedObjectAttachment[]);
|
||||
setInput(normalizedPrompt);
|
||||
|
||||
if (parsed.selectedModelId) {
|
||||
setSelectedModelId(String(parsed.selectedModelId));
|
||||
}
|
||||
|
||||
if (parsed.imageParams && typeof parsed.imageParams === "object") {
|
||||
setImageParams((prev) => ({
|
||||
...prev,
|
||||
...parsed.imageParams,
|
||||
}));
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
normalizedAttachments.forEach((attachment, index) => {
|
||||
insertImageTagInEditor(attachment.url, attachment.name, index);
|
||||
@ -491,6 +502,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
attachments: normalizedAttachments,
|
||||
autoSend: true,
|
||||
createdAt: parsed.createdAt,
|
||||
selectedModelId: parsed.selectedModelId,
|
||||
imageParams: parsed.imageParams,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import useSWRInfinite from "swr/infinite";
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUp,
|
||||
@ -10,17 +11,16 @@ import {
|
||||
ChevronDown,
|
||||
Clapperboard,
|
||||
Folder,
|
||||
Globe,
|
||||
Heart,
|
||||
Hexagon,
|
||||
Home,
|
||||
ImageIcon,
|
||||
Info,
|
||||
Lightbulb,
|
||||
Loader2,
|
||||
Monitor,
|
||||
Palette,
|
||||
Plus,
|
||||
Send,
|
||||
Settings,
|
||||
Sparkles,
|
||||
Star,
|
||||
Upload,
|
||||
@ -31,6 +31,8 @@ import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
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 { fetcher } from "@/lib/utils";
|
||||
import {
|
||||
@ -44,6 +46,7 @@ type HomeUser = {
|
||||
email?: string | null;
|
||||
name?: string | null;
|
||||
image?: string | null;
|
||||
type?: string | null;
|
||||
};
|
||||
|
||||
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 = {
|
||||
config: MembershipConfig;
|
||||
currentPlanId: string;
|
||||
@ -95,6 +116,10 @@ type LovartHomeProps = {
|
||||
siteName: string;
|
||||
siteLogo?: string | null;
|
||||
surpriseCategories: string[];
|
||||
heroPromoText?: string | null;
|
||||
heroPromoActionText?: string | null;
|
||||
heroTitleText?: string | null;
|
||||
heroSubtitleText?: string | null;
|
||||
};
|
||||
|
||||
type UploadFileItem = {
|
||||
@ -124,6 +149,7 @@ const PROMPT_TAGS = [
|
||||
];
|
||||
|
||||
const MASONRY_HEIGHTS = [420, 290, 340, 470, 300, 360, 430, 320];
|
||||
const FEATURED_PAGE_SIZE = 24;
|
||||
|
||||
function formatRelativeTime(value?: string) {
|
||||
if (!value) return "更新于今天";
|
||||
@ -148,6 +174,10 @@ export function LovartHome({
|
||||
siteName,
|
||||
siteLogo,
|
||||
surpriseCategories,
|
||||
heroPromoText,
|
||||
heroPromoActionText,
|
||||
heroTitleText,
|
||||
heroSubtitleText,
|
||||
}: LovartHomeProps) {
|
||||
const router = useRouter();
|
||||
const [prompt, setPrompt] = useState("");
|
||||
@ -157,7 +187,16 @@ export function LovartHome({
|
||||
const [showAllProjects, setShowAllProjects] = useState(false);
|
||||
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
|
||||
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 featuredLoadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||
const filesRef = useRef<UploadFileItem[]>([]);
|
||||
|
||||
const { data: canvasData, mutate: mutateCanvas } = useSWR<{
|
||||
@ -165,15 +204,39 @@ export function LovartHome({
|
||||
data: CanvasProject[];
|
||||
}>("/api/canvas", fetcher);
|
||||
|
||||
const { data: featuredData } = useSWR<{
|
||||
images: FeaturedImage[];
|
||||
}>("/api/featured?page=1&pageSize=40", fetcher);
|
||||
const { data: featuredPages, size: featuredPageCount, setSize: setFeaturedPageCount, isValidating: isFeaturedValidating } =
|
||||
useSWRInfinite<FeaturedResponse>(
|
||||
(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>(
|
||||
"/api/user/membership-summary",
|
||||
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 membershipPlans = membershipConfig?.plans || [];
|
||||
const currentPlan = membershipData?.currentPlan;
|
||||
@ -189,6 +252,30 @@ export function LovartHome({
|
||||
}, {});
|
||||
}, [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 merged = [
|
||||
"全部",
|
||||
@ -198,8 +285,39 @@ export function LovartHome({
|
||||
return Array.from(new Set(merged)).slice(0, 12);
|
||||
}, [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 images = featuredData?.images || [];
|
||||
const images = loadedFeaturedImages;
|
||||
if (activeCategory === "全部") {
|
||||
return images;
|
||||
}
|
||||
@ -208,7 +326,7 @@ export function LovartHome({
|
||||
? String(item.featuredCategory) === activeCategory
|
||||
: String(item.prompt || "").toLowerCase().includes(activeCategory.toLowerCase())
|
||||
);
|
||||
}, [featuredData?.images, activeCategory]);
|
||||
}, [loadedFeaturedImages, activeCategory]);
|
||||
|
||||
const recentProjects = useMemo(() => {
|
||||
const projects = canvasData?.data || [];
|
||||
@ -219,6 +337,109 @@ export function LovartHome({
|
||||
return (canvasData?.data || []).length > 4;
|
||||
}, [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(() => {
|
||||
filesRef.current = files;
|
||||
}, [files]);
|
||||
@ -322,6 +543,8 @@ export function LovartHome({
|
||||
attachments: uploadedAttachments,
|
||||
autoSend: true,
|
||||
createdAt: Date.now(),
|
||||
selectedModelId: selectedModelId || "auto",
|
||||
imageParams,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -342,9 +565,18 @@ export function LovartHome({
|
||||
router.push("/canvas?new=1");
|
||||
};
|
||||
|
||||
const openAllProjectsInPage = () => {
|
||||
setShowAllProjects(true);
|
||||
projectSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
};
|
||||
|
||||
const composerPlaceholder = `${typedPreviewText || composerPreviewTexts[0] || "描述你想创作的内容"}${
|
||||
previewCursorVisible ? " |" : ""
|
||||
}`;
|
||||
|
||||
return (
|
||||
<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">
|
||||
<button
|
||||
type="button"
|
||||
@ -364,11 +596,6 @@ export function LovartHome({
|
||||
</button>
|
||||
|
||||
<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">
|
||||
<Bell className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
@ -460,9 +687,7 @@ export function LovartHome({
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-black/5"
|
||||
onClick={() =>
|
||||
projectSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })
|
||||
}
|
||||
onClick={openAllProjectsInPage}
|
||||
>
|
||||
<Folder className="h-[18px] w-[18px]" />
|
||||
</button>
|
||||
@ -480,6 +705,15 @@ export function LovartHome({
|
||||
>
|
||||
<Info className="h-[18px] w-[18px]" />
|
||||
</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>
|
||||
</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"
|
||||
>
|
||||
<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 折优惠
|
||||
<span className="text-[#ff7a00]">立即升级</span>
|
||||
{heroPromoText || "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠"}
|
||||
<span className="text-[#ff7a00]">{heroPromoActionText || "立即升级"}</span>
|
||||
</button>
|
||||
|
||||
<div className="mt-8 flex items-center justify-center gap-2">
|
||||
@ -504,16 +738,18 @@ export function LovartHome({
|
||||
</div>
|
||||
)}
|
||||
<h1 className="text-[34px] font-semibold tracking-tight">
|
||||
{siteName || "Lovart"} 让设计更简单
|
||||
{heroTitleText || `${siteName || "Lovart"} 让设计更简单`}
|
||||
</h1>
|
||||
</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)]">
|
||||
<Textarea
|
||||
value={prompt}
|
||||
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"
|
||||
/>
|
||||
|
||||
@ -535,39 +771,61 @@ export function LovartHome({
|
||||
)}
|
||||
|
||||
<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">
|
||||
<Upload className="h-4 w-4" />
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(event) => {
|
||||
handleAddFiles(event.target.files);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
<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">
|
||||
<Upload className="h-4 w-4" />
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(event) => {
|
||||
handleAddFiles(event.target.files);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button className="flex h-8 w-8 items-center justify-center rounded-full border border-black/10 text-black/50 hover:bg-black/5">
|
||||
<Lightbulb className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button className="flex h-8 w-8 items-center justify-center rounded-full border border-black/10 text-black/50 hover:bg-black/5">
|
||||
<Zap className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button className="flex h-8 w-8 items-center justify-center rounded-full border border-black/10 text-black/50 hover:bg-black/5">
|
||||
<Globe className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button className="flex h-8 w-8 items-center justify-center rounded-full border border-[#72a3ff] text-[#3a78f2]">
|
||||
<Hexagon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<Select value={selectedModelId || "auto"} onValueChange={setSelectedModelId}>
|
||||
<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">
|
||||
<span className="truncate max-w-[100px]">
|
||||
{currentComposerModel?.displayName || "智能自动"}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="auto" className="text-xs">
|
||||
智能自动
|
||||
</SelectItem>
|
||||
{imageModels.map((model) => (
|
||||
<SelectItem key={model.id} value={model.id} className="text-xs">
|
||||
{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
|
||||
onClick={createCanvasFromComposer}
|
||||
disabled={isSubmitting || (!prompt.trim() && files.length === 0)}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@ -672,7 +930,7 @@ export function LovartHome({
|
||||
</div>
|
||||
|
||||
<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
|
||||
key={item.id}
|
||||
type="button"
|
||||
@ -709,10 +967,18 @@ export function LovartHome({
|
||||
|
||||
{featuredItems.length === 0 && (
|
||||
<div className="rounded-xl border border-dashed border-black/15 bg-white p-10 text-center text-black/35">
|
||||
当前没有可展示的灵感数据。
|
||||
{isLoadingFeatured ? "灵感内容加载中..." : "当前没有可展示的灵感数据。"}
|
||||
</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>
|
||||
</main>
|
||||
|
||||
|
||||
@ -4,11 +4,24 @@ export interface CanvasLaunchAttachment {
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
export interface CanvasLaunchImageParams {
|
||||
aspectRatio?: string;
|
||||
imageSize?: string;
|
||||
imageCount?: number;
|
||||
size?: string;
|
||||
negativePrompt?: string;
|
||||
promptExtend?: boolean;
|
||||
watermark?: boolean;
|
||||
seed?: number;
|
||||
}
|
||||
|
||||
export interface CanvasLaunchPayload {
|
||||
prompt: string;
|
||||
attachments: CanvasLaunchAttachment[];
|
||||
autoSend?: boolean;
|
||||
createdAt?: number;
|
||||
selectedModelId?: string;
|
||||
imageParams?: CanvasLaunchImageParams;
|
||||
}
|
||||
|
||||
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,
|
||||
"tag": "0028_add_featured_category",
|
||||
"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,
|
||||
tutorialEnabled: false,
|
||||
rankingEnabled: true,
|
||||
lovartPromoText: "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠",
|
||||
lovartPromoActionText: "立即升级",
|
||||
lovartHeroTitle: "由壹点AI 让设计更简单",
|
||||
lovartHeroSubtitle: "懂你的设计代理,帮你搞定一切",
|
||||
};
|
||||
try {
|
||||
const [settings] = await db.select().from(siteSettings).limit(1);
|
||||
|
||||
@ -310,6 +310,12 @@ export const siteSettings = pgTable("SiteSettings", {
|
||||
canvasEnabled: boolean("canvasEnabled").notNull().default(true), // 是否启用无限画布功能
|
||||
canvasDefaultModelId: varchar("canvasDefaultModelId", { length: 128 }), // 画布默认选择的模型ID(null表示智能自动)
|
||||
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(),
|
||||
updatedBy: uuid("updatedBy").references(() => user.id),
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user