PersonalWebApplication/Client/src/components/PostEditor.tsx
cc f1487f509e feat(blog): PostEditor 写/改文章(含 Markdown 预览)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 16:52:36 +08:00

108 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react'
import { createPost, updatePost, type Post, type PostError } from '../api/posts'
import { Markdown } from './Markdown'
interface PostEditorProps {
token: string
post?: Post
onSaved: () => void
onCancel: () => void
}
const ERROR_TEXT: Record<PostError, string> = {
full: '文章数已达上限200 篇)',
'too-large': '正文太长了(上限 64KB',
'bad-input': '标题不能为空,标签也别太长',
failed: '保存失败,稍后再试',
}
export function PostEditor({ token, post, onSaved, onCancel }: PostEditorProps) {
const [title, setTitle] = useState(post?.title ?? '')
const [tag, setTag] = useState(post?.tag ?? '')
const [body, setBody] = useState(post?.body ?? '')
const [preview, setPreview] = useState(false)
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const save = async () => {
setSaving(true)
setError(null)
try {
const input = { title, tag, body }
if (post) await updatePost(token, post.id, input)
else await createPost(token, input)
onSaved()
} catch (e) {
const reason = (e as Error).message as PostError
setError(ERROR_TEXT[reason] ?? ERROR_TEXT.failed)
setSaving(false)
}
}
return (
<section className="w-full px-6 py-14">
<div className="mb-8 flex items-center justify-between">
<h2 className="text-xs font-medium uppercase tracking-[0.3em] text-muted">
{post ? 'Edit post' : 'New post'}
</h2>
<button
onClick={() => setPreview((p) => !p)}
className="rounded-full border border-line px-4 py-1.5 text-xs font-medium uppercase tracking-[0.16em] text-muted transition-colors hover:border-accent hover:text-accent"
>
{preview ? 'Write' : 'Preview'}
</button>
</div>
{preview ? (
<article>
<h1 className="mb-6 font-display text-3xl font-medium text-text">
{title || '(无标题)'}
</h1>
<Markdown>{body || '_正文为空_'}</Markdown>
</article>
) : (
<div className="flex flex-col gap-4">
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="标题"
className="w-full rounded-lg border border-line bg-surface px-4 py-3 font-display text-2xl text-text outline-none focus:border-accent"
/>
<input
value={tag}
onChange={(e) => setTag(e.target.value)}
placeholder="标签(如 Rust、随笔"
className="w-full rounded-lg border border-line bg-surface px-4 py-2 text-sm text-text outline-none focus:border-accent"
/>
<textarea
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder="正文,支持 Markdown…"
rows={16}
className="w-full resize-y rounded-lg border border-line bg-surface px-4 py-3 font-mono text-sm leading-relaxed text-text outline-none focus:border-accent"
/>
</div>
)}
{error && <p className="mt-4 text-sm text-[#ff8585]">{error}</p>}
<div className="mt-8 flex items-center gap-3">
<button
onClick={save}
disabled={saving}
className="rounded-full bg-accent px-6 py-2 text-sm font-medium text-bg transition-opacity hover:opacity-90 disabled:opacity-50"
>
{saving ? '保存中…' : '保存'}
</button>
<button
onClick={onCancel}
disabled={saving}
className="rounded-full border border-line px-6 py-2 text-sm font-medium text-muted transition-colors hover:border-accent hover:text-accent disabled:opacity-50"
>
</button>
</div>
</section>
)
}