Browse Source

feat(markdown): enhance dark mode styling and add post preview functionality

- Updated SCSS for dark mode to use `:where()` for better specificity in styling `.markdown-body`.
- Added transparent background to highlighted code blocks in dark mode.
- Removed unnecessary navigation buttons from various pages to streamline the user interface.
- Introduced a new post preview page to allow users to view posts before publishing, with visibility handling for different post states.

These changes improve the visual consistency in dark mode and enhance the user experience when managing posts.
main
npmrun 2 weeks ago
parent
commit
22ab1e11b8
  1. 4
      app/assets/scss/markdown/green.scss
  2. 4
      app/assets/scss/markdown/highlight.scss
  3. 5
      app/pages/@[publicSlug]/about/index.vue
  4. 3
      app/pages/@[publicSlug]/posts/[postSlug].vue
  5. 7
      app/pages/login/index.vue
  6. 3
      app/pages/me/admin/media-storage.vue
  7. 3
      app/pages/me/media/orphans.vue
  8. 12
      app/pages/me/posts/[id].vue
  9. 26
      app/pages/me/posts/index.vue
  10. 11
      app/pages/me/posts/new.vue
  11. 101
      app/pages/me/posts/preview/[id].vue
  12. 7
      app/pages/register/index.vue
  13. BIN
      packages/drizzle-pkg/db.sqlite

4
app/assets/scss/markdown/green.scss

