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