519fb92c34
Turn on react-hooks/set-state-in-effect and react-hooks/incompatible-library, then remove effect-driven local state sync patterns across affected pages. Keep behavior stable by deriving values from source state, remounting tab state by role key, and replacing useForm watch with useWatch.
263 lines
10 KiB
TypeScript
263 lines
10 KiB
TypeScript
"use client"
|
||
|
||
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"
|
||
import { ArrowLeft, ImagePlus, X } from "lucide-react"
|
||
import Link from "next/link"
|
||
import { useRouter } from "next/navigation"
|
||
import { useState } from "react"
|
||
import { useForm } from "react-hook-form"
|
||
import { z } from "zod"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Label } from "@/components/ui/label"
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select"
|
||
import { Textarea } from "@/components/ui/textarea"
|
||
import { useRequireAuth } from "@/lib/use-require-auth"
|
||
import { useAuthStore } from "@/store/auth"
|
||
import { useOrderStore } from "@/store/orders"
|
||
import { usePostStore } from "@/store/posts"
|
||
|
||
const postSchema = z.object({
|
||
title: z.string().min(2, "标题至少2个字符").max(50, "标题最多50个字符"),
|
||
content: z.string().min(10, "内容至少10个字符"),
|
||
})
|
||
|
||
const tagOptions = ["英雄联盟", "王者荣耀", "CS2", "原神", "上分", "攻略", "好评", "吐槽", "求组队"]
|
||
|
||
export default function NewPostPage() {
|
||
const router = useRouter()
|
||
const { isAuthenticated, requireAuth } = useRequireAuth()
|
||
const currentRole = useAuthStore((state) => state.currentRole)
|
||
const userId = useAuthStore((state) => state.user?.id)
|
||
const user = useAuthStore((state) => state.user)
|
||
const orders = useOrderStore((state) => state.orders)
|
||
const posts = usePostStore((state) => state.posts)
|
||
const createPost = usePostStore((state) => state.createPost)
|
||
const [postType, setPostType] = useState("normal")
|
||
const [selectedTags, setSelectedTags] = useState<string[]>([])
|
||
const [imageCount, setImageCount] = useState(0)
|
||
const [selectedQuotePostId, setSelectedQuotePostId] = useState<string | undefined>(undefined)
|
||
const [selectedOrderId, setSelectedOrderId] = useState<string | undefined>(undefined)
|
||
const canShowOrder = currentRole === "consumer"
|
||
const effectivePostType = canShowOrder || postType !== "show_order" ? postType : "normal"
|
||
|
||
const {
|
||
register,
|
||
handleSubmit,
|
||
formState: { errors, isSubmitting },
|
||
} = useForm<z.infer<typeof postSchema>>({
|
||
resolver: standardSchemaResolver(postSchema),
|
||
})
|
||
|
||
const toggleTag = (tag: string) => {
|
||
setSelectedTags((prev) =>
|
||
prev.includes(tag) ? prev.filter((t) => t !== tag) : prev.length < 5 ? [...prev, tag] : prev,
|
||
)
|
||
}
|
||
|
||
const availableOrders = orders.filter(
|
||
(order) => order.status === "completed" && order.consumerId === userId,
|
||
)
|
||
|
||
const onSubmit = async (data: z.infer<typeof postSchema>) => {
|
||
if (!isAuthenticated) {
|
||
requireAuth(() => undefined)
|
||
return
|
||
}
|
||
|
||
requireAuth(() => {
|
||
if (!user) return
|
||
|
||
createPost({
|
||
author: user,
|
||
authorRole: currentRole,
|
||
title: data.title,
|
||
content: data.content,
|
||
images: Array.from({ length: imageCount }).map(() => "/posts/p1-1.jpg"),
|
||
tags: selectedTags,
|
||
linkedOrderId: effectivePostType === "show_order" ? selectedOrderId : undefined,
|
||
quotedPostId: effectivePostType === "quote" ? selectedQuotePostId : undefined,
|
||
})
|
||
|
||
router.push("/community")
|
||
})
|
||
}
|
||
|
||
return (
|
||
<div className="container mx-auto py-8 px-4 max-w-2xl">
|
||
<Link
|
||
href="/community"
|
||
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground mb-4"
|
||
>
|
||
<ArrowLeft className="h-4 w-4" />
|
||
返回社区
|
||
</Link>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>发布帖子</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||
<div className="space-y-2">
|
||
<Label>帖子类型</Label>
|
||
<Select value={effectivePostType} onValueChange={setPostType}>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="normal">普通帖</SelectItem>
|
||
{canShowOrder && <SelectItem value="show_order">秀单帖</SelectItem>}
|
||
<SelectItem value="quote">引用帖</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
{effectivePostType === "show_order" && (
|
||
<div className="space-y-2">
|
||
<Label>关联订单</Label>
|
||
<Select value={selectedOrderId} onValueChange={setSelectedOrderId}>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="选择要展示的订单" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{availableOrders.map((order) => (
|
||
<SelectItem key={order.id} value={order.id}>
|
||
{order.service.title} · {order.playerName}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
)}
|
||
|
||
{effectivePostType === "quote" && (
|
||
<div className="space-y-2">
|
||
<Label>引用帖子</Label>
|
||
<Select value={selectedQuotePostId} onValueChange={setSelectedQuotePostId}>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="选择要引用的帖子" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{posts.map((post) => (
|
||
<SelectItem key={post.id} value={post.id}>
|
||
{post.title}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<div className="mt-2 rounded-md border bg-muted/50 p-3 text-sm text-muted-foreground">
|
||
<p className="font-medium text-foreground">预览:</p>
|
||
{selectedQuotePostId ? (
|
||
(() => {
|
||
const post = posts.find((p) => p.id === selectedQuotePostId)
|
||
if (!post) return <p className="mt-1">未找到帖子</p>
|
||
return (
|
||
<div className="mt-2 rounded border bg-background p-3">
|
||
<p className="font-medium text-foreground">{post.title}</p>
|
||
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
||
{post.content}
|
||
</p>
|
||
<div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
|
||
<span>@{post.author.nickname}</span>
|
||
<span>·</span>
|
||
<span>{new Date(post.createdAt).toLocaleDateString("zh-CN")}</span>
|
||
</div>
|
||
</div>
|
||
)
|
||
})()
|
||
) : (
|
||
<p className="mt-1">选择一个帖子以查看预览...</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="title">标题</Label>
|
||
<Input id="title" placeholder="请输入帖子标题" {...register("title")} />
|
||
{errors.title && <p className="text-xs text-destructive">{errors.title.message}</p>}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="content">内容</Label>
|
||
<Textarea id="content" placeholder="写点什么..." rows={6} {...register("content")} />
|
||
{errors.content && (
|
||
<p className="text-xs text-destructive">{errors.content.message}</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>图片(最多9张)</Label>
|
||
<div className="flex gap-2 flex-wrap">
|
||
{Array.from({ length: imageCount }).map((_, i) => (
|
||
<div
|
||
key={`img-${i.toString()}`}
|
||
className="h-20 w-20 rounded-md bg-muted flex items-center justify-center relative"
|
||
>
|
||
<span className="text-xs text-muted-foreground">图片</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => setImageCount((c) => c - 1)}
|
||
className="absolute -top-1 -right-1 h-4 w-4 rounded-full bg-destructive text-destructive-foreground flex items-center justify-center"
|
||
>
|
||
<X className="h-3 w-3" />
|
||
</button>
|
||
</div>
|
||
))}
|
||
{imageCount < 9 && (
|
||
<button
|
||
type="button"
|
||
onClick={() => setImageCount((c) => c + 1)}
|
||
className="h-20 w-20 rounded-md border-2 border-dashed border-muted-foreground/25 flex flex-col items-center justify-center gap-1 text-muted-foreground hover:border-muted-foreground/50 transition-colors"
|
||
>
|
||
<ImagePlus className="h-5 w-5" />
|
||
<span className="text-[10px]">添加</span>
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>标签(最多5个)</Label>
|
||
<div className="flex flex-wrap gap-2">
|
||
{tagOptions.map((tag) => (
|
||
<Badge
|
||
key={tag}
|
||
variant={selectedTags.includes(tag) ? "default" : "outline"}
|
||
className="cursor-pointer"
|
||
onClick={() => toggleTag(tag)}
|
||
>
|
||
{tag}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex gap-2">
|
||
<Button
|
||
type="submit"
|
||
className="flex-1"
|
||
disabled={isSubmitting || (postType === "show_order" && !selectedOrderId)}
|
||
>
|
||
{isSubmitting ? "发布中..." : "发布"}
|
||
</Button>
|
||
<Button type="button" variant="outline" asChild>
|
||
<Link href="/community">取消</Link>
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|