支持精选图片分类并新增最近项目页内查看更多
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-13 23:49:13 +08:00
parent 335a6da684
commit f2c6002969
7 changed files with 139 additions and 29 deletions

View File

@ -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"}

View File

@ -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 });
}
}

View File

@ -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 });
}
}

View File

@ -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">

View 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;

View File

@ -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
}
]
}
}

View File

@ -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(),