接入前端P0页面真实接口

This commit is contained in:
andy
2026-07-08 16:20:06 +08:00
parent 5c5b8e33b0
commit 66a613d6cd
45 changed files with 8719 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
import { getJson } from '@/services/httpClient'
import type { BackendHealthResult } from '@/types/reservation'
export async function fetchBackendHealth(): Promise<BackendHealthResult> {
return getJson<BackendHealthResult>('/api/health')
}

View File

@@ -0,0 +1,54 @@
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? ''
export class ApiError extends Error {
readonly status: number
readonly details: unknown
constructor(message: string, status: number, details: unknown) {
super(message)
this.name = 'ApiError'
this.status = status
this.details = details
}
}
export async function getJson<T>(path: string): Promise<T> {
return requestJson<T>(path, {
method: 'GET',
})
}
export async function sendJson<T>(path: string, method: 'POST' | 'PUT', body?: unknown): Promise<T> {
return requestJson<T>(path, {
method,
headers: {
'Content-Type': 'application/json',
},
body: body === undefined ? undefined : JSON.stringify(body),
})
}
async function requestJson<T>(path: string, init: RequestInit): Promise<T> {
const response = await fetch(`${apiBaseUrl}${path}`, {
...init,
headers: {
Accept: 'application/json',
...init.headers,
},
})
const payload = await readResponsePayload(response)
if (!response.ok) {
throw new ApiError(`Request failed with status ${response.status}`, response.status, payload)
}
return payload as T
}
async function readResponsePayload(response: Response): Promise<unknown> {
const contentType = response.headers.get('content-type') ?? ''
if (!contentType.includes('application/json')) {
return response.text()
}
return response.json()
}

View File

