refactor(api): add adapter layer for order/chat/review/dispute writes

This commit is contained in:
zetaloop
2026-02-23 11:04:16 +08:00
parent 1dfcd3927d
commit 8e62b15403
10 changed files with 258 additions and 98 deletions
+22 -24
View File
@@ -9,6 +9,7 @@ import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
import { sendImageMessage, sendTextMessage } from "@/lib/api/chat"
import { cn } from "@/lib/utils"
import { useAuthStore } from "@/store/auth"
import { useChatStore } from "@/store/chat"
@@ -25,8 +26,6 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
() => allMessages.filter((item) => item.sessionId === id),
[allMessages, id],
)
const sendTextMessage = useChatStore((state) => state.sendTextMessage)
const sendImageMessage = useChatStore((state) => state.sendImageMessage)
const [input, setInput] = useState("")
const imageInputRef = useRef<HTMLInputElement>(null)
const { user } = useAuthStore()
@@ -39,8 +38,25 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
)
}
const userId = user?.id ?? session.participants[0].id
const other = session.participants.find((p) => p.id !== userId) ?? session.participants[1]
if (!user?.id) {
return (
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">
</div>
)
}
const userId = user.id
const isParticipant = session.participants.some((participant) => participant.id === userId)
if (!isParticipant) {
return (
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">
</div>
)
}
const other = session.participants.find((p) => p.id !== userId) ?? session.participants[0]
return (
<div className="flex flex-col h-[calc(100vh-3.5rem)]">
@@ -130,16 +146,7 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
onChange={(event) => {
const file = event.target.files?.[0]
if (!file) return
const sender = session.participants.find((participant) => participant.id === userId)
sendImageMessage(
session.id,
{
id: userId,
name: sender?.name ?? user?.nickname ?? "",
avatar: sender?.avatar ?? user?.avatar ?? "",
},
URL.createObjectURL(file),
)
sendImageMessage(session.id, URL.createObjectURL(file))
event.target.value = ""
}}
/>
@@ -150,16 +157,7 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
const text = input.trim()
if (!text) return
const sender = session.participants.find((participant) => participant.id === userId)
sendTextMessage(
session.id,
{
id: userId,
name: sender?.name ?? user?.nickname ?? "",
avatar: sender?.avatar ?? user?.avatar ?? "",
},
text,
)
sendTextMessage(session.id, text)
setInput("")
}}
>
+11 -14
View File
@@ -16,6 +16,7 @@ import {
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { submitDispute, submitDisputeAppeal, submitDisputeResponse } from "@/lib/api/disputes"
import { DISPUTE_TO_RESOLVED_MS } from "@/lib/config/demo-timers"
import { notifyInfo } from "@/lib/toast"
import { Label } from "@/components/ui/label"
@@ -38,11 +39,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
const searchParams = useSearchParams()
const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
const userId = useAuthStore((state) => state.user?.id)
const userName = useAuthStore((state) => state.user?.nickname)
const existingDispute = useDisputeStore((state) => state.getDisputeByOrderId(id))
const submitDispute = useDisputeStore((state) => state.submitDispute)
const submitResponse = useDisputeStore((state) => state.submitResponse)
const submitAppeal = useDisputeStore((state) => state.submitAppeal)
const [reason, setReason] = useState("")
const [files, setFiles] = useState<string[]>([])
@@ -108,11 +105,9 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
}
const handleSubmit = () => {
if (!userId || !userName || !reason.trim()) return
if (!userId || !reason.trim()) return
const result = submitDispute({
orderId: id,
initiatorId: userId,
initiatorName: userName,
reason,
evidence: files,
})
@@ -287,12 +282,11 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
<Button
onClick={() => {
if (!userId) return
const decision = submitResponse(
existingDispute.id,
userId,
responseReason,
responseFiles,
)
const decision = submitDisputeResponse({
disputeId: existingDispute.id,
reason: responseReason,
evidence: responseFiles,
})
if (!decision.ok) {
notifyInfo(decision.message ?? "提交回应失败")
}
@@ -337,7 +331,10 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
variant="outline"
onClick={() => {
if (!userId) return
const decision = submitAppeal(existingDispute.id, userId, appealReason)
const decision = submitDisputeAppeal({
disputeId: existingDispute.id,
reason: appealReason,
})
if (!decision.ok) {
notifyInfo(decision.message ?? "提交申诉失败")
}
-7
View File
@@ -37,7 +37,6 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
const { id } = use(params)
const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
const sessions = useChatStore((state) => state.sessions)
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
const allReviews = useReviewStore((state) => state.reviews)
// Filtering is deferred to useMemo after reading the raw store array.
// Zustand v5 compares selector outputs by reference stability.
@@ -46,12 +45,6 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
const reviews = useMemo(() => allReviews.filter((item) => item.orderId === id), [allReviews, id])
const [nowTs, setNowTs] = useState(0)
useEffect(() => {
if (!order) return
if (order.status === "pending_payment" || order.status === "cancelled") return
ensureOrderSession(order)
}, [order, ensureOrderSession])
useEffect(() => {
if (!order) return
if (order.status !== "pending_accept" && order.status !== "pending_close") return
+6 -3
View File
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { submitReview } from "@/lib/api/reviews"
import { notifyInfo, notifySuccess } from "@/lib/toast"
import { useAuthStore } from "@/store/auth"
import { useOrderStore } from "@/store/orders"
@@ -16,7 +17,6 @@ export default function ReviewPage({ params }: { params: Promise<{ id: string }>
const { id } = use(params)
const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
const userId = useAuthStore((state) => state.user?.id)
const submitReview = useReviewStore((state) => state.submitReview)
const allReviews = useReviewStore((state) => state.reviews)
// The selector returns the raw store array and useMemo derives the subset.
// This keeps useSyncExternalStore snapshots stable across render checks.
@@ -138,10 +138,13 @@ export default function ReviewPage({ params }: { params: Promise<{ id: string }>
className="w-full"
disabled={rating === 0 || !userId}
onClick={() => {
if (!userId) return
if (!userId) {
notifyInfo("请先登录")
return
}
const decision = submitReview({
orderId: id,
fromUserId: userId,
rating,
content,
})