支持精选图片分类并新增最近项目页内查看更多
This commit is contained in:
parent
335a6da684
commit
f2c6002969
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
import { toast } from "sonner";
|
||||
import { formatBeijingTime } from "@/lib/utils";
|
||||
@ -229,6 +229,7 @@ type GeneratedImageRecord = {
|
||||
prompt: string | null;
|
||||
modelId: string | null;
|
||||
isFeatured: boolean;
|
||||
featuredCategory: string | null;
|
||||
featuredAt: string | null;
|
||||
createdAt: string;
|
||||
chatId: string | null;
|
||||
@ -455,6 +456,13 @@ export function AdminSettingsClient() {
|
||||
}
|
||||
}, [siteSettingsData]);
|
||||
|
||||
const featuredCategoryOptions = useMemo(() => {
|
||||
const values = (siteSettings.surpriseCategories || [])
|
||||
.map((item) => String(item || "").trim())
|
||||
.filter((item) => item && item !== "全部");
|
||||
return Array.from(new Set(values));
|
||||
}, [siteSettings.surpriseCategories]);
|
||||
|
||||
// 抠图API配置状态
|
||||
const [rmbgConfig, setRmbgConfig] = useState({
|
||||
provider: "rmbg" as "rmbg" | "photoroom", // 抠图服务提供商
|
||||
@ -1994,6 +2002,10 @@ export function AdminSettingsClient() {
|
||||
<Label className="text-muted-foreground">模型</Label>
|
||||
<p>{selectedImage.modelId || "未知"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">精选分类</Label>
|
||||
<p>{selectedImage.featuredCategory || "未设置"}</p>
|
||||
</div>
|
||||
{selectedImage.isFeatured && selectedImage.featuredAt && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground">精选时间</Label>
|
||||
@ -2006,7 +2018,56 @@ export function AdminSettingsClient() {
|
||||
</div>
|
||||
</div>
|
||||
{/* 精选操作按钮 */}
|
||||
<div className="flex justify-end pt-2 border-t">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-t pt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs text-muted-foreground">分类</Label>
|
||||
<Select
|
||||
value={selectedImage.featuredCategory || "__none__"}
|
||||
onValueChange={(value) =>
|
||||
setSelectedImage({
|
||||
...selectedImage,
|
||||
featuredCategory: value === "__none__" ? null : value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[180px]">
|
||||
<SelectValue placeholder="选择分类" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">未分类</SelectItem>
|
||||
{featuredCategoryOptions.map((category) => (
|
||||
<SelectItem key={category} value={category}>
|
||||
{category}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const res = await fetch("/api/admin/images", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
imageId: selectedImage.id,
|
||||
featuredCategory: selectedImage.featuredCategory,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("更新精选分类失败");
|
||||
}
|
||||
toast.success("精选分类已更新");
|
||||
mutateImages();
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "更新精选分类失败");
|
||||
}
|
||||
}}
|
||||
>
|
||||
保存分类
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant={selectedImage.isFeatured ? "outline" : "default"}
|
||||
className={selectedImage.isFeatured ? "" : "bg-gradient-to-r from-yellow-400 to-orange-500 hover:from-yellow-500 hover:to-orange-600"}
|
||||
|
||||
@ -89,6 +89,7 @@ export async function GET(request: NextRequest) {
|
||||
prompt: generatedImage.prompt,
|
||||
modelId: generatedImage.modelId,
|
||||
isFeatured: generatedImage.isFeatured,
|
||||
featuredCategory: generatedImage.featuredCategory,
|
||||
featuredAt: generatedImage.featuredAt,
|
||||
createdAt: generatedImage.createdAt,
|
||||
chatId: generatedImage.chatId,
|
||||
@ -131,23 +132,39 @@ export async function PATCH(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { imageId, isFeatured } = body;
|
||||
const { imageId, isFeatured, featuredCategory } = body;
|
||||
|
||||
if (!imageId) {
|
||||
return NextResponse.json({ error: "Image ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// 更新精选状态
|
||||
const updateData: Record<string, unknown> = {
|
||||
isFeatured: Boolean(isFeatured),
|
||||
};
|
||||
const updateData: Record<string, unknown> = {};
|
||||
const shouldUpdateFeaturedState = typeof isFeatured === "boolean";
|
||||
const shouldUpdateCategory = Object.prototype.hasOwnProperty.call(body, "featuredCategory");
|
||||
|
||||
if (isFeatured) {
|
||||
updateData.featuredAt = new Date();
|
||||
updateData.featuredBy = session.user.id;
|
||||
} else {
|
||||
updateData.featuredAt = null;
|
||||
updateData.featuredBy = null;
|
||||
if (!shouldUpdateFeaturedState && !shouldUpdateCategory) {
|
||||
return NextResponse.json({ error: "No valid fields to update" }, { status: 400 });
|
||||
}
|
||||
|
||||
// 更新精选状态
|
||||
if (shouldUpdateFeaturedState) {
|
||||
updateData.isFeatured = isFeatured;
|
||||
|
||||
if (isFeatured) {
|
||||
updateData.featuredAt = new Date();
|
||||
updateData.featuredBy = session.user.id;
|
||||
} else {
|
||||
updateData.featuredAt = null;
|
||||
updateData.featuredBy = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldUpdateCategory) {
|
||||
const normalizedCategory =
|
||||
typeof featuredCategory === "string" && featuredCategory.trim().length > 0
|
||||
? featuredCategory.trim().slice(0, 64)
|
||||
: null;
|
||||
updateData.featuredCategory = normalizedCategory;
|
||||
}
|
||||
|
||||
await db
|
||||
@ -155,15 +172,22 @@ export async function PATCH(request: NextRequest) {
|
||||
.set(updateData)
|
||||
.where(eq(generatedImage.id, imageId));
|
||||
|
||||
console.log(`[Admin Images API] Image ${imageId} featured status set to ${isFeatured} by ${session.user.id}`);
|
||||
console.log(
|
||||
`[Admin Images API] Image ${imageId} updated by ${session.user.id}, featured=${String(
|
||||
isFeatured
|
||||
)}`
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: isFeatured ? "已添加到精选" : "已取消精选"
|
||||
message: shouldUpdateFeaturedState
|
||||
? isFeatured
|
||||
? "已添加到精选"
|
||||
: "已取消精选"
|
||||
: "精选分类已更新",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Admin Images API] PATCH Error:", error);
|
||||
return NextResponse.json({ error: "Failed to update featured status" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ export async function GET(request: NextRequest) {
|
||||
imageSize: generatedImage.imageSize,
|
||||
prompt: generatedImage.prompt,
|
||||
modelId: generatedImage.modelId,
|
||||
featuredCategory: generatedImage.featuredCategory,
|
||||
createdAt: generatedImage.createdAt,
|
||||
featuredAt: generatedImage.featuredAt,
|
||||
userId: generatedImage.userId,
|
||||
@ -61,6 +62,7 @@ export async function GET(request: NextRequest) {
|
||||
prompt: img.prompt,
|
||||
modelId: img.modelId,
|
||||
modelName: img.modelName || img.modelId || "未知模型",
|
||||
featuredCategory: img.featuredCategory,
|
||||
createdAt: img.createdAt,
|
||||
featuredAt: img.featuredAt,
|
||||
user: {
|
||||
@ -81,4 +83,3 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: "Failed to fetch featured images" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -59,6 +59,7 @@ type FeaturedImage = {
|
||||
thumbnailUrl: string;
|
||||
originalUrl: string;
|
||||
prompt?: string | null;
|
||||
featuredCategory?: string | null;
|
||||
user?: {
|
||||
name?: string;
|
||||
department?: string;
|
||||
@ -153,6 +154,7 @@ export function LovartHome({
|
||||
const [files, setFiles] = useState<UploadFileItem[]>([]);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [activeCategory, setActiveCategory] = useState("全部");
|
||||
const [showAllProjects, setShowAllProjects] = useState(false);
|
||||
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
|
||||
const [billingCycle, setBillingCycle] = useState<"monthly" | "annual">("monthly");
|
||||
const projectSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -201,14 +203,20 @@ export function LovartHome({
|
||||
if (activeCategory === "全部") {
|
||||
return images;
|
||||
}
|
||||
const keyword = activeCategory.toLowerCase();
|
||||
return images.filter((item) =>
|
||||
String(item.prompt || "").toLowerCase().includes(keyword)
|
||||
item.featuredCategory
|
||||
? String(item.featuredCategory) === activeCategory
|
||||
: String(item.prompt || "").toLowerCase().includes(activeCategory.toLowerCase())
|
||||
);
|
||||
}, [featuredData?.images, activeCategory]);
|
||||
|
||||
const recentProjects = useMemo(() => {
|
||||
return (canvasData?.data || []).slice(0, 7);
|
||||
const projects = canvasData?.data || [];
|
||||
return showAllProjects ? projects : projects.slice(0, 4);
|
||||
}, [canvasData?.data, showAllProjects]);
|
||||
|
||||
const hasMoreProjects = useMemo(() => {
|
||||
return (canvasData?.data || []).length > 4;
|
||||
}, [canvasData?.data]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -586,14 +594,16 @@ export function LovartHome({
|
||||
<section ref={projectSectionRef} className="mt-20">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-[18px] font-semibold tracking-tight">最近项目</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 text-[13px] text-black/45 hover:text-black"
|
||||
onClick={() => router.push("/canvas")}
|
||||
>
|
||||
查看全部
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{hasMoreProjects ? (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 text-[13px] text-black/45 hover:text-black"
|
||||
onClick={() => setShowAllProjects((value) => !value)}
|
||||
>
|
||||
{showAllProjects ? "收起" : "查看更多"}
|
||||
<ArrowRight className={`h-3.5 w-3.5 transition-transform ${showAllProjects ? "rotate-90" : ""}`} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
|
||||
6
lib/db/migrations/0028_add_featured_category.sql
Normal file
6
lib/db/migrations/0028_add_featured_category.sql
Normal file
@ -0,0 +1,6 @@
|
||||
ALTER TABLE "GeneratedImage"
|
||||
ADD COLUMN IF NOT EXISTS "featuredCategory" varchar(64);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "generated_image_featured_category_idx"
|
||||
ON "GeneratedImage" ("featuredCategory")
|
||||
WHERE "isFeatured" = true;
|
||||
@ -99,6 +99,13 @@
|
||||
"when": 1769247486013,
|
||||
"tag": "0013_loving_ultimatum",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "7",
|
||||
"when": 1769247486014,
|
||||
"tag": "0028_add_featured_category",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -389,6 +389,7 @@ export const generatedImage = pgTable("GeneratedImage", {
|
||||
prompt: text("prompt"),
|
||||
modelId: varchar("modelId", { length: 128 }),
|
||||
isFeatured: boolean("isFeatured").notNull().default(false), // 是否精选
|
||||
featuredCategory: varchar("featuredCategory", { length: 64 }), // 精选分类
|
||||
featuredAt: timestamp("featuredAt"), // 精选时间
|
||||
featuredBy: uuid("featuredBy").references(() => user.id), // 精选操作人
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user