feat: 瀑布流布局

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-26 22:36:37 +08:00
parent 547b5ad063
commit f7bdedf7d2
22 changed files with 608 additions and 67 deletions
+75
View File
@@ -0,0 +1,75 @@
// server/api/images/presets.get.ts - 预设提示词列表接口:分页返回已启用的生图预设。
import type { IPromptPresetListData } from "#shared/types/openai";
import {
createApiLogger,
createSuccessResponse,
createUpstreamErrorResponse,
listPromptPresets,
toSafeLogError
} from "~~/server/utils";
const DEFAULT_PAGE = 1;
const DEFAULT_PAGE_SIZE = 20;
const MAX_PAGE_SIZE = 50;
/**
* GET /api/images/presets
*
* 流程:
* 1. 读取 page/size 查询参数,并限制最大 pageSize。
* 2. 只返回已启用的预设提示词,按导入顺序展示。
* 3. 返回前端展示和填充输入所需字段,不包含内部排查信息。
*/
export default defineEventHandler(async (event) => {
const logger = createApiLogger("images.presets.list");
logger.info("开始");
try {
const query = getQuery(event);
const page = normalizePositiveInt(query.page, DEFAULT_PAGE);
const pageSize = Math.min(
normalizePositiveInt(query.size, DEFAULT_PAGE_SIZE),
MAX_PAGE_SIZE
);
logger.info("查询参数", {
page,
pageSize
});
const result = await listPromptPresets({
page,
pageSize
});
logger.done("成功", {
page,
pageSize,
total: result.total,
itemCount: result.items.length
});
return createSuccessResponse<IPromptPresetListData>(
result,
"获取预设提示词成功"
);
} catch (error) {
logger.error("失败", {
error: toSafeLogError(error)
});
return createUpstreamErrorResponse(error, "获取预设提示词失败");
}
});
/** 将 query 参数归一化为正整数,非法值回退到默认值 */
const normalizePositiveInt = (
value: unknown,
fallbackValue: number
): number => {
const rawValue = Array.isArray(value) ? value[0] : value;
const numberValue = Number.parseInt(String(rawValue ?? ""), 10);
return Number.isInteger(numberValue) && numberValue > 0
? numberValue
: fallbackValue;
};
+1
View File
@@ -10,3 +10,4 @@ export * from "./newApiTokens";
export * from "./openai";
export * from "./plazaPosts";
export * from "./prisma";
export * from "./promptPresets";
+74
View File
@@ -0,0 +1,74 @@
// server/utils/promptPresets.ts - 预设提示词数据库查询:为前端提供可分页展示的预设列表。
import type {
IPromptPresetItem,
IPromptPresetListData
} from "#shared/types/openai";
import { prisma } from "~~/server/utils/prisma";
export const listPromptPresets = async ({
page,
pageSize
}: {
page: number;
pageSize: number;
}): Promise<IPromptPresetListData> => {
const where = {
enabled: true
};
const [total, records] = await Promise.all([
prisma.promptPreset.count({ where }),
prisma.promptPreset.findMany({
where,
orderBy: [
{
sortOrder: "asc"
},
{
id: "asc"
}
],
skip: (page - 1) * pageSize,
take: pageSize
})
]);
return {
page,
pageSize,
total,
items: records.map(mapPromptPresetItem)
};
};
const mapPromptPresetItem = (record: {
id: bigint;
sourceId: string;
name: string;
promptRaw: string;
prompt: string;
negativePrompt: string;
category: string;
tags: unknown;
aspectRatio: string;
previewUrl: string;
compressedPreviewUrl: string;
sortOrder: number;
}): IPromptPresetItem => {
return {
id: record.id.toString(),
sourceId: record.sourceId,
name: record.name,
promptRaw: record.promptRaw,
prompt: record.prompt,
negativePrompt: record.negativePrompt,
category: record.category,
tags: Array.isArray(record.tags)
? record.tags.filter((tag): tag is string => typeof tag === "string")
: [],
aspectRatio: record.aspectRatio,
previewUrl: record.previewUrl,
compressedPreviewUrl: record.compressedPreviewUrl,
sortOrder: record.sortOrder
};
};