You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

234 lines
6.7 KiB

<script setup lang="ts">
import { generateRandomPostSlug } from '../../../utils/post-slug'
import { clearPostPreviewDrafts, createPostPreviewDraft } from '../../../utils/post-preview-draft'
usePageTitle('新建文章')
const { fetchData } = useClientApi()
const toast = useToast()
const state = reactive({
title: '',
slug: '',
excerpt: '',
bodyMarkdown: '',
tags: [] as string[],
visibility: 'private',
})
const loading = ref(false)
const availableTags = ref<string[]>([])
const visibilityItems = [
{ label: '私密', value: 'private' },
{ label: '公开', value: 'public' },
{ label: '仅链接', value: 'unlisted' },
]
const bodyLength = computed(() => state.bodyMarkdown.trim().length)
function generateSlugFromTitle() {
const previous = state.slug
const normalized = generateRandomPostSlug()
state.slug = normalized
if (!state.slug) {
toast.add({ title: '未生成 slug,请检查标题内容', color: 'warning' })
return
}
if (state.slug === previous) {
toast.add({ title: 'slug 未变化', color: 'neutral' })
return
}
toast.add({ title: '已生成 slug', color: 'success' })
}
async function submit() {
loading.value = true
try {
const { post } = await fetchData<{ post: { id: number } }>('/api/me/posts', {
method: 'POST',
body: {
title: state.title,
slug: state.slug,
excerpt: state.excerpt,
bodyMarkdown: state.bodyMarkdown,
tags: state.tags,
visibility: state.visibility,
},
})
const id = post.id
if (import.meta.client) {
clearPostPreviewDrafts()
}
toast.add({ title: '文章已创建', color: 'success' })
await navigateTo(`/me/posts/${id}`)
} finally {
loading.value = false
}
}
async function loadTagSuggestions() {
try {
const data = await fetchData<{ availableTags?: string[] }>('/api/me/posts')
availableTags.value = data.availableTags ?? []
} catch {
availableTags.value = []
}
}
onMounted(() => {
void loadTagSuggestions()
})
function openPreviewInNewPage() {
if (!import.meta.client) {
return
}
const key = createPostPreviewDraft({
title: state.title,
excerpt: state.excerpt,
bodyMarkdown: state.bodyMarkdown,
visibility: state.visibility as 'private' | 'unlisted' | 'public',
})
const url = `/me/posts/preview/draft?key=${encodeURIComponent(key)}`
window.open(url, '_blank', 'noopener')
}
</script>
<template>
<UContainer class="py-8 max-w-[1600px] space-y-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="space-y-1">
<h1 class="text-2xl font-semibold tracking-tight">
新建文章
</h1>
<p class="text-sm text-muted">
先填写标题和正文再在右侧完成发布设置
</p>
</div>
</div>
<UForm
id="new-post-form"
:state="state"
class="grid gap-6 min-w-0 xl:grid-cols-[minmax(0,1fr)_280px]"
@submit.prevent="submit"
>
<div class="space-y-6 min-w-0">
<UCard :ui="{ body: 'p-4 sm:p-6 space-y-4' }">
<UFormField label="标题" name="title" required class="w-full">
<UInput
v-model="state.title"
class="w-full"
placeholder="例如:我的 2026 开发工作流复盘"
/>
</UFormField>
<UFormField label="摘要" name="excerpt" required class="w-full">
<UTextarea
v-model="state.excerpt"
class="w-full"
:rows="3"
autoresize
placeholder="一句话概括文章核心内容,便于列表页快速浏览。"
/>
</UFormField>
</UCard>
<UCard :ui="{ body: 'p-4 sm:p-6 space-y-3' }">
<div class="flex items-center justify-between gap-3">
<h2 class="text-base font-medium">
正文内容
</h2>
<span class="text-xs text-muted">字数 {{ bodyLength }}</span>
</div>
<PostBodyMarkdownEditor v-model="state.bodyMarkdown" />
<UFormField label="正文" name="bodyMarkdown" required class="sr-only" />
</UCard>
</div>
<div class="side-rail-scroll space-y-6 xl:sticky xl:top-20 xl:self-start xl:max-h-[calc(100vh-5rem)] xl:overflow-y-auto xl:pr-1">
<UCard :ui="{ body: 'p-4 sm:p-5 space-y-4' }">
<h2 class="text-base font-medium">
发布设置
</h2>
<UFormField label="slug" name="slug" required>
<div class="flex items-center gap-2">
<UInput v-model="state.slug" placeholder="my-post-slug" class="flex-1" />
<UButton
type="button"
variant="soft"
color="neutral"
icon="i-lucide-wand-sparkles"
label="生成"
@click="generateSlugFromTitle"
/>
</div>
</UFormField>
<UFormField label="可见性" name="visibility">
<USelect v-model="state.visibility" :items="visibilityItems" />
</UFormField>
</UCard>
<UCard :ui="{ body: 'p-4 sm:p-5 space-y-3' }">
<h2 class="text-base font-medium">
标签
</h2>
<p class="text-xs text-muted">
用于列表筛选与归类;可与正文主题分开管理。
</p>
<UFormField label="文章标签" name="tags" class="w-full">
<PostTagsInput
v-model="state.tags"
:suggestions="availableTags"
placeholder="输入后回车,例如:复盘、Nuxt"
/>
</UFormField>
</UCard>
<UCard :ui="{ body: 'p-4 sm:p-5 space-y-3' }">
<h2 class="text-base font-medium">
操作
</h2>
<UButton type="button" color="neutral" variant="soft" block @click="openPreviewInNewPage">
预览新窗口
</UButton>
<UButton type="submit" :loading="loading" block>
创建文章
</UButton>
</UCard>
</div>
</UForm>
</UContainer>
</template>
<style scoped>
@media (min-width: 1280px) {
.side-rail-scroll {
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.55) transparent;
}
.side-rail-scroll::-webkit-scrollbar {
width: 10px;
}
.side-rail-scroll::-webkit-scrollbar-track {
background: transparent;
}
.side-rail-scroll::-webkit-scrollbar-thumb {
border-radius: 9999px;
background: rgba(148, 163, 184, 0.45);
border: 2px solid transparent;
background-clip: content-box;
}
.side-rail-scroll::-webkit-scrollbar-thumb:hover {
background: rgba(148, 163, 184, 0.7);
background-clip: content-box;
}
}
</style>