@ -32,7 +32,7 @@
--markdown-padding: 1.6em;
}
.dark .markdown-body {
:where(.dark, [data-theme='dark'], [data-color-mode='dark']) .markdown-body {
--color-fg-default: #e5e7eb;
--color-fg-muted: #c0c7d1;
--color-fg-subtle: #9ca3af;
@ -293,7 +293,7 @@
@include chinese;
}
.dark .markdown-body.green {
:where(.dark, [data-theme='dark'], [data-color-mode='dark']) .markdown-body.green {
.code-block-wrapper {
border-color: var(--code-border);
background: var(--code-bg);

4
app/assets/scss/markdown/highlight.scss

@ -3,6 +3,7 @@
pre {
code.hljs {
color: #1f2937;
background: transparent;
}
.hljs-comment,
@ -52,11 +53,12 @@
}
}
.dark .markdown-body {
:where(.dark, [data-theme='dark'], [data-color-mode='dark']) .markdown-body {
.code-block-wrapper {
pre {
code.hljs {
color: #e5e7eb;
background: transparent;
}
.hljs-comment,

5
app/pages/@[publicSlug]/about/index.vue

@ -64,10 +64,5 @@ usePageTitle(() =>
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>

3
app/pages/@[publicSlug]/posts/[postSlug].vue

@ -116,9 +116,6 @@ function exportMarkdown(): void {
<UAlert v-else-if="error && !data" color="error" title="文章不存在或未公开" />
<template v-else-if="data">
<div class="flex flex-wrap items-center gap-2">
<UButton :to="`/@${publicSlug}`" variant="ghost" color="neutral" size="sm" class="-ml-2">
返回主页
</UButton>
<UButton
v-if="editPostHref"
:to="editPostHref"

7
app/pages/login/index.vue

@ -122,13 +122,6 @@ const onSubmit = async (_event: FormSubmitEvent<LoginFormState>) => {
<template>
<div class="min-h-screen flex items-start justify-center px-4 pt-14 pb-8 md:pt-20">
<div class="w-full max-w-md space-y-4">
<div class="text-center space-y-1">
<NuxtLink to="/" class="inline-flex items-center gap-2 text-sm text-muted hover:text-primary transition-colors">
<UIcon name="i-lucide-arrow-left" class="size-4" />
返回首页
</NuxtLink>
</div>
<UCard class="shadow-lg">
<template #header>
<div class="space-y-1 text-center">

3
app/pages/me/admin/media-storage.vue

@ -201,9 +201,6 @@ onMounted(async () => {
对有引用但缺文件的记录可使用<strong>重新上传</strong>按原 <code class="text-xs">storage_key</code> 写回 WebP不破坏文章/资料中的链接
</p>
</div>
<UButton to="/me" variant="ghost" size="sm">
返回控制台
</UButton>
</div>
<div class="flex flex-wrap items-center gap-3">

3
app/pages/me/media/orphans.vue

@ -165,9 +165,6 @@ async function executeDelete() {
未被任何文章引用的上传图片宽限期结束后可删除
</p>
</div>
<UButton to="/me" variant="ghost" size="sm">
返回控制台
</UButton>
</div>
<div class="flex flex-wrap items-end gap-x-5 gap-y-3">

12
app/pages/me/posts/[id].vue

@ -26,6 +26,9 @@ const visibilityItems = [
const bodyLength = computed(() => state.bodyMarkdown.trim().length)
const publicPostHref = computed(() => {
if (state.visibility !== 'public') {
return `/me/posts/preview/${id.value}`
}
const ps = user.value?.publicSlug
if (!ps || !state.slug) {
return ''
@ -158,12 +161,6 @@ async function copyShareUrl() {
>
详情
</UButton>
<UButton to="/me/posts" variant="ghost" color="neutral" size="sm">
返回列表
</UButton>
<!-- <UButton type="submit" form="edit-post-form" :loading="saving" size="sm">
保存文章
</UButton> -->
</div>
</div>
@ -271,9 +268,6 @@ async function copyShareUrl() {
<UButton type="submit" :loading="saving" block>
保存文章
</UButton>
<UButton to="/me/posts" variant="ghost" color="neutral" block>
取消并返回
</UButton>
<UButton color="error" variant="soft" type="button" block @click="remove">
删除文章
</UButton>

26
app/pages/me/posts/index.vue

@ -3,7 +3,8 @@ import { useAuthSession } from '../../../composables/useAuthSession'
usePageTitle('我的文章')
type Row = { id: number; title: string; slug: string; visibility: string }
type Visibility = 'private' | 'unlisted' | 'public'
type Row = { id: number; title: string; slug: string; visibility: Visibility }
type ViewMode = 'list' | 'card'
const posts = ref<Row[]>([])
@ -26,12 +27,15 @@ onMounted(() => {
void load()
})
function postDetailHref(slug: string) {
function postDetailHref(post: Row) {
if (post.visibility !== 'public') {
return `/me/posts/preview/${post.id}`
}
const ps = user.value?.publicSlug
if (!ps || !slug) {
if (!ps || !post.slug) {
return ''
}
return `/@${ps}/posts/${encodeURIComponent(slug)}`
return `/@${ps}/posts/${encodeURIComponent(post.slug)}`
}
function visibilityLabel(visibility: string) {
@ -52,8 +56,9 @@ function visibilityLabel(visibility: string) {
文章
</h1>
<div class="flex items-center gap-2">
<UButtonGroup size="sm" class="rounded-lg border border-default p-0.5 bg-elevated/40">
<div class="flex items-center rounded-lg border border-default p-0.5 bg-elevated/40">
<UButton
size="sm"
:variant="viewMode === 'list' ? 'soft' : 'ghost'"
color="neutral"
icon="i-lucide-list"
@ -63,6 +68,7 @@ function visibilityLabel(visibility: string) {
列表
</UButton>
<UButton
size="sm"
:variant="viewMode === 'card' ? 'soft' : 'ghost'"
color="neutral"
icon="i-lucide-layout-grid"
@ -71,7 +77,7 @@ function visibilityLabel(visibility: string) {
>
卡片
</UButton>
</UButtonGroup>
</div>
<UButton to="/me/posts/new">
新建
</UButton>
@ -98,8 +104,8 @@ function visibilityLabel(visibility: string) {
</div>
<div class="flex flex-wrap gap-1 justify-end">
<UButton
v-if="postDetailHref(p.slug)"
:to="postDetailHref(p.slug)"
v-if="postDetailHref(p)"
:to="postDetailHref(p)"
size="xs"
variant="soft"
color="neutral"
@ -132,8 +138,8 @@ function visibilityLabel(visibility: string) {
<template #footer>
<div class="flex flex-wrap gap-2 justify-end">
<UButton
v-if="postDetailHref(p.slug)"
:to="postDetailHref(p.slug)"
v-if="postDetailHref(p)"
:to="postDetailHref(p)"
size="xs"
variant="soft"
color="neutral"

11
app/pages/me/posts/new.vue

@ -79,14 +79,6 @@ async function submit() {
先填写标题和正文再在右侧完成发布设置
</p>
</div>
<div class="flex items-center gap-2">
<UButton to="/me/posts" variant="ghost" color="neutral">
返回列表
</UButton>
<!-- <UButton type="submit" form="new-post-form" :loading="loading">
创建文章
</UButton> -->
</div>
</div>
<UForm
@ -158,9 +150,6 @@ async function submit() {
<UButton type="submit" :loading="loading" block>
创建文章
</UButton>
<UButton to="/me/posts" variant="ghost" color="neutral" block>
取消并返回
</UButton>
</UCard>
</div>
</UForm>

101
app/pages/me/posts/preview/[id].vue

@ -0,0 +1,101 @@
<script setup lang="ts">
import { unwrapApiBody, type ApiResponse } from '../../../../utils/http/factory'
import { renderSafeMarkdown } from '../../../../utils/render-markdown'
import { formatOccurredOnDisplay, occurredOnToIsoAttr } from '../../../../utils/timeline-datetime'
const route = useRoute()
const requestFetch = useRequestFetch()
const id = computed(() => route.params.id as string)
type Post = {
id: number
title: string
slug: string
excerpt: string
bodyMarkdown: string
coverUrl: string | null
publishedAt: Date | null
visibility: 'private' | 'unlisted' | 'public'
shareToken?: string | null
}
const { data, pending, error } = await useAsyncData(
() => `me-post-preview-${id.value}`,
async () => {
const res = await requestFetch<ApiResponse<{ post: Post }>>(`/api/me/posts/${encodeURIComponent(id.value)}`)
return unwrapApiBody(res).post
},
{ watch: [id] },
)
const renderedBody = computed(() =>
data.value ? renderSafeMarkdown(data.value.bodyMarkdown) : '',
)
const publishedAtLabel = computed(() =>
data.value?.publishedAt != null ? formatOccurredOnDisplay(data.value.publishedAt) : '',
)
const publishedAtIso = computed(() =>
data.value?.publishedAt != null ? occurredOnToIsoAttr(data.value.publishedAt) : '',
)
const visibilityLabel = computed(() => {
if (!data.value) {
return ''
}
if (data.value.visibility === 'private') {
return '私密预览'
}
if (data.value.visibility === 'unlisted') {
return '仅链接预览'
}
return '公开预览'
})
usePageTitle(() => {
const t = data.value?.title?.trim()
return t ? [t, '预览'] : ['文章预览']
})
</script>
<template>
<UContainer class="py-10 space-y-6">
<div v-if="pending && !data" class="text-muted">
加载中
</div>
<UAlert v-else-if="error && !data" color="error" title="文章不存在或无权限查看" />
<template v-else-if="data">
<div class="flex flex-wrap items-center gap-2">
<UBadge color="neutral" variant="soft">
{{ visibilityLabel }}
</UBadge>
<UButton :to="`/me/posts/${data.id}`" color="neutral" variant="soft" size="sm">
编辑
</UButton>
</div>
<div v-if="data.coverUrl" class="flex justify-center">
<img
:src="data.coverUrl"
alt=""
class="max-h-64 w-full max-w-2xl rounded-lg object-cover border border-default"
>
</div>
<p v-if="publishedAtLabel" class="text-sm tabular-nums text-muted">
发布于
<time :datetime="publishedAtIso" class="text-default">{{ publishedAtLabel }}</time>
</p>
<h1 class="text-2xl font-semibold">
{{ data.title }}
</h1>
<p v-if="data.excerpt" class="text-muted">
{{ data.excerpt }}
</p>
<article
class="prose prose-neutral dark:prose-invert max-w-none prose-a:text-primary prose-img:rounded-lg prose-headings:text-highlighted prose-p:text-default prose-strong:text-highlighted markdown-body green"
v-html="renderedBody"
/>
</template>
</UContainer>
</template>

7
app/pages/register/index.vue

@ -119,13 +119,6 @@ const onSubmit = async (_event: FormSubmitEvent<RegisterFormState>) => {
<template>
<div class="min-h-screen flex items-start justify-center px-4 pt-14 pb-8 md:pt-20">
<div class="w-full max-w-md space-y-4">
<div class="text-center space-y-1">
<NuxtLink to="/" class="inline-flex items-center gap-2 text-sm text-muted hover:text-primary transition-colors">
<UIcon name="i-lucide-arrow-left" class="size-4" />
返回首页
</NuxtLink>
</div>
<UCard class="shadow-lg">
<template #header>
<div class="space-y-1 text-center">

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.
Loading…
Cancel
Save