feat: 一部分优化

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-26 23:48:53 +08:00
parent f7bdedf7d2
commit b3c2c88be3
7 changed files with 96 additions and 26 deletions
+2 -2
View File
@@ -24,14 +24,14 @@ const { isOnline, userInfo } = storeToRefs(userStore);
<el-popover
v-if="isOnline"
placement="bottom"
placement="bottom-end"
trigger="click"
:show-arrow="false"
width="auto"
popper-class="!min-w-0 !p-0 !rounded-lg"
>
<template #reference>
<img :src="avatar" class="h-8 w-8 rounded-full cursor-pointer" />
<img :src="avatar" class="size-8 rounded-full cursor-pointer" />
</template>
<div v-if="!$route.path.includes('login')" class="py-2">
+2 -2
View File
@@ -69,7 +69,7 @@ watch(isOnline, () => {
</script>
<template>
<section class="w-full max-w-xl space-y-3">
<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="loadHistory">
@@ -103,7 +103,7 @@ watch(isOnline, () => {
v-if="item.hostedImageUrl"
:src="item.hostedImageUrl"
alt="历史生成图片"
class="block w-full rounded border border-gray-200 object-contain"
class="block w-52 rounded border border-gray-200 object-contain"
/>
<p v-else class="text-sm text-gray-500">图片归档中</p>
+50 -20
View File
@@ -3,7 +3,19 @@
import type { IPlazaPostItem } from "#shared/types";
import type { VueInfiniteGridEvents } from "@egjs/vue3-infinitegrid";
import { MasonryInfiniteGrid } from "@egjs/vue3-infinitegrid";
import { debounce } from "lodash-es";
import { ImageService } from "~/services";
import { ExclamationTriangleIcon } from "@heroicons/vue/24/outline";
// 网格组件实例引用,用于图片加载后主动触发重排
const gridRef = ref<InstanceType<typeof MasonryInfiniteGrid> | null>(null);
// el-image 异步加载完成后,网格需要重新测量各项 DOM 高度才能修正布局。
// renderItems({ useResize: true }) 会强制重新量取 DOM 尺寸(而非使用缓存),
// 用 debounce 合并同批次多张图片的 load 事件,避免频繁重排。
const onImageLoad = debounce(() => {
gridRef.value?.renderItems({ useResize: true });
}, 100);
type RequestAppendEvent = Parameters<VueInfiniteGridEvents["requestAppend"]>[0];
@@ -24,19 +36,6 @@ const GRID_GAP = 8;
const containerEl = ref<HTMLElement | null>(null);
const itemWidth = ref(0);
onMounted(() => {
const measure = () => {
if (!containerEl.value) return;
itemWidth.value = Math.floor(
(containerEl.value.offsetWidth - GRID_GAP) / 2
);
};
measure();
const ro = new ResizeObserver(measure);
if (containerEl.value) ro.observe(containerEl.value);
onUnmounted(() => ro.disconnect());
});
const loadPlazaPosts = async (page = 1) => {
loading.value = true;
errorMessage.value = "";
@@ -79,7 +78,24 @@ const onRequestAppend = async (event: RequestAppendEvent) => {
}
};
onMounted(refreshPlazaPosts);
onMounted(() => {
const measure = () => {
if (!containerEl.value) return;
itemWidth.value = Math.floor(
(containerEl.value.offsetWidth - GRID_GAP) / 2
);
};
measure();
const ro = new ResizeObserver(measure);
if (containerEl.value) ro.observe(containerEl.value);
onUnmounted(() => {
ro.disconnect();
});
refreshPlazaPosts();
});
</script>
<template>
@@ -105,6 +121,7 @@ onMounted(refreshPlazaPosts);
<!-- itemWidth > 0 确保容器宽度测量完毕后再挂载避免 egjs 0 宽初始化 -->
<ClientOnly v-if="plazaPosts.length > 0 && itemWidth > 0">
<MasonryInfiniteGrid
ref="gridRef"
class="w-full"
:gap="GRID_GAP"
:column-size="itemWidth"
@@ -114,19 +131,32 @@ onMounted(refreshPlazaPosts);
:is-reach-end="isReachEnd"
@request-append="onRequestAppend"
>
<article
<div
v-for="(item, index) in plazaPosts"
:key="item.id"
:data-grid-groupkey="getGroupKey(index)"
:style="{ width: itemWidth + 'px' }"
class="overflow-hidden rounded border border-gray-200 bg-white"
>
<img
<el-image
:src="item.hostedImageUrl"
alt="广场图片"
:max-scale="5"
:min-scale="0.5"
:preview-src-list="[item.hostedImageUrl]"
:hide-on-click-modal="true"
class="block w-full object-cover"
/>
@load="onImageLoad"
@error="onImageLoad"
>
<template #error>
<div
class="w-full flex flex-col gap-2 items-center justify-center p-4 pt-8"
>
<ExclamationTriangleIcon class="size-6 text-gray-400" />
<div>图片加载失败</div>
</div>
</template>
</el-image>
<div class="space-y-2 p-3">
<p class="text-sm leading-6 text-gray-800">
{{ item.prompt }}
@@ -136,7 +166,7 @@ onMounted(refreshPlazaPosts);
{{ item.authorDisplayName }}
</p>
</div>
</article>
</div>
</MasonryInfiniteGrid>
</ClientOnly>
</section>
+1 -1
View File
@@ -2,5 +2,5 @@
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt(
// Your custom configs here
{ ignores: ["node_modules/**"] }
)
+2
View File
@@ -23,6 +23,8 @@
"@prisma/adapter-mariadb": "^7.8.0",
"@prisma/client": "^7.8.0",
"@takumi-rs/core": "^1.0.16",
"@types/lodash-es": "^4.17.12",
"lodash-es": "^4.18.1",
"nuxt": "^4.4.2",
"openai": "^6.34.0",
"pinia": "^3.0.4",
+6
View File
@@ -41,6 +41,12 @@ importers:
'@takumi-rs/core':
specifier: ^1.0.16
version: 1.0.16(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
lodash-es:
specifier: ^4.18.1
version: 4.18.1
nuxt:
specifier: ^4.4.2
version: 4.4.2(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@electric-sql/pglite@0.4.1)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@parcel/watcher@2.5.6)(@types/node@25.6.0)(@vue/compiler-sfc@3.5.33)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.1)(mysql2@3.15.3))(eslint@10.2.1(jiti@2.6.1))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.2)(mysql2@3.15.3)(optionator@0.9.4)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.33(typescript@6.0.3)))(rollup-plugin-visualizer@7.0.1(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.1)(typescript@6.0.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.3)
+33 -1
View File
@@ -1,4 +1,6 @@
// server/utils/plazaPosts.ts - 广场发布记录数据库操作:公开、取消公开广场列表查询。
// server/utils/plazaPosts.ts - 广场发布记录数据库操作:公开、取消公开广场列表查询。
// 广场帖子与 ImageGeneration 一对一关联,状态流转:PUBLIC → HIDDEN(用户自主) / REMOVED(管理员下架,不可恢复)。
// 本文件不向调用方暴露上游响应、图床响应或内部错误字段。
import type {
IImagePublicStateData,
IPlazaPostItem,
@@ -10,10 +12,18 @@ import {
} from "~~/app/generated/prisma/client";
import { prisma } from "~~/server/utils/prisma";
/** 作者未设置昵称时的展示兜底文案 */
const ANONYMOUS_AUTHOR_NAME = "匿名用户";
/** 发布操作的三种结果:成功返回公开状态、记录不存在返回 null、已被管理员下架返回 "REMOVED" */
export type PublishPlazaPostResult = IImagePublicStateData | null | "REMOVED";
/**
* 将指定生图记录发布到广场。
* - 记录不存在或已软删除时返回 null。
* - 记录已被管理员 REMOVED 时返回 "REMOVED",调用方应提示用户无法重新发布。
* - 其余情况 upsert 为 PUBLIC,允许重复调用(幂等)。
*/
export const publishImageGenerationToPlaza = async (
userId: number,
recordId: bigint
@@ -59,6 +69,12 @@ export const publishImageGenerationToPlaza = async (
};
};
/**
* 将指定生图记录从广场隐藏(用户主动操作)。
* - 记录不存在或已软删除时返回 null。
* - 帖子不存在或已被 REMOVED 时 updateMany count 为 0,同样返回 null。
* - 不允许将 REMOVED 状态改为 HIDDENREMOVED 由管理员操作,不在此处处理。
*/
export const hideImageGenerationFromPlaza = async (
userId: number,
recordId: bigint
@@ -97,6 +113,17 @@ export const hideImageGenerationFromPlaza = async (
};
};
/**
* 分页查询广场公开列表。
* 只返回满足以下全部条件的记录:
* - PlazaPost.status = PUBLIC
* - 关联的 ImageGeneration.status = SUCCEEDED
* - 关联的 ImageGeneration 未软删除
* - 关联的 ImageGeneration.hostedImageUrl 不为 null(图床归档完成)
*
* count 与 findMany 并行执行以减少网络往返耗时。
* 返回字段仅包含广场展示所需的公开信息,不含上游响应、图床响应或错误详情。
*/
export const listPlazaPosts = async ({
page,
pageSize
@@ -152,6 +179,11 @@ export const listPlazaPosts = async ({
};
};
/**
* 将 Prisma 查询结果映射为广场展示条目。
* hostedImageUrl 在 where 条件中已过滤,此处二次检查是为了满足类型收窄。
* 映射失败(理论上不应发生)返回 null,由上层 filter 去除。
*/
const mapPlazaPostItem = (post: {
id: bigint;
publishedAt: Date;