feat: 完善广场功能
This commit is contained in:
@@ -0,0 +1,124 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { IImageHistoryItem } from "#shared/types";
|
||||||
|
import { ImageService } from "~/services";
|
||||||
|
import { useUserStore } from "~/stores";
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { isOnline } = storeToRefs(userStore);
|
||||||
|
|
||||||
|
const histories = ref<IImageHistoryItem[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const errorMessage = ref("");
|
||||||
|
const operatingRecordId = ref("");
|
||||||
|
|
||||||
|
const loadHistory = async () => {
|
||||||
|
if (!isOnline.value) {
|
||||||
|
histories.value = [];
|
||||||
|
errorMessage.value = "";
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
errorMessage.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await ImageService.GetImageHistory(1, 20);
|
||||||
|
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
histories.value = res.data.items;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage.value = res.msg || "获取生图历史失败";
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = getErrorMessage(error, "获取生图历史失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePublicState = async (item: IImageHistoryItem) => {
|
||||||
|
operatingRecordId.value = item.id;
|
||||||
|
errorMessage.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = item.isPublic
|
||||||
|
? await ImageService.HideHistoryImage(item.id)
|
||||||
|
: await ImageService.PublishHistoryImage(item.id);
|
||||||
|
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
item.isPublic = res.data.isPublic;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage.value = res.msg || "更新公开状态失败";
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = getErrorMessage(error, "更新公开状态失败");
|
||||||
|
} finally {
|
||||||
|
operatingRecordId.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(loadHistory);
|
||||||
|
|
||||||
|
watch(isOnline, () => {
|
||||||
|
loadHistory();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="w-full max-w-xl space-y-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-base font-medium text-gray-900">历史记录</h2>
|
||||||
|
<el-button size="small" :loading="loading" @click="loadHistory">
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="errorMessage" class="text-sm text-red-500">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
v-else-if="!loading && histories.length === 0"
|
||||||
|
class="text-sm text-gray-500"
|
||||||
|
>
|
||||||
|
暂无历史记录
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-else class="space-y-4">
|
||||||
|
<article
|
||||||
|
v-for="item in histories"
|
||||||
|
:key="item.id"
|
||||||
|
class="space-y-2 border-b border-gray-200 pb-4"
|
||||||
|
>
|
||||||
|
<p class="text-sm leading-6 text-gray-800">
|
||||||
|
{{ item.prompt }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="item.hostedImageUrl"
|
||||||
|
:src="item.hostedImageUrl"
|
||||||
|
alt="历史生成图片"
|
||||||
|
class="block w-full rounded border border-gray-200 object-contain"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p v-else class="text-sm text-gray-500">图片归档中</p>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-xs text-gray-500">
|
||||||
|
{{ item.isPublic ? "已公开" : "未公开" }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
:loading="operatingRecordId === item.id"
|
||||||
|
@click="updatePublicState(item)"
|
||||||
|
>
|
||||||
|
{{ item.isPublic ? "取消公开" : "公开" }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { IPlazaPostItem } from "#shared/types";
|
||||||
|
import { ImageService } from "~/services";
|
||||||
|
|
||||||
|
const plazaPosts = ref<IPlazaPostItem[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const errorMessage = ref("");
|
||||||
|
|
||||||
|
const loadPlazaPosts = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
errorMessage.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await ImageService.GetPlazaPosts(1, 20);
|
||||||
|
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
plazaPosts.value = res.data.items;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage.value = res.msg || "获取广场图片失败";
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = getErrorMessage(error, "获取广场图片失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(loadPlazaPosts);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="w-full space-y-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-base font-medium text-gray-900">广场</h2>
|
||||||
|
<el-button size="small" :loading="loading" @click="loadPlazaPosts">
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="errorMessage" class="text-sm text-red-500">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p v-else-if="!loading && plazaPosts.length === 0" class="text-sm text-gray-500">
|
||||||
|
暂无广场图片
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-else class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<article
|
||||||
|
v-for="item in plazaPosts"
|
||||||
|
:key="item.id"
|
||||||
|
class="overflow-hidden rounded border border-gray-200 bg-white"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="item.hostedImageUrl"
|
||||||
|
alt="广场图片"
|
||||||
|
class="block w-full object-cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="space-y-2 p-3">
|
||||||
|
<p class="text-sm leading-6 text-gray-800">
|
||||||
|
{{ item.prompt }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
{{ item.authorDisplayName }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="w-full h-dvh flex flex-col">
|
<main class="w-full h-dvh flex flex-col">
|
||||||
<HeaderCom />
|
<HeaderCom />
|
||||||
<div class="min-h-0 flex-1 px-4 overflow-y-auto flex">
|
<div class="min-h-0 flex-1 px-4 overflow-y-auto flex bg-gray-50">
|
||||||
<div
|
<div
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:class="{ 'm-auto max-w-sm': $route.path.includes('login') }"
|
:class="{ 'm-auto max-w-sm': $route.path.includes('login') }"
|
||||||
|
|||||||
+4
-1
@@ -5,7 +5,10 @@ definePageMeta({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="py-2">
|
<div class="space-y-8 py-2">
|
||||||
<ImageGenerateCom />
|
<ImageGenerateCom />
|
||||||
|
<ClientOnly>
|
||||||
|
<ImageHistoryListCom />
|
||||||
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import type {
|
import type {
|
||||||
ICommonResponse,
|
ICommonResponse,
|
||||||
IImageGenerateData,
|
IImageGenerateData,
|
||||||
IImageGenerateRequest
|
IImageGenerateRequest,
|
||||||
|
IImageHistoryListData,
|
||||||
|
IImagePublicStateData,
|
||||||
|
IPlazaPostListData
|
||||||
} from "#shared/types";
|
} from "#shared/types";
|
||||||
|
|
||||||
export class ImageService {
|
export class ImageService {
|
||||||
public static basePath = "/api/images";
|
public static basePath = "/api/images";
|
||||||
|
|
||||||
|
/** 发起生图请求,成功后返回当前可展示的图片地址。 */
|
||||||
public static GenerateImage(request: IImageGenerateRequest) {
|
public static GenerateImage(request: IImageGenerateRequest) {
|
||||||
return $fetch<ICommonResponse<IImageGenerateData>>(
|
return $fetch<ICommonResponse<IImageGenerateData>>(
|
||||||
`${ImageService.basePath}/generate`,
|
`${ImageService.basePath}/generate`,
|
||||||
@@ -16,4 +20,52 @@ export class ImageService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 分页读取当前登录用户的生图历史记录。 */
|
||||||
|
public static GetImageHistory(page = 1, size = 10) {
|
||||||
|
return $fetch<ICommonResponse<IImageHistoryListData>>(
|
||||||
|
`${ImageService.basePath}/history`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
page,
|
||||||
|
size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页读取广场中所有用户可见的公开图片。 */
|
||||||
|
public static GetPlazaPosts(page = 1, size = 20) {
|
||||||
|
return $fetch<ICommonResponse<IPlazaPostListData>>(
|
||||||
|
`${ImageService.basePath}/plaza`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
page,
|
||||||
|
size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将当前用户的一条生图历史发布到广场。 */
|
||||||
|
public static PublishHistoryImage(recordId: string) {
|
||||||
|
return $fetch<ICommonResponse<IImagePublicStateData>>(
|
||||||
|
`${ImageService.basePath}/history/${recordId}/public`,
|
||||||
|
{
|
||||||
|
method: "POST"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消公开当前用户的一条广场图片,服务端会软隐藏发布记录。 */
|
||||||
|
public static HideHistoryImage(recordId: string) {
|
||||||
|
return $fetch<ICommonResponse<IImagePublicStateData>>(
|
||||||
|
`${ImageService.basePath}/history/${recordId}/public`,
|
||||||
|
{
|
||||||
|
method: "DELETE"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-8
@@ -1,4 +1,4 @@
|
|||||||
-- 表1:用户表
|
-- 表1:用户快照表
|
||||||
CREATE TABLE IF NOT EXISTS `users` (
|
CREATE TABLE IF NOT EXISTS `users` (
|
||||||
`id` INT NOT NULL COMMENT 'NewAPI 用户 ID,作为本站用户主键',
|
`id` INT NOT NULL COMMENT 'NewAPI 用户 ID,作为本站用户主键',
|
||||||
`username` VARCHAR(191) NOT NULL DEFAULT '' COMMENT '用户名快照',
|
`username` VARCHAR(191) NOT NULL DEFAULT '' COMMENT '用户名快照',
|
||||||
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS `users` (
|
|||||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户快照表';
|
||||||
|
|
||||||
-- 表2:生图记录表
|
-- 表2:生图记录表
|
||||||
CREATE TABLE IF NOT EXISTS `image_generations` (
|
CREATE TABLE IF NOT EXISTS `image_generations` (
|
||||||
@@ -21,12 +21,12 @@ CREATE TABLE IF NOT EXISTS `image_generations` (
|
|||||||
`started_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '生图开始时间',
|
`started_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '生图开始时间',
|
||||||
`ended_at` DATETIME(3) NULL COMMENT '生图结束时间',
|
`ended_at` DATETIME(3) NULL COMMENT '生图结束时间',
|
||||||
`duration_ms` INT NULL COMMENT '生图耗时,单位毫秒',
|
`duration_ms` INT NULL COMMENT '生图耗时,单位毫秒',
|
||||||
`image_url` VARCHAR(2048) NULL COMMENT 'NewAPI 上游返回的图片 URL',
|
`image_url` VARCHAR(2048) NULL COMMENT '生成图片访问地址',
|
||||||
`hosted_image_url` VARCHAR(2048) NULL COMMENT 'Lsky 图床归档后的图片 URL',
|
`hosted_image_url` VARCHAR(2048) NULL COMMENT '图床归档后的稳定图片地址',
|
||||||
`image_mime_type` VARCHAR(191) NULL COMMENT '图片 MIME 类型',
|
`image_mime_type` VARCHAR(191) NULL COMMENT '图片 MIME 类型',
|
||||||
`revised_prompt` TEXT NULL COMMENT '上游返回的修订提示词',
|
`revised_prompt` TEXT NULL COMMENT '修订提示词',
|
||||||
`upstream_response` JSON NULL COMMENT '完整上游生图接口响应',
|
`upstream_response` JSON NULL COMMENT '完整上游生图响应,仅服务端排查使用',
|
||||||
`hosted_response` JSON NULL COMMENT '完整 Lsky 图床上传接口响应',
|
`hosted_response` JSON NULL COMMENT '完整图床归档响应,仅服务端排查使用',
|
||||||
`error_message` TEXT NULL COMMENT '失败原因或图床归档失败提示',
|
`error_message` TEXT NULL COMMENT '失败原因或图床归档失败提示',
|
||||||
`archive_status` ENUM('NOT_REQUIRED', 'PENDING', 'RUNNING', 'SUCCEEDED', 'FAILED') NOT NULL DEFAULT 'NOT_REQUIRED' COMMENT '图床归档任务状态',
|
`archive_status` ENUM('NOT_REQUIRED', 'PENDING', 'RUNNING', 'SUCCEEDED', 'FAILED') NOT NULL DEFAULT 'NOT_REQUIRED' COMMENT '图床归档任务状态',
|
||||||
`archive_attempts` INT NOT NULL DEFAULT 0 COMMENT '图床归档已尝试次数',
|
`archive_attempts` INT NOT NULL DEFAULT 0 COMMENT '图床归档已尝试次数',
|
||||||
@@ -48,7 +48,30 @@ CREATE TABLE IF NOT EXISTS `image_generations` (
|
|||||||
ON DELETE CASCADE ON UPDATE CASCADE
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='生图记录表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='生图记录表';
|
||||||
|
|
||||||
-- 表3:生图统计表
|
-- 表3:广场发布表
|
||||||
|
CREATE TABLE IF NOT EXISTS `plaza_posts` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '广场发布记录 ID',
|
||||||
|
`image_generation_id` BIGINT UNSIGNED NOT NULL COMMENT '关联的生图记录 ID',
|
||||||
|
`user_id` INT NOT NULL COMMENT 'NewAPI 用户 ID',
|
||||||
|
`status` ENUM('PUBLIC', 'HIDDEN', 'REMOVED') NOT NULL DEFAULT 'PUBLIC' COMMENT '广场发布状态',
|
||||||
|
`published_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '发布时间',
|
||||||
|
`hidden_at` DATETIME(3) NULL COMMENT '用户取消公开时间',
|
||||||
|
`removed_at` DATETIME(3) NULL COMMENT '管理侧下架时间',
|
||||||
|
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE INDEX `plaza_posts_image_generation_id_key` (`image_generation_id`),
|
||||||
|
INDEX `plaza_posts_status_published_at_idx` (`status`, `published_at`),
|
||||||
|
INDEX `plaza_posts_user_id_published_at_idx` (`user_id`, `published_at`),
|
||||||
|
CONSTRAINT `plaza_posts_image_generation_id_fkey`
|
||||||
|
FOREIGN KEY (`image_generation_id`) REFERENCES `image_generations` (`id`)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT `plaza_posts_user_id_fkey`
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='广场发布表';
|
||||||
|
|
||||||
|
-- 表4:生图统计表
|
||||||
CREATE TABLE IF NOT EXISTS `generation_stats` (
|
CREATE TABLE IF NOT EXISTS `generation_stats` (
|
||||||
`id` VARCHAR(32) NOT NULL COMMENT '统计行 ID,固定为 global',
|
`id` VARCHAR(32) NOT NULL COMMENT '统计行 ID,固定为 global',
|
||||||
`total_requests` INT NOT NULL DEFAULT 0 COMMENT '总生图请求数,包含成功和失败',
|
`total_requests` INT NOT NULL DEFAULT 0 COMMENT '总生图请求数,包含成功和失败',
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
CREATE TABLE `plaza_posts` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`image_generation_id` BIGINT UNSIGNED NOT NULL,
|
||||||
|
`user_id` INT NOT NULL,
|
||||||
|
`status` ENUM('PUBLIC', 'HIDDEN', 'REMOVED') NOT NULL DEFAULT 'PUBLIC',
|
||||||
|
`published_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`hidden_at` DATETIME(3) NULL,
|
||||||
|
`removed_at` DATETIME(3) NULL,
|
||||||
|
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE INDEX `plaza_posts_image_generation_id_key` (`image_generation_id`),
|
||||||
|
INDEX `plaza_posts_status_published_at_idx` (`status`, `published_at`),
|
||||||
|
INDEX `plaza_posts_user_id_published_at_idx` (`user_id`, `published_at`),
|
||||||
|
CONSTRAINT `plaza_posts_image_generation_id_fkey`
|
||||||
|
FOREIGN KEY (`image_generation_id`) REFERENCES `image_generations` (`id`)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT `plaza_posts_user_id_fkey`
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -27,6 +27,12 @@ enum ImageArchiveStatus {
|
|||||||
FAILED
|
FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PlazaPostStatus {
|
||||||
|
PUBLIC
|
||||||
|
HIDDEN
|
||||||
|
REMOVED
|
||||||
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @id
|
id Int @id
|
||||||
username String @default("") @db.VarChar(191)
|
username String @default("") @db.VarChar(191)
|
||||||
@@ -37,6 +43,7 @@ model User {
|
|||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
imageGenerations ImageGeneration[]
|
imageGenerations ImageGeneration[]
|
||||||
|
plazaPosts PlazaPost[]
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
@@ -67,6 +74,7 @@ model ImageGeneration {
|
|||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
deletedAt DateTime? @map("deleted_at")
|
deletedAt DateTime? @map("deleted_at")
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
plazaPost PlazaPost?
|
||||||
|
|
||||||
@@index([userId, createdAt])
|
@@index([userId, createdAt])
|
||||||
@@index([status])
|
@@index([status])
|
||||||
@@ -76,6 +84,24 @@ model ImageGeneration {
|
|||||||
@@map("image_generations")
|
@@map("image_generations")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model PlazaPost {
|
||||||
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
||||||
|
imageGenerationId BigInt @unique @map("image_generation_id") @db.UnsignedBigInt
|
||||||
|
userId Int @map("user_id")
|
||||||
|
status PlazaPostStatus @default(PUBLIC)
|
||||||
|
publishedAt DateTime @default(now()) @map("published_at")
|
||||||
|
hiddenAt DateTime? @map("hidden_at")
|
||||||
|
removedAt DateTime? @map("removed_at")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
imageGeneration ImageGeneration @relation(fields: [imageGenerationId], references: [id], onDelete: Cascade)
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([status, publishedAt])
|
||||||
|
@@index([userId, publishedAt])
|
||||||
|
@@map("plaza_posts")
|
||||||
|
}
|
||||||
|
|
||||||
model GenerationStats {
|
model GenerationStats {
|
||||||
id String @id @db.VarChar(32)
|
id String @id @db.VarChar(32)
|
||||||
totalRequests Int @default(0) @map("total_requests")
|
totalRequests Int @default(0) @map("total_requests")
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
// server/api/images/history/[id].get.ts - 生图历史详情接口:返回当前用户单条记录的展示字段与归档状态。
|
// server/api/images/history/[id].get.ts - 生图历史详情接口:返回当前用户单条记录的展示字段与归档状态。
|
||||||
import type { IImageHistoryDetail } from "#shared/types/openai";
|
import type { IImageHistoryDetail } from "#shared/types/openai";
|
||||||
import {
|
|
||||||
clearNewApiAuthCookies,
|
|
||||||
createApiLogger,
|
|
||||||
createErrorResponse,
|
|
||||||
createSuccessResponse,
|
|
||||||
createUpstreamErrorResponse,
|
|
||||||
getImageGenerationDetail,
|
|
||||||
getNewApiUserIdFromCookie,
|
|
||||||
isUnauthorizedError,
|
|
||||||
toSafeLogError
|
|
||||||
} from "~~/server/utils";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/images/history/:id
|
* GET /api/images/history/:id
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
// server/api/images/history/[id]/public.delete.ts - 生图历史取消公开接口:将当前用户广场发布记录软隐藏。
|
||||||
|
import type { IImagePublicStateData } from "#shared/types/openai";
|
||||||
|
import {
|
||||||
|
clearNewApiAuthCookies,
|
||||||
|
createApiLogger,
|
||||||
|
createErrorResponse,
|
||||||
|
createSuccessResponse,
|
||||||
|
createUpstreamErrorResponse,
|
||||||
|
getNewApiUserIdFromCookie,
|
||||||
|
hideImageGenerationFromPlaza,
|
||||||
|
isUnauthorizedError,
|
||||||
|
toSafeLogError
|
||||||
|
} from "~~/server/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/images/history/:id/public
|
||||||
|
*
|
||||||
|
* 流程:
|
||||||
|
* 1. 从 httpOnly cookie 读取当前用户 ID。
|
||||||
|
* 2. 只允许取消公开当前用户、未软删除的生图记录。
|
||||||
|
* 3. 不删除广场发布记录,只改为隐藏并记录 hiddenAt。
|
||||||
|
* 4. 不返回上游响应、图床响应或内部错误详情。
|
||||||
|
*/
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const logger = createApiLogger("images.history.public.hide");
|
||||||
|
logger.info("开始");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userId = getNewApiUserIdFromCookie(event);
|
||||||
|
const recordId = parseRecordId(getRouterParam(event, "id"));
|
||||||
|
|
||||||
|
logger.info("取消公开参数", {
|
||||||
|
userId,
|
||||||
|
recordId: recordId.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await hideImageGenerationFromPlaza(userId, recordId);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
logger.warn("公开记录不存在", {
|
||||||
|
userId,
|
||||||
|
recordId: recordId.toString()
|
||||||
|
});
|
||||||
|
return createErrorResponse(404, "公开记录不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.done("成功", {
|
||||||
|
userId,
|
||||||
|
recordId: recordId.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
return createSuccessResponse<IImagePublicStateData>(result, "已取消公开");
|
||||||
|
} catch (error) {
|
||||||
|
if (isUnauthorizedError(error)) {
|
||||||
|
clearNewApiAuthCookies(event);
|
||||||
|
logger.warn("未登录或登录态失效", {
|
||||||
|
error: toSafeLogError(error)
|
||||||
|
});
|
||||||
|
return createErrorResponse(401, "未登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBadRequestError(error)) {
|
||||||
|
return createErrorResponse(400, "生图记录 ID 不正确");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error("失败", {
|
||||||
|
error: toSafeLogError(error)
|
||||||
|
});
|
||||||
|
return createUpstreamErrorResponse(error, "取消公开图片失败");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 校验并解析路由里的生图记录 ID */
|
||||||
|
const parseRecordId = (value: string | undefined): bigint => {
|
||||||
|
if (!value || !/^\d+$/.test(value)) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: "生图记录 ID 不正确"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return BigInt(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isBadRequestError = (error: unknown): boolean => {
|
||||||
|
return (
|
||||||
|
typeof error === "object" &&
|
||||||
|
error !== null &&
|
||||||
|
"statusCode" in error &&
|
||||||
|
error.statusCode === 400
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// server/api/images/history/[id]/public.post.ts - 生图历史公开接口:将当前用户单条生图记录发布到广场。
|
||||||
|
import type { IImagePublicStateData } from "#shared/types/openai";
|
||||||
|
import {
|
||||||
|
clearNewApiAuthCookies,
|
||||||
|
createApiLogger,
|
||||||
|
createErrorResponse,
|
||||||
|
createSuccessResponse,
|
||||||
|
createUpstreamErrorResponse,
|
||||||
|
getNewApiUserIdFromCookie,
|
||||||
|
isUnauthorizedError,
|
||||||
|
publishImageGenerationToPlaza,
|
||||||
|
toSafeLogError
|
||||||
|
} from "~~/server/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/images/history/:id/public
|
||||||
|
*
|
||||||
|
* 流程:
|
||||||
|
* 1. 从 httpOnly cookie 读取当前用户 ID。
|
||||||
|
* 2. 只允许公开当前用户、未软删除的生图记录。
|
||||||
|
* 3. 允许图片尚未归档时先创建公开记录;广场列表会等 hostedImageUrl 可用后再展示。
|
||||||
|
* 4. 不返回上游响应、图床响应或内部错误详情。
|
||||||
|
*/
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const logger = createApiLogger("images.history.public.publish");
|
||||||
|
logger.info("开始");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userId = getNewApiUserIdFromCookie(event);
|
||||||
|
const recordId = parseRecordId(getRouterParam(event, "id"));
|
||||||
|
|
||||||
|
logger.info("公开参数", {
|
||||||
|
userId,
|
||||||
|
recordId: recordId.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await publishImageGenerationToPlaza(userId, recordId);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
logger.warn("记录不存在", {
|
||||||
|
userId,
|
||||||
|
recordId: recordId.toString()
|
||||||
|
});
|
||||||
|
return createErrorResponse(404, "生图记录不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result === "REMOVED") {
|
||||||
|
logger.warn("记录已下架,不能重新公开", {
|
||||||
|
userId,
|
||||||
|
recordId: recordId.toString()
|
||||||
|
});
|
||||||
|
return createErrorResponse(403, "图片已下架,无法公开");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.done("成功", {
|
||||||
|
userId,
|
||||||
|
recordId: recordId.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
return createSuccessResponse<IImagePublicStateData>(result, "图片已公开");
|
||||||
|
} catch (error) {
|
||||||
|
if (isUnauthorizedError(error)) {
|
||||||
|
clearNewApiAuthCookies(event);
|
||||||
|
logger.warn("未登录或登录态失效", {
|
||||||
|
error: toSafeLogError(error)
|
||||||
|
});
|
||||||
|
return createErrorResponse(401, "未登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBadRequestError(error)) {
|
||||||
|
return createErrorResponse(400, "生图记录 ID 不正确");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error("失败", {
|
||||||
|
error: toSafeLogError(error)
|
||||||
|
});
|
||||||
|
return createUpstreamErrorResponse(error, "公开图片失败");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 校验并解析路由里的生图记录 ID */
|
||||||
|
const parseRecordId = (value: string | undefined): bigint => {
|
||||||
|
if (!value || !/^\d+$/.test(value)) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
statusMessage: "生图记录 ID 不正确"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return BigInt(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isBadRequestError = (error: unknown): boolean => {
|
||||||
|
return (
|
||||||
|
typeof error === "object" &&
|
||||||
|
error !== null &&
|
||||||
|
"statusCode" in error &&
|
||||||
|
error.statusCode === 400
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// server/api/images/plaza.get.ts - 广场列表接口:分页返回所有用户已公开且已归档成功的生图作品。
|
||||||
|
import type { IPlazaPostListData } from "#shared/types/openai";
|
||||||
|
import {
|
||||||
|
createApiLogger,
|
||||||
|
createSuccessResponse,
|
||||||
|
createUpstreamErrorResponse,
|
||||||
|
listPlazaPosts,
|
||||||
|
toSafeLogError
|
||||||
|
} from "~~/server/utils";
|
||||||
|
|
||||||
|
const DEFAULT_PAGE = 1;
|
||||||
|
const DEFAULT_PAGE_SIZE = 20;
|
||||||
|
const MAX_PAGE_SIZE = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/images/plaza
|
||||||
|
*
|
||||||
|
* 流程:
|
||||||
|
* 1. 读取 page/size 查询参数,并限制最大 pageSize。
|
||||||
|
* 2. 只返回公开、生成成功、未删除、且已有图床归档地址的记录。
|
||||||
|
* 3. 返回广场展示字段,不返回上游响应、图床响应或内部错误详情。
|
||||||
|
*/
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const logger = createApiLogger("images.plaza.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 listPlazaPosts({
|
||||||
|
page,
|
||||||
|
pageSize
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.done("成功", {
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
total: result.total,
|
||||||
|
itemCount: result.items.length
|
||||||
|
});
|
||||||
|
|
||||||
|
return createSuccessResponse<IPlazaPostListData>(
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -10,6 +10,7 @@ import { consola } from "consola";
|
|||||||
import {
|
import {
|
||||||
ImageArchiveStatus,
|
ImageArchiveStatus,
|
||||||
ImageGenerationStatus,
|
ImageGenerationStatus,
|
||||||
|
PlazaPostStatus,
|
||||||
Prisma
|
Prisma
|
||||||
} from "~~/app/generated/prisma/client";
|
} from "~~/app/generated/prisma/client";
|
||||||
import { prisma } from "~~/server/utils/prisma";
|
import { prisma } from "~~/server/utils/prisma";
|
||||||
@@ -368,10 +369,17 @@ export const listImageGenerationHistory = async ({
|
|||||||
deletedAt: null
|
deletedAt: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const [total, records] = await prisma.$transaction([
|
const [total, records] = await Promise.all([
|
||||||
prisma.imageGeneration.count({ where }),
|
prisma.imageGeneration.count({ where }),
|
||||||
prisma.imageGeneration.findMany({
|
prisma.imageGeneration.findMany({
|
||||||
where,
|
where,
|
||||||
|
include: {
|
||||||
|
plazaPost: {
|
||||||
|
select: {
|
||||||
|
status: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: "desc"
|
createdAt: "desc"
|
||||||
},
|
},
|
||||||
@@ -398,6 +406,13 @@ export const getImageGenerationDetail = async (
|
|||||||
id: recordId,
|
id: recordId,
|
||||||
userId,
|
userId,
|
||||||
deletedAt: null
|
deletedAt: null
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
plazaPost: {
|
||||||
|
select: {
|
||||||
|
status: true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -549,6 +564,9 @@ const mapImageGenerationItem = (record: {
|
|||||||
revisedPrompt: string | null;
|
revisedPrompt: string | null;
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
plazaPost?: {
|
||||||
|
status: PlazaPostStatus;
|
||||||
|
} | null;
|
||||||
}): IImageHistoryItem => {
|
}): IImageHistoryItem => {
|
||||||
return {
|
return {
|
||||||
id: record.id.toString(),
|
id: record.id.toString(),
|
||||||
@@ -563,6 +581,7 @@ const mapImageGenerationItem = (record: {
|
|||||||
hostedImageUrl: record.hostedImageUrl,
|
hostedImageUrl: record.hostedImageUrl,
|
||||||
revisedPrompt: record.revisedPrompt,
|
revisedPrompt: record.revisedPrompt,
|
||||||
errorMessage: getPublicRecordMessage(record.status, record.errorMessage),
|
errorMessage: getPublicRecordMessage(record.status, record.errorMessage),
|
||||||
|
isPublic: record.plazaPost?.status === PlazaPostStatus.PUBLIC,
|
||||||
createdAt: record.createdAt.toISOString()
|
createdAt: record.createdAt.toISOString()
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ export * from "./lsky";
|
|||||||
export * from "./newApiAuthCookies";
|
export * from "./newApiAuthCookies";
|
||||||
export * from "./newApiTokens";
|
export * from "./newApiTokens";
|
||||||
export * from "./openai";
|
export * from "./openai";
|
||||||
|
export * from "./plazaPosts";
|
||||||
export * from "./prisma";
|
export * from "./prisma";
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
// server/utils/plazaPosts.ts - 广场发布记录数据库操作:公开、取消公开和广场列表查询。
|
||||||
|
import type {
|
||||||
|
IImagePublicStateData,
|
||||||
|
IPlazaPostItem,
|
||||||
|
IPlazaPostListData
|
||||||
|
} from "#shared/types/openai";
|
||||||
|
import {
|
||||||
|
ImageGenerationStatus,
|
||||||
|
PlazaPostStatus
|
||||||
|
} from "~~/app/generated/prisma/client";
|
||||||
|
import { prisma } from "~~/server/utils/prisma";
|
||||||
|
|
||||||
|
const ANONYMOUS_AUTHOR_NAME = "匿名用户";
|
||||||
|
|
||||||
|
export type PublishPlazaPostResult = IImagePublicStateData | null | "REMOVED";
|
||||||
|
|
||||||
|
export const publishImageGenerationToPlaza = async (
|
||||||
|
userId: number,
|
||||||
|
recordId: bigint
|
||||||
|
): Promise<PublishPlazaPostResult> => {
|
||||||
|
const record = await prisma.imageGeneration.findFirst({
|
||||||
|
where: {
|
||||||
|
id: recordId,
|
||||||
|
userId,
|
||||||
|
deletedAt: null
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
userId: true,
|
||||||
|
plazaPost: {
|
||||||
|
select: {
|
||||||
|
status: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!record) return null;
|
||||||
|
if (record.plazaPost?.status === PlazaPostStatus.REMOVED) return "REMOVED";
|
||||||
|
|
||||||
|
await prisma.plazaPost.upsert({
|
||||||
|
where: {
|
||||||
|
imageGenerationId: record.id
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
imageGenerationId: record.id,
|
||||||
|
userId: record.userId,
|
||||||
|
status: PlazaPostStatus.PUBLIC
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
status: PlazaPostStatus.PUBLIC,
|
||||||
|
hiddenAt: null,
|
||||||
|
removedAt: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPublic: true
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hideImageGenerationFromPlaza = async (
|
||||||
|
userId: number,
|
||||||
|
recordId: bigint
|
||||||
|
): Promise<IImagePublicStateData | null> => {
|
||||||
|
const record = await prisma.imageGeneration.findFirst({
|
||||||
|
where: {
|
||||||
|
id: recordId,
|
||||||
|
userId,
|
||||||
|
deletedAt: null
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!record) return null;
|
||||||
|
|
||||||
|
const result = await prisma.plazaPost.updateMany({
|
||||||
|
where: {
|
||||||
|
imageGenerationId: record.id,
|
||||||
|
userId,
|
||||||
|
status: {
|
||||||
|
not: PlazaPostStatus.REMOVED
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: PlazaPostStatus.HIDDEN,
|
||||||
|
hiddenAt: new Date()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.count === 0) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPublic: false
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listPlazaPosts = async ({
|
||||||
|
page,
|
||||||
|
pageSize
|
||||||
|
}: {
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}): Promise<IPlazaPostListData> => {
|
||||||
|
const where = {
|
||||||
|
status: PlazaPostStatus.PUBLIC,
|
||||||
|
imageGeneration: {
|
||||||
|
status: ImageGenerationStatus.SUCCEEDED,
|
||||||
|
deletedAt: null,
|
||||||
|
hostedImageUrl: {
|
||||||
|
not: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [total, posts] = await Promise.all([
|
||||||
|
prisma.plazaPost.count({ where }),
|
||||||
|
prisma.plazaPost.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
imageGeneration: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
prompt: true,
|
||||||
|
hostedImageUrl: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
displayName: true,
|
||||||
|
username: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
publishedAt: "desc"
|
||||||
|
},
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
total,
|
||||||
|
items: posts
|
||||||
|
.map(mapPlazaPostItem)
|
||||||
|
.filter((item): item is IPlazaPostItem => Boolean(item))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapPlazaPostItem = (post: {
|
||||||
|
id: bigint;
|
||||||
|
publishedAt: Date;
|
||||||
|
imageGeneration: {
|
||||||
|
id: bigint;
|
||||||
|
prompt: string;
|
||||||
|
hostedImageUrl: string | null;
|
||||||
|
};
|
||||||
|
user: {
|
||||||
|
displayName: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
}): IPlazaPostItem | null => {
|
||||||
|
if (!post.imageGeneration.hostedImageUrl) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: post.id.toString(),
|
||||||
|
imageGenerationId: post.imageGeneration.id.toString(),
|
||||||
|
prompt: post.imageGeneration.prompt,
|
||||||
|
hostedImageUrl: post.imageGeneration.hostedImageUrl,
|
||||||
|
authorDisplayName:
|
||||||
|
post.user.displayName || post.user.username || ANONYMOUS_AUTHOR_NAME,
|
||||||
|
publishedAt: post.publishedAt.toISOString()
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -62,6 +62,8 @@ export interface IImageHistoryItem {
|
|||||||
revisedPrompt: string | null;
|
revisedPrompt: string | null;
|
||||||
/** 面向前端的本地泛化错误提示;为空且 hostedImageUrl 也为空时表示仍在归档中 */
|
/** 面向前端的本地泛化错误提示;为空且 hostedImageUrl 也为空时表示仍在归档中 */
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
|
/** 此图片是否已发布到广场 */
|
||||||
|
isPublic: boolean;
|
||||||
/** 记录创建时间,ISO 字符串 */
|
/** 记录创建时间,ISO 字符串 */
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -92,6 +94,37 @@ export interface IImageHistoryListData {
|
|||||||
items: IImageHistoryItem[];
|
items: IImageHistoryItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPlazaPostItem {
|
||||||
|
/** 广场发布记录 ID,BigInt 会以字符串返回 */
|
||||||
|
id: string;
|
||||||
|
/** 生图记录 ID,BigInt 会以字符串返回 */
|
||||||
|
imageGenerationId: string;
|
||||||
|
/** 广场卡片展示的用户提示词 */
|
||||||
|
prompt: string;
|
||||||
|
/** 广场使用的图床归档图片地址 */
|
||||||
|
hostedImageUrl: string;
|
||||||
|
/** 广场展示的作者名称 */
|
||||||
|
authorDisplayName: string;
|
||||||
|
/** 发布到广场的时间,ISO 字符串 */
|
||||||
|
publishedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPlazaPostListData {
|
||||||
|
/** 当前页码 */
|
||||||
|
page: number;
|
||||||
|
/** 每页数量 */
|
||||||
|
pageSize: number;
|
||||||
|
/** 当前可见广场记录总数 */
|
||||||
|
total: number;
|
||||||
|
/** 当前页广场记录 */
|
||||||
|
items: IPlazaPostItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IImagePublicStateData {
|
||||||
|
/** 此图片是否已发布到广场 */
|
||||||
|
isPublic: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局生图统计响应。
|
* 全局生图统计响应。
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user