Files
sundynix-site/admin/src/pages/post-edit.tsx
T
Blizzard 5abc705eae feat: 官网 + 管理端 + Gin/GORM 后端首个完整版本
- web/: 产品官网(Hero 事件流终端、功能矩阵、架构、快速开始、下载、博客 + Markdown 详情页),青瓷绿双主题
- admin/: 内容管理(JWT 登录、文章分页/搜索/CRUD、草稿与发布),embed 挂 /admin
- server/: Gin + GORM,MySQL(DSN 走 .env,SQLite 兜底);规范落地:sundynix_ 表前缀、字符串雪花主键、snake_case 列名、统一响应信封、公共分页参数
- 双 SPA embed 单二进制部署,make build 一键出包

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:03:31 +08:00

193 lines
6.0 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 { useEffect, useState, type FormEvent } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { ArrowLeft } from 'lucide-react'
import {
createPost,
fetchPost,
updatePost,
type PostForm,
} from '@/api/posts'
const EMPTY: PostForm = {
slug: '',
title: '',
category: '',
summary: '',
content: '',
published: false,
}
export default function PostEditPage() {
const { id } = useParams()
const navigate = useNavigate()
const isEdit = Boolean(id)
const [form, setForm] = useState<PostForm>(EMPTY)
const [loading, setLoading] = useState(isEdit)
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
if (!id) return
let cancelled = false
fetchPost(id)
.then((post) => {
if (cancelled) return
setForm({
slug: post.slug,
title: post.title,
category: post.category,
summary: post.summary,
content: post.content ?? '',
published: post.published_at !== null,
})
setLoading(false)
})
.catch((err: Error) => {
if (cancelled) return
setError(err.message)
setLoading(false)
})
return () => {
cancelled = true
}
}, [id])
const set = <K extends keyof PostForm>(key: K, value: PostForm[K]) =>
setForm((f) => ({ ...f, [key]: value }))
const onSubmit = async (e: FormEvent) => {
e.preventDefault()
setSaving(true)
setError('')
try {
if (isEdit && id) {
await updatePost(id, form)
} else {
await createPost(form)
}
navigate('/')
} catch (err) {
setError(err instanceof Error ? err.message : '保存失败')
} finally {
setSaving(false)
}
}
const inputCls =
'w-full rounded-lg border border-hairline-strong bg-ground px-3.5 py-2 text-[14px] outline-none transition-colors focus:border-accent'
return (
<div>
<Link
to="/"
className="mb-6 inline-flex items-center gap-1.5 font-mono text-[12.5px] tracking-[0.08em] text-ink-3 transition-colors hover:text-accent-ink"
>
<ArrowLeft className="size-3.5" />
</Link>
<h1 className="mb-6 text-[22px] font-[650] tracking-[-0.01em]">
{isEdit ? '编辑文章' : '新建文章'}
</h1>
{loading ? (
<div className="space-y-4">
<div className="h-10 animate-pulse rounded-lg bg-code" />
<div className="h-64 animate-pulse rounded-lg bg-code" />
</div>
) : (
<form onSubmit={onSubmit} className="space-y-5">
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
<label className="block">
<span className="mb-1.5 block text-[13px] text-ink-2"> *</span>
<input
type="text"
value={form.title}
onChange={(e) => set('title', e.target.value)}
required
className={inputCls}
/>
</label>
<label className="block">
<span className="mb-1.5 block text-[13px] text-ink-2">
Slug *URL my-first-post
</span>
<input
type="text"
value={form.slug}
onChange={(e) => set('slug', e.target.value)}
required
pattern="[a-z0-9]+(-[a-z0-9]+)*"
title="小写字母、数字,用 - 连接"
className={`${inputCls} font-mono`}
/>
</label>
<label className="block">
<span className="mb-1.5 block text-[13px] text-ink-2">
RELEASE / ENGINEERING
</span>
<input
type="text"
value={form.category}
onChange={(e) => set('category', e.target.value.toUpperCase())}
className={`${inputCls} font-mono`}
/>
</label>
<label className="block">
<span className="mb-1.5 block text-[13px] text-ink-2"></span>
<input
type="text"
value={form.summary}
onChange={(e) => set('summary', e.target.value)}
className={inputCls}
/>
</label>
</div>
<label className="block">
<span className="mb-1.5 block text-[13px] text-ink-2">
Markdown
</span>
<textarea
value={form.content}
onChange={(e) => set('content', e.target.value)}
rows={18}
spellCheck={false}
className={`${inputCls} resize-y font-mono text-[13.5px] leading-relaxed`}
/>
</label>
<div className="flex items-center justify-between border-t border-hairline pt-5">
<label className="flex cursor-pointer items-center gap-2.5 text-[14px]">
<input
type="checkbox"
checked={form.published}
onChange={(e) => set('published', e.target.checked)}
className="size-4 accent-(--accent)"
/>
<span className="text-[12.5px] text-ink-3">
稿
</span>
</label>
<div className="flex items-center gap-3">
{error && (
<span className="text-[13px] text-red-600 dark:text-red-400">
{error}
</span>
)}
<button
type="submit"
disabled={saving}
className="rounded-lg bg-accent px-6 py-2 text-[14px] font-medium text-ground transition-opacity hover:opacity-90 disabled:opacity-60"
>
{saving ? '保存中…' : '保存'}
</button>
</div>
</div>
</form>
)}
</div>
)
}