Files
2026-04-25 21:41:01 +08:00

260 lines
9.5 KiB
TypeScript
Raw Permalink 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.
"use client"
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 { createPost, listOrders, uploadFile } from "@/lib/api"
import { toApiError } from "@/lib/errors"
import { notifyInfo, notifySuccess } from "@/lib/toast"
import { useRequireAuth } from "@/lib/use-require-auth"
import { useAuthStore } from "@/store/auth"
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 { type ChangeEvent, useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { z } from "zod"
const postSchema = z.object({
title: z.string().min(2, "标题至少2个字符").max(50, "标题最多50个字符"),
content: z.string().min(10, "内容至少10个字符"),
})
const tagOptions = ["英雄联盟", "王者荣耀", "CS2", "原神", "上分", "攻略", "好评", "吐槽", "求组队"]
type PostType = "normal" | "show_order"
export default function NewPostPage() {
const router = useRouter()
const { isAuthenticated, requireAuth } = useRequireAuth()
const currentRole = useAuthStore((state) => state.currentRole)
const [orders, setOrders] = useState<Awaited<ReturnType<typeof listOrders>>>([])
const [postType, setPostType] = useState<PostType>("normal")
const [selectedTags, setSelectedTags] = useState<string[]>([])
const [imageFiles, setImageFiles] = useState<File[]>([])
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,
)
}
useEffect(() => {
if (!isAuthenticated || !canShowOrder) {
return
}
let cancelled = false
listOrders({ role: "consumer", status: "completed" })
.then((items) => {
if (cancelled) return
setOrders(items)
})
.catch((error) => {
if (cancelled) return
notifyInfo(toApiError(error).msg)
})
return () => {
cancelled = true
}
}, [canShowOrder, isAuthenticated])
const handleSelectImages = (event: ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files ?? [])
if (files.length === 0) return
setImageFiles((prev) => [...prev, ...files].slice(0, 9))
event.target.value = ""
}
const availableOrders = isAuthenticated && canShowOrder ? orders : []
const onSubmit = async (data: z.infer<typeof postSchema>) => {
if (!isAuthenticated) {
requireAuth(() => undefined)
return
}
try {
const images = await Promise.all(imageFiles.map((file) => uploadFile(file, "post")))
await createPost({
title: data.title,
content: data.content,
images,
tags: selectedTags,
linkedOrderId: effectivePostType === "show_order" ? selectedOrderId : undefined,
})
notifySuccess("帖子已发布")
router.push("/community")
} catch (error) {
notifyInfo(toApiError(error).msg)
}
}
return (
<div className="container mx-auto max-w-2xl px-4 py-8 space-y-6">
<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 className="border-border/80 shadow-sm">
<CardHeader className="border-b border-border/60">
<CardTitle className="tracking-tight leading-tight"></CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<div className="space-y-2">
<Label></Label>
<Select
value={effectivePostType}
onValueChange={(value) => setPostType(value as PostType)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="normal"></SelectItem>
{canShowOrder && <SelectItem value="show_order"></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}
</SelectItem>
))}
</SelectContent>
</Select>
</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">
{imageFiles.map((file) => (
<div
key={`${file.name}-${file.lastModified}-${file.size}`}
className="relative flex h-20 w-20 items-center justify-center rounded-lg border border-border/60 bg-muted/30"
>
<span className="max-w-16 truncate text-xs text-muted-foreground">
{file.name}
</span>
<button
type="button"
aria-label={`移除图片 ${file.name}`}
onClick={() =>
setImageFiles((files) => files.filter((item) => item !== file))
}
className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-destructive text-destructive-foreground shadow-sm"
>
<X className="h-3 w-3" />
</button>
</div>
))}
{imageFiles.length < 9 && (
<Label
htmlFor="post-images"
className="flex h-20 w-20 cursor-pointer flex-col items-center justify-center gap-1 rounded-lg border-2 border-dashed border-border/60 bg-muted/20 text-muted-foreground transition-colors hover:border-primary/60 hover:bg-muted/30 hover:text-foreground"
>
<ImagePlus className="h-5 w-5" />
<span className="text-[10px]"></span>
<Input
id="post-images"
type="file"
accept="image/*"
multiple
className="sr-only"
onChange={handleSelectImages}
/>
</Label>
)}
</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 border-border/60"
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" className="border-border/60" asChild>
<Link href="/community"></Link>
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
)
}