diff --git a/frontend/src/features/solutions/SolutionLinkPanel.tsx b/frontend/src/features/solutions/SolutionLinkPanel.tsx index 91712f0..1eb8e2c 100644 --- a/frontend/src/features/solutions/SolutionLinkPanel.tsx +++ b/frontend/src/features/solutions/SolutionLinkPanel.tsx @@ -16,10 +16,12 @@ const LINK_TYPE_LABELS: Record = { export function SolutionLinkPanel({ ticketId, - onTicketChanged + onTicketChanged, + onLinkedSolutionsChange }: { ticketId: number onTicketChanged?: () => void + onLinkedSolutionsChange?: (solutions: TicketSolution[]) => void }) { const [solutions, setSolutions] = React.useState([]) const [linkedSolutions, setLinkedSolutions] = React.useState([]) @@ -29,7 +31,15 @@ export function SolutionLinkPanel({ function refresh() { listSolutions({ status: 'published' }).then(setSolutions).catch(() => setSolutions([])) - listTicketSolutions(ticketId).then(setLinkedSolutions).catch(() => setLinkedSolutions([])) + listTicketSolutions(ticketId) + .then((links) => { + setLinkedSolutions(links) + onLinkedSolutionsChange?.(links) + }) + .catch(() => { + setLinkedSolutions([]) + onLinkedSolutionsChange?.([]) + }) } React.useEffect(() => { diff --git a/frontend/src/features/solutions/solutionSelection.ts b/frontend/src/features/solutions/solutionSelection.ts new file mode 100644 index 0000000..4b6e76b --- /dev/null +++ b/frontend/src/features/solutions/solutionSelection.ts @@ -0,0 +1,14 @@ +import type { TicketSolution } from './types' + +const LINK_TYPE_WEIGHT: Record = { + adopted: 2, + referenced: 1 +} + +export function selectDefaultTicketSolution(links: TicketSolution[]) { + return [...links].sort((left, right) => { + const weightDiff = (LINK_TYPE_WEIGHT[right.link_type] ?? 0) - (LINK_TYPE_WEIGHT[left.link_type] ?? 0) + if (weightDiff !== 0) return weightDiff + return new Date(right.created_at).getTime() - new Date(left.created_at).getTime() + })[0] +} diff --git a/frontend/src/features/tickets/TicketDetailPage.tsx b/frontend/src/features/tickets/TicketDetailPage.tsx index ab5793f..e16b1e3 100644 --- a/frontend/src/features/tickets/TicketDetailPage.tsx +++ b/frontend/src/features/tickets/TicketDetailPage.tsx @@ -13,6 +13,8 @@ import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { Separator } from '@/components/ui/separator' import { SolutionLinkPanel } from '@/features/solutions/SolutionLinkPanel' +import { selectDefaultTicketSolution } from '@/features/solutions/solutionSelection' +import type { TicketSolution } from '@/features/solutions/types' import { formatDateTime } from '@/lib/format' import { getTicket, listTicketEvents, transitionTicket } from './api' @@ -64,6 +66,7 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) { const [targetActionLabel, setTargetActionLabel] = React.useState('') const [transitionDialogOpen, setTransitionDialogOpen] = React.useState(false) const [transitionError, setTransitionError] = React.useState('') + const [linkedSolutions, setLinkedSolutions] = React.useState([]) React.useEffect(() => { refreshTicket() @@ -127,6 +130,15 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) { } const nextStatuses = getTicketNextStatuses(ticket.category_code, ticket.status) + const defaultLinkedSolution = selectDefaultTicketSolution(linkedSolutions) + const transitionDefaultResolution = + targetStatus === 'resolved' || targetStatus === 'closed' + ? ticket.resolution || defaultLinkedSolution?.solution.solution || '' + : '' + const transitionDefaultRemark = + targetStatus === 'closed' && transitionDefaultResolution && !ticket.resolution + ? '根据关联解决方案关闭工单' + : '' return ( @@ -195,6 +207,7 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) { { refreshTicket() refreshEvents() @@ -254,6 +267,8 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) { open={transitionDialogOpen} status={targetStatus} actionLabel={targetActionLabel} + defaultResolution={transitionDefaultResolution} + defaultRemark={transitionDefaultRemark} onOpenChange={setTransitionDialogOpen} onSubmit={handleTransition} /> diff --git a/frontend/src/features/tickets/TicketTransitionDialog.tsx b/frontend/src/features/tickets/TicketTransitionDialog.tsx index c005bbf..b8e45cd 100644 --- a/frontend/src/features/tickets/TicketTransitionDialog.tsx +++ b/frontend/src/features/tickets/TicketTransitionDialog.tsx @@ -26,6 +26,8 @@ type TicketTransitionDialogProps = { open: boolean status: TicketStatus | null actionLabel?: string + defaultResolution?: string + defaultRemark?: string onOpenChange: (open: boolean) => void onSubmit: ( status: TicketStatus, @@ -42,6 +44,8 @@ export function TicketTransitionDialog({ open, status, actionLabel: customActionLabel, + defaultResolution = '', + defaultRemark = '', onOpenChange, onSubmit }: TicketTransitionDialogProps) { @@ -51,16 +55,22 @@ export function TicketTransitionDialog({ const [submitting, setSubmitting] = React.useState(false) const actionLabel = customActionLabel ?? (status ? STATUS_ACTION_LABELS[status] ?? '调整状态' : '调整状态') const isResolveAction = status === 'resolved' + const isOutcomeAction = status === 'resolved' || status === 'closed' const canSubmit = isResolveAction ? Boolean((resolution || remark).trim()) : Boolean(remark.trim()) React.useEffect(() => { - if (!open) { - setRemark('') - setResolution('') + if (open) { + setRemark(defaultRemark) + setResolution(defaultResolution) setRepliedToCustomer(true) setSubmitting(false) + return } - }, [open]) + setRemark('') + setResolution('') + setRepliedToCustomer(true) + setSubmitting(false) + }, [defaultRemark, defaultResolution, open]) async function handleSubmit(event: React.FormEvent) { event.preventDefault() @@ -86,14 +96,14 @@ export function TicketTransitionDialog({ {actionLabel} - {isResolveAction ? '填写本次处理结论,系统会同步记录到工单详情和流转时间线。' : '请说明本次流转的原因、处理动作或待确认事项。'} + {isOutcomeAction ? '填写本次处理结论,系统会同步记录到工单详情和流转时间线。' : '请说明本次流转的原因、处理动作或待确认事项。'} - {isResolveAction ? ( - + {isOutcomeAction ? ( +