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 { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { sendImageMessage, sendTextMessage } from "@/lib/api/chat"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { useAuthStore } from "@/store/auth" import { useAuthStore } from "@/store/auth"
import { useChatStore } from "@/store/chat" 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.filter((item) => item.sessionId === id),
[allMessages, id], [allMessages, id],
) )
const sendTextMessage = useChatStore((state) => state.sendTextMessage)
const sendImageMessage = useChatStore((state) => state.sendImageMessage)
const [input, setInput] = useState("") const [input, setInput] = useState("")
const imageInputRef = useRef<HTMLInputElement>(null) const imageInputRef = useRef<HTMLInputElement>(null)
const { user } = useAuthStore() const { user } = useAuthStore()
@@ -39,8 +38,25 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
) )
} }
const userId = user?.id ?? session.participants[0].id if (!user?.id) {
const other = session.participants.find((p) => p.id !== userId) ?? session.participants[1] 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 ( return (
<div className="flex flex-col h-[calc(100vh-3.5rem)]"> <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) => { onChange={(event) => {
const file = event.target.files?.[0] const file = event.target.files?.[0]
if (!file) return if (!file) return
const sender = session.participants.find((participant) => participant.id === userId) sendImageMessage(session.id, URL.createObjectURL(file))
sendImageMessage(
session.id,
{
id: userId,
name: sender?.name ?? user?.nickname ?? "",
avatar: sender?.avatar ?? user?.avatar ?? "",
},
URL.createObjectURL(file),
)
event.target.value = "" event.target.value = ""
}} }}
/> />
@@ -150,16 +157,7 @@ export default function ChatDetailPage({ params }: { params: Promise<{ id: strin
const text = input.trim() const text = input.trim()
if (!text) return if (!text) return
const sender = session.participants.find((participant) => participant.id === userId) sendTextMessage(session.id, text)
sendTextMessage(
session.id,
{
id: userId,
name: sender?.name ?? user?.nickname ?? "",
avatar: sender?.avatar ?? user?.avatar ?? "",
},
text,
)
setInput("") setInput("")
}} }}
> >
+11 -14
View File
@@ -16,6 +16,7 @@ import {
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" 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 { DISPUTE_TO_RESOLVED_MS } from "@/lib/config/demo-timers"
import { notifyInfo } from "@/lib/toast" import { notifyInfo } from "@/lib/toast"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
@@ -38,11 +39,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
const searchParams = useSearchParams() const searchParams = useSearchParams()
const order = useOrderStore((state) => state.orders.find((item) => item.id === id)) const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
const userId = useAuthStore((state) => state.user?.id) const userId = useAuthStore((state) => state.user?.id)
const userName = useAuthStore((state) => state.user?.nickname)
const existingDispute = useDisputeStore((state) => state.getDisputeByOrderId(id)) 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 [reason, setReason] = useState("")
const [files, setFiles] = useState<string[]>([]) const [files, setFiles] = useState<string[]>([])
@@ -108,11 +105,9 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
} }
const handleSubmit = () => { const handleSubmit = () => {
if (!userId || !userName || !reason.trim()) return if (!userId || !reason.trim()) return
const result = submitDispute({ const result = submitDispute({
orderId: id, orderId: id,
initiatorId: userId,
initiatorName: userName,
reason, reason,
evidence: files, evidence: files,
}) })
@@ -287,12 +282,11 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
<Button <Button
onClick={() => { onClick={() => {
if (!userId) return if (!userId) return
const decision = submitResponse( const decision = submitDisputeResponse({
existingDispute.id, disputeId: existingDispute.id,
userId, reason: responseReason,
responseReason, evidence: responseFiles,
responseFiles, })
)
if (!decision.ok) { if (!decision.ok) {
notifyInfo(decision.message ?? "提交回应失败") notifyInfo(decision.message ?? "提交回应失败")
} }
@@ -337,7 +331,10 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
variant="outline" variant="outline"
onClick={() => { onClick={() => {
if (!userId) return if (!userId) return
const decision = submitAppeal(existingDispute.id, userId, appealReason) const decision = submitDisputeAppeal({
disputeId: existingDispute.id,
reason: appealReason,
})
if (!decision.ok) { if (!decision.ok) {
notifyInfo(decision.message ?? "提交申诉失败") notifyInfo(decision.message ?? "提交申诉失败")
} }
-7
View File
@@ -37,7 +37,6 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
const { id } = use(params) const { id } = use(params)
const order = useOrderStore((state) => state.orders.find((item) => item.id === id)) const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
const sessions = useChatStore((state) => state.sessions) const sessions = useChatStore((state) => state.sessions)
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
const allReviews = useReviewStore((state) => state.reviews) const allReviews = useReviewStore((state) => state.reviews)
// Filtering is deferred to useMemo after reading the raw store array. // Filtering is deferred to useMemo after reading the raw store array.
// Zustand v5 compares selector outputs by reference stability. // 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 reviews = useMemo(() => allReviews.filter((item) => item.orderId === id), [allReviews, id])
const [nowTs, setNowTs] = useState(0) const [nowTs, setNowTs] = useState(0)
useEffect(() => {
if (!order) return
if (order.status === "pending_payment" || order.status === "cancelled") return
ensureOrderSession(order)
}, [order, ensureOrderSession])
useEffect(() => { useEffect(() => {
if (!order) return if (!order) return
if (order.status !== "pending_accept" && order.status !== "pending_close") 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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { submitReview } from "@/lib/api/reviews"
import { notifyInfo, notifySuccess } from "@/lib/toast" import { notifyInfo, notifySuccess } from "@/lib/toast"
import { useAuthStore } from "@/store/auth" import { useAuthStore } from "@/store/auth"
import { useOrderStore } from "@/store/orders" import { useOrderStore } from "@/store/orders"
@@ -16,7 +17,6 @@ export default function ReviewPage({ params }: { params: Promise<{ id: string }>
const { id } = use(params) const { id } = use(params)
const order = useOrderStore((state) => state.orders.find((item) => item.id === id)) const order = useOrderStore((state) => state.orders.find((item) => item.id === id))
const userId = useAuthStore((state) => state.user?.id) const userId = useAuthStore((state) => state.user?.id)
const submitReview = useReviewStore((state) => state.submitReview)
const allReviews = useReviewStore((state) => state.reviews) const allReviews = useReviewStore((state) => state.reviews)
// The selector returns the raw store array and useMemo derives the subset. // The selector returns the raw store array and useMemo derives the subset.
// This keeps useSyncExternalStore snapshots stable across render checks. // This keeps useSyncExternalStore snapshots stable across render checks.
@@ -138,10 +138,13 @@ export default function ReviewPage({ params }: { params: Promise<{ id: string }>
className="w-full" className="w-full"
disabled={rating === 0 || !userId} disabled={rating === 0 || !userId}
onClick={() => { onClick={() => {
if (!userId) return if (!userId) {
notifyInfo("请先登录")
return
}
const decision = submitReview({ const decision = submitReview({
orderId: id, orderId: id,
fromUserId: userId,
rating, rating,
content, content,
}) })
+22 -38
View File
@@ -12,6 +12,14 @@ import {
import Link from "next/link" import Link from "next/link"
import { useCallback, useEffect } from "react" import { useCallback, useEffect } from "react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import {
acceptOrder,
acceptOrderAsActor,
cancelPreAccept,
confirmClose,
payOrder,
requestClose,
} from "@/lib/api/orders"
import type { Actor } from "@/lib/policy/actor" import type { Actor } from "@/lib/policy/actor"
import { notifyInfo, notifySuccess } from "@/lib/toast" import { notifyInfo, notifySuccess } from "@/lib/toast"
import type { OrderStatus } from "@/lib/types" import type { OrderStatus } from "@/lib/types"
@@ -37,21 +45,9 @@ export default function OrderActions({
chatSessionId, chatSessionId,
serviceId, serviceId,
}: OrderActionsProps) { }: OrderActionsProps) {
const currentRole = useAuthStore((state) => state.currentRole)
const currentUserId = useAuthStore((state) => state.user?.id) const currentUserId = useAuthStore((state) => state.user?.id)
const payOrder = useOrderStore((state) => state.payOrder)
const acceptOrder = useOrderStore((state) => state.acceptOrder)
const requestClose = useOrderStore((state) => state.requestClose)
const confirmClose = useOrderStore((state) => state.confirmClose)
const cancelPreAccept = useOrderStore((state) => state.cancelPreAccept)
const order = useOrderStore((state) => state.orders.find((item) => item.id === orderId)) const order = useOrderStore((state) => state.orders.find((item) => item.id === orderId))
const sessions = useChatStore((state) => state.sessions) const sessions = useChatStore((state) => state.sessions)
const ensureOrderSession = useChatStore((state) => state.ensureOrderSession)
const actorShopId = useShopStore((state) => {
if (!currentUserId || currentRole !== "owner") return undefined
const owned = state.shops.find((shop) => shop.owner.id === currentUserId)
return owned?.id
})
const dispatchMode = useShopStore((state) => { const dispatchMode = useShopStore((state) => {
if (!order?.shopId) return "manual" if (!order?.shopId) return "manual"
const shop = state.shops.find((item) => item.id === order.shopId) const shop = state.shops.find((item) => item.id === order.shopId)
@@ -62,13 +58,6 @@ export default function OrderActions({
sessions.find((session) => session.type === "order" && session.orderId === orderId)?.id sessions.find((session) => session.type === "order" && session.orderId === orderId)?.id
const status = order?.status ?? initialStatus const status = order?.status ?? initialStatus
const actor: Actor | undefined = currentUserId
? {
userId: currentUserId,
role: currentRole,
shopId: actorShopId,
}
: undefined
const handleDecision = useCallback( const handleDecision = useCallback(
(okMessage: string, result: { decision: { ok: boolean; message?: string } }) => { (okMessage: string, result: { decision: { ok: boolean; message?: string } }) => {
@@ -82,11 +71,6 @@ export default function OrderActions({
[], [],
) )
useEffect(() => {
if (chatSessionId || !order || resolvedChatSessionId) return
ensureOrderSession(order)
}, [chatSessionId, order, ensureOrderSession, resolvedChatSessionId])
useEffect(() => { useEffect(() => {
if (!order) return if (!order) return
if (order.status !== "pending_accept") return if (order.status !== "pending_accept") return
@@ -99,12 +83,12 @@ export default function OrderActions({
role: "player", role: "player",
shopId: order.shopId, shopId: order.shopId,
} }
const result = acceptOrder(orderId, systemActor) const result = acceptOrderAsActor(orderId, systemActor)
handleDecision("系统已自动派单", result) handleDecision("系统已自动派单", result)
}, 3000) }, 3000)
return () => clearTimeout(timer) return () => clearTimeout(timer)
}, [acceptOrder, dispatchMode, handleDecision, order, orderId]) }, [dispatchMode, handleDecision, order, orderId])
return ( return (
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
@@ -113,12 +97,12 @@ export default function OrderActions({
<Button <Button
variant="outline" variant="outline"
onClick={() => { onClick={() => {
if (!actor) { if (!currentUserId) {
notifyInfo("请先登录") notifyInfo("请先登录")
return return
} }
const result = cancelPreAccept(orderId, actor) const result = cancelPreAccept(orderId)
handleDecision("订单已取消", result) handleDecision("订单已取消", result)
}} }}
> >
@@ -127,12 +111,12 @@ export default function OrderActions({
</Button> </Button>
<Button <Button
onClick={() => { onClick={() => {
if (!actor) { if (!currentUserId) {
notifyInfo("请先登录") notifyInfo("请先登录")
return return
} }
const result = payOrder(orderId, actor) const result = payOrder(orderId)
handleDecision("订单支付成功", result) handleDecision("订单支付成功", result)
}} }}
> >
@@ -147,12 +131,12 @@ export default function OrderActions({
<Button <Button
variant="outline" variant="outline"
onClick={() => { onClick={() => {
if (!actor) { if (!currentUserId) {
notifyInfo("请先登录") notifyInfo("请先登录")
return return
} }
const result = cancelPreAccept(orderId, actor) const result = cancelPreAccept(orderId)
handleDecision("订单已取消", result) handleDecision("订单已取消", result)
}} }}
> >
@@ -167,12 +151,12 @@ export default function OrderActions({
) : ( ) : (
<Button <Button
onClick={() => { onClick={() => {
if (!actor) { if (!currentUserId) {
notifyInfo("请先登录") notifyInfo("请先登录")
return return
} }
const result = acceptOrder(orderId, actor) const result = acceptOrder(orderId)
handleDecision("已接单", result) handleDecision("已接单", result)
}} }}
> >
@@ -196,12 +180,12 @@ export default function OrderActions({
<> <>
<Button <Button
onClick={() => { onClick={() => {
if (!actor) { if (!currentUserId) {
notifyInfo("请先登录") notifyInfo("请先登录")
return return
} }
const result = requestClose(orderId, actor) const result = requestClose(orderId)
handleDecision("已发起结单", result) handleDecision("已发起结单", result)
}} }}
> >
@@ -220,12 +204,12 @@ export default function OrderActions({
<> <>
<Button <Button
onClick={() => { onClick={() => {
if (!actor) { if (!currentUserId) {
notifyInfo("请先登录") notifyInfo("请先登录")
return return
} }
const result = confirmClose(orderId, actor) const result = confirmClose(orderId)
handleDecision("已确认结单", result) handleDecision("已确认结单", result)
}} }}
> >
+42
View File
@@ -1,4 +1,6 @@
import { allow, deny } from "@/lib/policy/assert"
import { useChatStore } from "@/store/chat" import { useChatStore } from "@/store/chat"
import { useAuthStore } from "@/store/auth"
export function listChatSessions() { export function listChatSessions() {
return useChatStore.getState().sessions return useChatStore.getState().sessions
@@ -11,3 +13,43 @@ export function getChatSessionById(sessionId: string) {
export function listChatMessages(sessionId: string) { export function listChatMessages(sessionId: string) {
return useChatStore.getState().messages.filter((message) => message.sessionId === sessionId) return useChatStore.getState().messages.filter((message) => message.sessionId === sessionId)
} }
export function sendTextMessage(sessionId: string, content: string) {
const userId = useAuthStore.getState().user?.id
if (!userId) return deny("AUTH_REQUIRED", "请先登录")
const chatState = useChatStore.getState()
const session = chatState.sessions.find((item) => item.id === sessionId)
if (!session) return deny("NOT_FOUND", "会话不存在")
if (session.readonly) return deny("INVALID_STATUS", "当前会话只读")
if (!session.participants.some((participant) => participant.id === userId)) {
return deny("NOT_PARTICIPANT", "仅会话参与方可发送消息")
}
if (!content.trim()) {
return deny("VALIDATION_FAILED", "消息不能为空")
}
chatState.sendTextMessage(sessionId, userId, content)
return allow()
}
export function sendImageMessage(sessionId: string, imageUrl: string) {
const userId = useAuthStore.getState().user?.id
if (!userId) return deny("AUTH_REQUIRED", "请先登录")
const chatState = useChatStore.getState()
const session = chatState.sessions.find((item) => item.id === sessionId)
if (!session) return deny("NOT_FOUND", "会话不存在")
if (session.readonly) return deny("INVALID_STATUS", "当前会话只读")
if (!session.participants.some((participant) => participant.id === userId)) {
return deny("NOT_PARTICIPANT", "仅会话参与方可发送消息")
}
if (!imageUrl.trim()) {
return deny("VALIDATION_FAILED", "图片地址无效")
}
chatState.sendImageMessage(sessionId, userId, imageUrl)
return allow()
}
+41
View File
@@ -1,3 +1,5 @@
import { deny } from "@/lib/policy/assert"
import { useAuthStore } from "@/store/auth"
import { useDisputeStore } from "@/store/disputes" import { useDisputeStore } from "@/store/disputes"
export function listDisputes() { export function listDisputes() {
@@ -7,3 +9,42 @@ export function listDisputes() {
export function getDisputeByOrderId(orderId: string) { export function getDisputeByOrderId(orderId: string) {
return useDisputeStore.getState().disputes.find((dispute) => dispute.orderId === orderId) return useDisputeStore.getState().disputes.find((dispute) => dispute.orderId === orderId)
} }
export function submitDispute(input: { orderId: string; reason: string; evidence: string[] }) {
const user = useAuthStore.getState().user
if (!user?.id || !user.nickname) {
return { decision: deny("AUTH_REQUIRED", "请先登录") }
}
return useDisputeStore.getState().submitDispute({
orderId: input.orderId,
initiatorId: user.id,
initiatorName: user.nickname,
reason: input.reason,
evidence: input.evidence,
})
}
export function submitDisputeResponse(input: {
disputeId: string
reason: string
evidence: string[]
}) {
const userId = useAuthStore.getState().user?.id
if (!userId) {
return deny("AUTH_REQUIRED", "请先登录")
}
return useDisputeStore
.getState()
.submitResponse(input.disputeId, userId, input.reason, input.evidence)
}
export function submitDisputeAppeal(input: { disputeId: string; reason: string }) {
const userId = useAuthStore.getState().user?.id
if (!userId) {
return deny("AUTH_REQUIRED", "请先登录")
}
return useDisputeStore.getState().submitAppeal(input.disputeId, userId, input.reason)
}
+86
View File
@@ -1,4 +1,11 @@
import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop"
import { allow, deny } from "@/lib/policy/assert"
import type { Actor } from "@/lib/policy/actor"
import type { PolicyDecision } from "@/lib/policy/decision"
import type { PlayerService } from "@/lib/types"
import { useAuthStore } from "@/store/auth"
import { useOrderStore } from "@/store/orders" import { useOrderStore } from "@/store/orders"
import { useShopStore } from "@/store/shops"
export function listOrders() { export function listOrders() {
return useOrderStore.getState().orders return useOrderStore.getState().orders
@@ -11,3 +18,82 @@ export function getOrderById(orderId: string) {
export function listOrdersByConsumer(consumerId: string) { export function listOrdersByConsumer(consumerId: string) {
return useOrderStore.getState().orders.filter((order) => order.consumerId === consumerId) return useOrderStore.getState().orders.filter((order) => order.consumerId === consumerId)
} }
interface CreatePaidOrderInput {
consumerId: string
consumerName: string
playerId: string
playerName: string
shopId?: string
shopName?: string
service: PlayerService
totalPrice: number
note?: string
}
function resolveActorContext(): { actor?: Actor; decision: PolicyDecision } {
const auth = useAuthStore.getState()
if (!auth.user?.id) {
return { decision: deny("AUTH_REQUIRED", "请先登录") }
}
const shopId =
auth.currentRole === "owner"
? resolveOwnerShop(auth.user.id, useShopStore.getState().shops)?.id
: undefined
return {
actor: {
userId: auth.user.id,
role: auth.currentRole,
shopId,
},
decision: allow(),
}
}
export function createPaidOrder(input: CreatePaidOrderInput) {
const { actor, decision } = resolveActorContext()
if (!actor) return { decision }
return useOrderStore.getState().createPaidOrder(input, actor)
}
export function payOrder(orderId: string) {
const { actor, decision } = resolveActorContext()
if (!actor) return { decision }
return useOrderStore.getState().payOrder(orderId, actor)
}
export function acceptOrder(orderId: string) {
const { actor, decision } = resolveActorContext()
if (!actor) return { decision }
return useOrderStore.getState().acceptOrder(orderId, actor)
}
export function acceptOrderAsActor(orderId: string, actor: Actor) {
return useOrderStore.getState().acceptOrder(orderId, actor)
}
export function requestClose(orderId: string) {
const { actor, decision } = resolveActorContext()
if (!actor) return { decision }
return useOrderStore.getState().requestClose(orderId, actor)
}
export function confirmClose(orderId: string) {
const { actor, decision } = resolveActorContext()
if (!actor) return { decision }
return useOrderStore.getState().confirmClose(orderId, actor)
}
export function cancelPreAccept(orderId: string) {
const { actor, decision } = resolveActorContext()
if (!actor) return { decision }
return useOrderStore.getState().cancelPreAccept(orderId, actor)
}
export function markDisputed(orderId: string) {
const { actor, decision } = resolveActorContext()
if (!actor) return { decision }
return useOrderStore.getState().markDisputed(orderId, actor)
}
+16
View File
@@ -1,3 +1,5 @@
import { deny } from "@/lib/policy/assert"
import { useAuthStore } from "@/store/auth"
import { useReviewStore } from "@/store/reviews" import { useReviewStore } from "@/store/reviews"
export function listReviews() { export function listReviews() {
@@ -11,3 +13,17 @@ export function listReviewsByOrder(orderId: string) {
export function listReviewsByTargetUser(userId: string) { export function listReviewsByTargetUser(userId: string) {
return useReviewStore.getState().reviews.filter((review) => review.toUserId === userId) return useReviewStore.getState().reviews.filter((review) => review.toUserId === userId)
} }
export function submitReview(input: { orderId: string; rating: number; content?: string }) {
const userId = useAuthStore.getState().user?.id
if (!userId) {
return deny("AUTH_REQUIRED", "请先登录")
}
return useReviewStore.getState().submitReview({
orderId: input.orderId,
fromUserId: userId,
rating: input.rating,
content: input.content,
})
}
+12 -12
View File
@@ -3,18 +3,12 @@ import { generateId } from "@/lib/id"
import { mockChatMessages, mockChatSessions, mockUsers } from "@/lib/mock" import { mockChatMessages, mockChatSessions, mockUsers } from "@/lib/mock"
import type { ChatMessage, ChatSession, Order } from "@/lib/types" import type { ChatMessage, ChatSession, Order } from "@/lib/types"
interface Sender {
id: string
name: string
avatar: string
}
interface ChatState { interface ChatState {
sessions: ChatSession[] sessions: ChatSession[]
messages: ChatMessage[] messages: ChatMessage[]
ensureOrderSession: (order: Order) => ChatSession ensureOrderSession: (order: Order) => ChatSession
sendTextMessage: (sessionId: string, sender: Sender, content: string) => void sendTextMessage: (sessionId: string, actorId: string, content: string) => void
sendImageMessage: (sessionId: string, sender: Sender, imageUrl: string) => void sendImageMessage: (sessionId: string, actorId: string, imageUrl: string) => void
} }
function resolveAvatar(userId: string) { function resolveAvatar(userId: string) {
@@ -72,17 +66,20 @@ export const useChatStore = create<ChatState>((set, get) => ({
return session return session
}, },
sendTextMessage: (sessionId, sender, content) => { sendTextMessage: (sessionId, actorId, content) => {
const text = content.trim() const text = content.trim()
if (!text) return if (!text) return
const session = get().sessions.find((item) => item.id === sessionId) const session = get().sessions.find((item) => item.id === sessionId)
if (!session || session.readonly) return if (!session || session.readonly) return
const sender = session.participants.find((participant) => participant.id === actorId)
if (!sender) return
const now = new Date().toISOString() const now = new Date().toISOString()
const message: ChatMessage = { const message: ChatMessage = {
id: generateId("msg"), id: generateId("msg"),
sessionId, sessionId,
senderId: sender.id, senderId: actorId,
senderName: sender.name, senderName: sender.name,
senderAvatar: sender.avatar, senderAvatar: sender.avatar,
type: "text", type: "text",
@@ -103,17 +100,20 @@ export const useChatStore = create<ChatState>((set, get) => ({
), ),
})) }))
}, },
sendImageMessage: (sessionId, sender, imageUrl) => { sendImageMessage: (sessionId, actorId, imageUrl) => {
const content = imageUrl.trim() const content = imageUrl.trim()
if (!content) return if (!content) return
const session = get().sessions.find((item) => item.id === sessionId) const session = get().sessions.find((item) => item.id === sessionId)
if (!session || session.readonly) return if (!session || session.readonly) return
const sender = session.participants.find((participant) => participant.id === actorId)
if (!sender) return
const now = new Date().toISOString() const now = new Date().toISOString()
const message: ChatMessage = { const message: ChatMessage = {
id: generateId("msg"), id: generateId("msg"),
sessionId, sessionId,
senderId: sender.id, senderId: actorId,
senderName: sender.name, senderName: sender.name,
senderAvatar: sender.avatar, senderAvatar: sender.avatar,
type: "image", type: "image",