@@ -0,0 +1,201 @@
import {
reservationTaskAudits,
reservationTaskDetails,
} from '@/fixtures/reservation'
import { getJson, sendJson } from '@/services/httpClient'
import type {
ReservationOperaOperationResult,
ReservationOperaSimulationRequest,
ReservationOrderDetailResult,
ReservationOrderListFilters,
ReservationOrderListResult,
ReservationTaskAuditListResult,
ReservationTaskDetailResult,
ReservationTaskListFilters,
ReservationTaskListResult,
ReservationTaskPayloadMutationRequest,
ReservationTaskPayloadMutationResult,
SourceMessageConversationResult,
} from '@/types/reservation'
const useReservationFixtures = import.meta.env.VITE_RESERVATION_USE_FIXTURES === 'true'
export class EndpointPendingError extends Error {
constructor(message: string) {
super(message)
this.name = 'EndpointPendingError'
}
}
export async function fetchReservationOrders(
filters: ReservationOrderListFilters = {},
): Promise<ReservationOrderListResult> {
void filters
throw new EndpointPendingError('GET /api/reservation/orders is pending backend implementation.')
}
export async function fetchReservationTaskList(
filters: ReservationTaskListFilters = {},
): Promise<ReservationTaskListResult> {
return getJson<ReservationTaskListResult>(withQuery('/api/reservation/tasks', filters))
}
export async function fetchReservationOrderDetail(orderId: string): Promise<ReservationOrderDetailResult> {
return getJson<ReservationOrderDetailResult>(
withQuery(`/api/reservation/orders/${encodeURIComponent(orderId)}`, {
include_tasks: true,
include_source_summary: true,
}),
)
}
export async function fetchSourceMessageConversation(
sourceMessageId: string,
): Promise<SourceMessageConversationResult> {
throw new EndpointPendingError(
`GET /api/source-messages/${sourceMessageId}/conversation is pending backend implementation.`,
)
}
export async function fetchReservationTaskDetail(taskId: string): Promise<ReservationTaskDetailResult> {
if (useReservationFixtures) {
return fixtureTaskDetail(taskId)
}
return getJson<ReservationTaskDetailResult>(`/api/reservation/tasks/${taskId}`)
}
export async function saveReservationTaskDraft(
taskId: string,
request: ReservationTaskPayloadMutationRequest,
): Promise<ReservationTaskPayloadMutationResult> {
if (useReservationFixtures) {
return fixtureTaskMutation(taskId, request, false)
}
return sendJson<ReservationTaskPayloadMutationResult>(`/api/reservation/tasks/${taskId}/draft`, 'PUT', request)
}
export async function confirmReservationTask(
taskId: string,
request: ReservationTaskPayloadMutationRequest,
): Promise<ReservationTaskPayloadMutationResult> {
if (useReservationFixtures) {
return fixtureTaskMutation(taskId, request, true)
}
return sendJson<ReservationTaskPayloadMutationResult>(`/api/reservation/tasks/${taskId}/confirm`, 'POST', request)
}
export async function executeReservationOperaOperation(
taskId: string,
operationId: string,
request: ReservationOperaSimulationRequest,
): Promise<ReservationOperaOperationResult> {
if (useReservationFixtures) {
return fixtureOperaOperation(taskId, operationId, request, 'execute')
}
return sendJson<ReservationOperaOperationResult>(
`/api/reservation/tasks/${taskId}/opera-operations/${operationId}/execute`,
'POST',
request,
)
}
export async function retryReservationOperaOperation(
taskId: string,
operationId: string,
request: ReservationOperaSimulationRequest,
): Promise<ReservationOperaOperationResult> {
if (useReservationFixtures) {
return fixtureOperaOperation(taskId, operationId, request, 'retry')
}
return sendJson<ReservationOperaOperationResult>(
`/api/reservation/tasks/${taskId}/opera-operations/${operationId}/retry`,
'POST',
request,
)
}
export async function fetchReservationTaskAudits(taskId: string): Promise<ReservationTaskAuditListResult> {
if (useReservationFixtures) {
return Promise.resolve(
reservationTaskAudits.find((audit) => audit.task_id === taskId) ?? {
task_id: taskId,
items: [],
},
)
}
return getJson<ReservationTaskAuditListResult>(`/api/reservation/tasks/${taskId}/audits`)
}
function withQuery(path: string, params: object): string {
const searchParams = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
if (
value === undefined ||
value === null ||
value === '' ||
!['string', 'number', 'boolean'].includes(typeof value)
) {
return
}
searchParams.set(key, String(value))
})
const query = searchParams.toString()
return query ? `${path}?${query}` : path
}
function fixtureTaskDetail(taskId: string): Promise<ReservationTaskDetailResult> {
const detail = reservationTaskDetails.find((item) => item.task_id === taskId)
if (!detail) {
throw new Error(`Reservation task ${taskId} was not found in frontend fixture data.`)
}
return Promise.resolve(structuredClone(detail))
}
function fixtureTaskMutation(
taskId: string,
request: ReservationTaskPayloadMutationRequest,
confirmed: boolean,
): Promise<ReservationTaskPayloadMutationResult> {
return fixtureTaskDetail(taskId).then((detail) => ({
task_id: detail.task_id,
order_id: detail.order_id,
task_status: confirmed ? 'READY' : detail.task_status,
draft_payload: request.field_values,
confirmed_payload: confirmed ? request.field_values : detail.confirmed_payload,
opera_operations: confirmed ? detail.opera_operations : [],
}))
}
async function fixtureOperaOperation(
taskId: string,
operationId: string,
request: ReservationOperaSimulationRequest,
action: 'execute' | 'retry',
): Promise<ReservationOperaOperationResult> {
const detail = await fixtureTaskDetail(taskId)
const operation = detail.opera_operations.find((item) => item.operation_id === operationId)
if (!operation) {
throw new Error(`Reservation OPERA operation ${operationId} was not found in frontend fixture data.`)
}
const success = request.simulate_success !== false
const attemptNumber = operation.attempt_count + 1
return {
...operation,
operation_status: success ? 'SUCCEEDED' : 'FAILED',
attempt_count: attemptNumber,
last_error_message: success ? null : request.failure_message ?? 'Frontend simulated OPERA failure',
attempts: [
...operation.attempts,
{
attempt_id: `${operationId}-${action}-${attemptNumber}`,
attempt_number: attemptNumber,
attempt_status: success ? 'SUCCEEDED' : 'FAILED',
response_payload: success ? { simulated: true } : null,
error_message: success ? null : request.failure_message ?? 'Frontend simulated OPERA failure',
started_at: new Date().toISOString(),
finished_at: new Date().toISOString(),
},
],
}
}