From 97c35cc131666acc527ac29de380ba07303fe1a9 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 30 Jun 2026 18:50:45 +0800 Subject: [PATCH] feat: add solution library pages --- frontend/src/app/router.tsx | 5 ++ .../features/solutions/SolutionLinkPanel.tsx | 48 +++++++++++++++++++ .../features/solutions/SolutionListPage.tsx | 47 ++++++++++++++++++ frontend/src/features/solutions/api.ts | 17 +++++++ frontend/src/features/solutions/types.ts | 23 +++++++++ .../src/features/tickets/TicketDetailPage.tsx | 2 + 6 files changed, 142 insertions(+) create mode 100644 frontend/src/features/solutions/SolutionLinkPanel.tsx create mode 100644 frontend/src/features/solutions/SolutionListPage.tsx create mode 100644 frontend/src/features/solutions/api.ts create mode 100644 frontend/src/features/solutions/types.ts diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx index ccfc86a..64bff69 100644 --- a/frontend/src/app/router.tsx +++ b/frontend/src/app/router.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { DesignSystemPreview } from '@/features/design-system/DesignSystemPreview' +import { SolutionListPage } from '@/features/solutions/SolutionListPage' import { TicketCreatePage } from '@/features/tickets/TicketCreatePage' import { TicketDetailPage } from '@/features/tickets/TicketDetailPage' import { TicketListPage } from '@/features/tickets/TicketListPage' @@ -22,6 +23,10 @@ export function Router() { return } + if (route === '/solutions') { + return + } + if (route === '/tickets/new') { return } diff --git a/frontend/src/features/solutions/SolutionLinkPanel.tsx b/frontend/src/features/solutions/SolutionLinkPanel.tsx new file mode 100644 index 0000000..18e0f23 --- /dev/null +++ b/frontend/src/features/solutions/SolutionLinkPanel.tsx @@ -0,0 +1,48 @@ +import * as React from 'react' + +import { AppEmptyState } from '@/components/app/AppEmptyState' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader } from '@/components/ui/card' + +import { linkSolutionToTicket, listSolutions } from './api' +import type { Solution } from './types' + +export function SolutionLinkPanel({ ticketId }: { ticketId: number }) { + const [solutions, setSolutions] = React.useState([]) + const [message, setMessage] = React.useState('') + + React.useEffect(() => { + listSolutions({ status: 'published' }).then(setSolutions).catch(() => setSolutions([])) + }, []) + + async function handleLink(solution: Solution, linkType: string) { + await linkSolutionToTicket(solution.id, ticketId, linkType) + setMessage(`${solution.title} linked as ${linkType}`) + } + + return ( + + +

Reusable Solutions

+
+ + {message ?

{message}

: null} + {solutions.length === 0 ? : null} + {solutions.map((solution) => ( +
+
+

{solution.title}

+

{solution.problem_signature}

+
+
+ + +
+
+ ))} +
+
+ ) +} diff --git a/frontend/src/features/solutions/SolutionListPage.tsx b/frontend/src/features/solutions/SolutionListPage.tsx new file mode 100644 index 0000000..760b814 --- /dev/null +++ b/frontend/src/features/solutions/SolutionListPage.tsx @@ -0,0 +1,47 @@ +import * as React from 'react' + +import { AppEmptyState } from '@/components/app/AppEmptyState' +import { AppPageHeader } from '@/components/app/AppPageHeader' +import { Badge } from '@/components/ui/badge' +import { Card, CardContent } from '@/components/ui/card' + +import { listSolutions } from './api' +import type { Solution } from './types' + +export function SolutionListPage() { + const [solutions, setSolutions] = React.useState([]) + const [error, setError] = React.useState('') + + React.useEffect(() => { + listSolutions() + .then(setSolutions) + .catch((reason: unknown) => setError(reason instanceof Error ? reason.message : '加载失败')) + }, []) + + return ( +
+
+ + {error ? : null} + {!error && solutions.length === 0 ? : null} +
+ {solutions.map((solution) => ( + + +
+

{solution.title}

+
+ {solution.status} + Used {solution.usage_count} +
+
+

{solution.problem_signature}

+

{solution.solution}

+
+
+ ))} +
+
+
+ ) +} diff --git a/frontend/src/features/solutions/api.ts b/frontend/src/features/solutions/api.ts new file mode 100644 index 0000000..b3647a6 --- /dev/null +++ b/frontend/src/features/solutions/api.ts @@ -0,0 +1,17 @@ +import { apiRequest } from '@/lib/api' + +import type { Solution, TicketSolutionLink } from './types' + +export function listSolutions(params: { status?: string } = {}) { + const query = new URLSearchParams() + if (params.status) query.set('status', params.status) + const suffix = query.toString() ? `?${query}` : '' + return apiRequest(`/solutions${suffix}`) +} + +export function linkSolutionToTicket(solutionId: number, ticketId: number, linkType: string) { + return apiRequest(`/solutions/${solutionId}/link-ticket`, { + method: 'POST', + body: JSON.stringify({ ticket_id: ticketId, link_type: linkType }) + }) +} diff --git a/frontend/src/features/solutions/types.ts b/frontend/src/features/solutions/types.ts new file mode 100644 index 0000000..74753dc --- /dev/null +++ b/frontend/src/features/solutions/types.ts @@ -0,0 +1,23 @@ +export type Solution = { + id: number + title: string + problem_signature: string + related_system: string + environment_scope: string + root_cause: string + solution: string + risk_note: string + status: string + usage_count: number + created_at: string + updated_at: string +} + +export type TicketSolutionLink = { + id: number + ticket_id: number + solution_id: number + link_type: string + solution_usage_count: number + created_at: string +} diff --git a/frontend/src/features/tickets/TicketDetailPage.tsx b/frontend/src/features/tickets/TicketDetailPage.tsx index 7c522d7..ef06d1b 100644 --- a/frontend/src/features/tickets/TicketDetailPage.tsx +++ b/frontend/src/features/tickets/TicketDetailPage.tsx @@ -7,6 +7,7 @@ import { AppPriorityBadge } from '@/components/app/AppPriorityBadge' import { AppStatusBadge } from '@/components/app/AppStatusBadge' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' +import { SolutionLinkPanel } from '@/features/solutions/SolutionLinkPanel' import { getTicket, transitionTicket } from './api' import { TicketComments } from './TicketComments' @@ -87,6 +88,7 @@ export function TicketDetailPage({ ticketId }: { ticketId: number }) { + )