feat(dispute): support bilateral evidence, timeline, and one-time appeal

This commit is contained in:
zetaloop
2026-02-22 08:16:51 +08:00
parent a7d56240ff
commit 5542015abe
2 changed files with 434 additions and 34 deletions
+222
View File
@@ -0,0 +1,222 @@
import { create } from "zustand"
import { generateId } from "@/lib/id"
import { mockDisputes } from "@/lib/mock"
import type { Dispute } from "@/lib/types"
import { useOrderStore } from "@/store/orders"
type DisputeTimelineType = "created" | "response" | "reviewing" | "resolved" | "appealed"
interface DisputeTimelineItem {
id: string
type: DisputeTimelineType
content: string
createdAt: string
}
export interface DisputeRecord extends Dispute {
respondentReason?: string
respondentEvidence: string[]
appealReason?: string
appealedAt?: string
timeline: DisputeTimelineItem[]
}
interface SubmitDisputeInput {
orderId: string
initiatorId: string
initiatorName: string
reason: string
evidence: string[]
}
interface DisputeState {
disputes: DisputeRecord[]
getDisputeByOrderId: (orderId: string) => DisputeRecord | undefined
submitDispute: (input: SubmitDisputeInput) => DisputeRecord
submitResponse: (disputeId: string, reason: string, evidence: string[]) => void
submitAppeal: (disputeId: string, reason: string) => void
}
const progressTimers = new Map<string, ReturnType<typeof setTimeout>[]>()
function clearProgressTimers(disputeId: string) {
const timers = progressTimers.get(disputeId)
if (!timers) return
timers.forEach((timer) => {
clearTimeout(timer)
})
progressTimers.delete(disputeId)
}
function asRecord(dispute: Dispute): DisputeRecord {
const timeline: DisputeTimelineItem[] = [
{
id: generateId("timeline"),
type: "created",
content: `${dispute.initiatorName} 提交争议`,
createdAt: dispute.createdAt,
},
]
if (dispute.status === "reviewing") {
timeline.push({
id: generateId("timeline"),
type: "reviewing",
content: "平台已受理并进入审核",
createdAt: dispute.createdAt,
})
}
if (dispute.status === "resolved") {
timeline.push({
id: generateId("timeline"),
type: "resolved",
content: "平台已给出仲裁结果",
createdAt: dispute.createdAt,
})
}
return {
...dispute,
respondentEvidence: [],
timeline,
}
}
export const useDisputeStore = create<DisputeState>((set, get) => {
const scheduleProgress = (disputeId: string) => {
clearProgressTimers(disputeId)
const toReviewing = setTimeout(() => {
set((state) => ({
disputes: state.disputes.map((dispute) => {
if (dispute.id !== disputeId) return dispute
if (dispute.status !== "open" && dispute.status !== "appealed") return dispute
return {
...dispute,
status: "reviewing",
timeline: [
...dispute.timeline,
{
id: generateId("timeline"),
type: "reviewing",
content: "平台已受理并进入审核",
createdAt: new Date().toISOString(),
},
],
}
}),
}))
}, 5000)
const toResolved = setTimeout(() => {
set((state) => ({
disputes: state.disputes.map((dispute) => {
if (dispute.id !== disputeId) return dispute
if (dispute.status !== "reviewing" && dispute.status !== "appealed") return dispute
return {
...dispute,
status: "resolved",
result: dispute.result ?? "partial_refund",
timeline: [
...dispute.timeline,
{
id: generateId("timeline"),
type: "resolved",
content: "平台已给出仲裁结果",
createdAt: new Date().toISOString(),
},
],
}
}),
}))
clearProgressTimers(disputeId)
}, 10000)
progressTimers.set(disputeId, [toReviewing, toResolved])
}
return {
disputes: mockDisputes.map(asRecord),
getDisputeByOrderId: (orderId) => get().disputes.find((dispute) => dispute.orderId === orderId),
submitDispute: (input) => {
const createdAt = new Date().toISOString()
const dispute: DisputeRecord = {
id: generateId("disp"),
orderId: input.orderId,
initiatorId: input.initiatorId,
initiatorName: input.initiatorName,
reason: input.reason.trim(),
evidence: input.evidence,
status: "open",
createdAt,
respondentEvidence: [],
timeline: [
{
id: generateId("timeline"),
type: "created",
content: `${input.initiatorName} 提交争议`,
createdAt,
},
],
}
set((state) => ({ disputes: [dispute, ...state.disputes] }))
useOrderStore.getState().updateOrderStatus(input.orderId, "disputed")
scheduleProgress(dispute.id)
return dispute
},
submitResponse: (disputeId, reason, evidence) => {
if (!reason.trim()) return
set((state) => ({
disputes: state.disputes.map((dispute) => {
if (dispute.id !== disputeId || dispute.respondentReason) return dispute
return {
...dispute,
respondentReason: reason.trim(),
respondentEvidence: evidence,
timeline: [
...dispute.timeline,
{
id: generateId("timeline"),
type: "response",
content: "对方已提交回应材料",
createdAt: new Date().toISOString(),
},
],
}
}),
}))
},
submitAppeal: (disputeId, reason) => {
if (!reason.trim()) return
set((state) => ({
disputes: state.disputes.map((dispute) => {
if (dispute.id !== disputeId || dispute.appealedAt || dispute.status !== "resolved") {
return dispute
}
return {
...dispute,
status: "appealed",
appealReason: reason.trim(),
appealedAt: new Date().toISOString(),
timeline: [
...dispute.timeline,
{
id: generateId("timeline"),
type: "appealed",
content: "已提交申诉,平台将复核",
createdAt: new Date().toISOString(),
},
],
}
}),
}))
scheduleProgress(disputeId)
},
}
})