"use client" import { useEffect, useState } from "react" import { zodResolver } from "@hookform/resolvers/zod" import { Resolver, useForm } from "react-hook-form" import { toast } from "sonner" import { z } from "zod/v4" import { SearchIcon } from "lucide-react" import { OptionCombobox } from "@/components/option-combobox" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Field, FieldContent, FieldError, FieldLabel } from "@/components/ui/field" import { Input } from "@/components/ui/input" import { addTicketRelation, fetchTickets, type TicketItem } from "@/lib/api/ticket" import { cn } from "@/lib/utils" const relationOptions = [ { value: "duplicate", label: "重复工单" }, { value: "related", label: "相关工单" }, { value: "parent", label: "父工单" }, { value: "child", label: "子工单" }, ] const schema = z.object({ relationType: z.string().trim().min(1, "请选择关联类型"), relatedTicketId: z.number().int().positive("请选择关联工单"), }) type FormValues = z.infer const resolver = zodResolver(schema as never) as Resolver< z.input, undefined, z.output > type TicketRelationDialogProps = { open: boolean ticketId: number | null onOpenChange: (open: boolean) => void onSuccess?: () => Promise | void } export function TicketRelationDialog({ open, ticketId, onOpenChange, onSuccess, }: TicketRelationDialogProps) { const [keyword, setKeyword] = useState("") const [searching, setSearching] = useState(false) const [searchResults, setSearchResults] = useState([]) const form = useForm< z.input, undefined, z.output >({ resolver, defaultValues: { relationType: "related", relatedTicketId: 0, }, }) const { handleSubmit, setValue, reset, watch, formState: { errors, isSubmitting }, } = form useEffect(() => { if (open) { reset({ relationType: "related", relatedTicketId: 0 }) setKeyword("") setSearchResults([]) } }, [open, reset]) useEffect(() => { if (!open) { return } const trimmedKeyword = keyword.trim() if (trimmedKeyword.length < 2) { setSearchResults([]) return } const timer = window.setTimeout(async () => { setSearching(true) try { const data = await fetchTickets({ keyword: trimmedKeyword, page: 1, limit: 8, }) const results = Array.isArray(data.results) ? data.results : [] setSearchResults(results.filter((item) => item.id !== ticketId)) } catch (error) { toast.error(error instanceof Error ? error.message : "搜索工单失败") } finally { setSearching(false) } }, 250) return () => window.clearTimeout(timer) }, [keyword, open, ticketId]) const selectedTicketId = watch("relatedTicketId") const selectedTicket = searchResults.find((item) => item.id === selectedTicketId) ?? null async function onFormSubmit(values: FormValues) { if (!ticketId) { toast.error("工单不存在") return } try { await addTicketRelation({ ticketId, relationType: values.relationType, relatedTicketId: values.relatedTicketId, }) toast.success("关联工单已添加") onOpenChange(false) await onSuccess?.() } catch (error) { toast.error(error instanceof Error ? error.message : "添加关联工单失败") } } return ( 新增关联工单
关联类型 setValue("relationType", value, { shouldValidate: true })} /> 搜索并选择工单
{ setKeyword(event.target.value) setValue("relatedTicketId", 0, { shouldValidate: true }) }} />
{searching ? (
搜索中...
) : searchResults.length > 0 ? ( searchResults.map((item) => { const active = item.id === selectedTicketId return ( ) }) ) : (
{keyword.trim().length < 2 ? "输入至少 2 个字开始搜索" : "未找到匹配工单"}
)}
{selectedTicket ? (
已选中:{selectedTicket.ticketNo} / {selectedTicket.title}
) : null}
) }