13 changed files with 1362 additions and 28 deletions
@ -0,0 +1,73 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { unwrapApiBody, type ApiResponse } from '../../../utils/http/factory' |
||||
|
import { renderSafeMarkdown } from '../../../utils/render-markdown' |
||||
|
|
||||
|
definePageMeta({ |
||||
|
layout: 'public', |
||||
|
}) |
||||
|
|
||||
|
const route = useRoute() |
||||
|
const slug = computed(() => route.params.publicSlug as string) |
||||
|
|
||||
|
type AboutPayload = { |
||||
|
user: { publicSlug: string | null; nickname: string | null; avatar: string | null } |
||||
|
bio: { markdown: string } |
||||
|
} |
||||
|
|
||||
|
const { data, pending, error } = await useAsyncData( |
||||
|
() => `public-about-${slug.value}`, |
||||
|
async () => { |
||||
|
const res = await $fetch<ApiResponse<AboutPayload>>( |
||||
|
`/api/public/profile/${encodeURIComponent(slug.value)}/about`, |
||||
|
) |
||||
|
return unwrapApiBody(res) |
||||
|
}, |
||||
|
{ watch: [slug] }, |
||||
|
) |
||||
|
|
||||
|
const renderedBio = computed(() => (data.value?.bio.markdown ? renderSafeMarkdown(data.value.bio.markdown) : '')) |
||||
|
|
||||
|
const displayName = computed( |
||||
|
() => data.value?.user.nickname || data.value?.user.publicSlug || slug.value, |
||||
|
) |
||||
|
|
||||
|
useHead(() => ({ |
||||
|
title: data.value ? `${displayName.value} · 关于` : '关于', |
||||
|
})) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div v-if="pending" class="text-muted py-10"> |
||||
|
<UContainer>加载中…</UContainer> |
||||
|
</div> |
||||
|
<UContainer v-else-if="error" class="py-10"> |
||||
|
<UAlert color="error" title="无法加载页面" description="该用户没有公开的简介,或主页不存在。" /> |
||||
|
</UContainer> |
||||
|
<UContainer v-else-if="data" class="py-10 max-w-3xl space-y-8"> |
||||
|
<div class="flex flex-col items-center gap-3 sm:flex-row sm:items-start"> |
||||
|
<img |
||||
|
v-if="data.user.avatar" |
||||
|
:src="data.user.avatar" |
||||
|
alt="" |
||||
|
class="h-20 w-20 shrink-0 rounded-full border border-default object-cover" |
||||
|
> |
||||
|
<div class="text-center sm:text-left"> |
||||
|
<h1 class="text-2xl font-semibold"> |
||||
|
{{ displayName }} |
||||
|
</h1> |
||||
|
<p class="mt-1 text-sm text-muted"> |
||||
|
关于 |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div |
||||
|
class="prose prose-neutral dark:prose-invert max-w-none prose-img:rounded-lg" |
||||
|
v-html="renderedBio" |
||||
|
/> |
||||
|
<div> |
||||
|
<UButton :to="`/@${slug}`" variant="ghost" icon="i-lucide-arrow-left"> |
||||
|
返回主页 |
||||
|
</UButton> |
||||
|
</div> |
||||
|
</UContainer> |
||||
|
</template> |
||||
Binary file not shown.
@ -0,0 +1,16 @@ |
|||||
|
CREATE TABLE `media_refs_new` ( |
||||
|
`owner_type` text NOT NULL, |
||||
|
`owner_id` integer NOT NULL, |
||||
|
`asset_id` integer NOT NULL, |
||||
|
PRIMARY KEY(`owner_type`, `owner_id`, `asset_id`), |
||||
|
FOREIGN KEY (`asset_id`) REFERENCES `media_assets`(`id`) ON UPDATE no action ON DELETE cascade |
||||
|
); |
||||
|
--> statement-breakpoint |
||||
|
INSERT INTO `media_refs_new` (`owner_type`, `owner_id`, `asset_id`) |
||||
|
SELECT 'post', `post_id`, `asset_id` FROM `media_refs`; |
||||
|
--> statement-breakpoint |
||||
|
DROP TABLE `media_refs`; |
||||
|
--> statement-breakpoint |
||||
|
ALTER TABLE `media_refs_new` RENAME TO `media_refs`; |
||||
|
--> statement-breakpoint |
||||
|
CREATE INDEX `media_refs_asset_id_idx` ON `media_refs` (`asset_id`); |
||||
File diff suppressed because it is too large
@ -0,0 +1,34 @@ |
|||||
|
import { dbGlobal } from "drizzle-pkg/lib/db"; |
||||
|
import { users } from "drizzle-pkg/lib/schema/auth"; |
||||
|
import { and, eq } from "drizzle-orm"; |
||||
|
|
||||
|
export default defineEventHandler(async (event) => { |
||||
|
const publicSlug = event.context.params?.publicSlug; |
||||
|
if (!publicSlug || typeof publicSlug !== "string") { |
||||
|
throw createError({ statusCode: 400, statusMessage: "无效主页" }); |
||||
|
} |
||||
|
|
||||
|
const [owner] = await dbGlobal |
||||
|
.select() |
||||
|
.from(users) |
||||
|
.where(and(eq(users.publicSlug, publicSlug), eq(users.status, "active"))) |
||||
|
.limit(1); |
||||
|
|
||||
|
if (!owner) { |
||||
|
throw createError({ statusCode: 404, statusMessage: "未找到" }); |
||||
|
} |
||||
|
|
||||
|
const bioOk = owner.bioVisibility === "public" && Boolean(owner.bioMarkdown?.trim()); |
||||
|
if (!bioOk) { |
||||
|
throw createError({ statusCode: 404, statusMessage: "未找到" }); |
||||
|
} |
||||
|
|
||||
|
return R.success({ |
||||
|
user: { |
||||
|
publicSlug: owner.publicSlug, |
||||
|
nickname: owner.nickname, |
||||
|
avatar: owner.avatarVisibility === "public" ? owner.avatar : null, |
||||
|
}, |
||||
|
bio: { markdown: owner.bioMarkdown as string }, |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,3 @@ |
|||||
|
export const MEDIA_REF_OWNER_POST = "post" as const; |
||||
|
export const MEDIA_REF_OWNER_PROFILE = "profile" as const; |
||||
|
export type MediaRefOwnerType = typeof MEDIA_REF_OWNER_POST | typeof MEDIA_REF_OWNER_PROFILE; |
||||
Loading…
Reference in new issue