diff --git a/vehicle-data-platform/apps/api/internal/platform/handler.go b/vehicle-data-platform/apps/api/internal/platform/handler.go index bd626cd5..6e2697b1 100644 --- a/vehicle-data-platform/apps/api/internal/platform/handler.go +++ b/vehicle-data-platform/apps/api/internal/platform/handler.go @@ -57,12 +57,13 @@ func (h *Handler) handleVehicleCoverage(w http.ResponseWriter, r *http.Request) } func (h *Handler) handleVehicleDetail(w http.ResponseWriter, r *http.Request) { - vin := strings.TrimSpace(r.URL.Query().Get("vin")) - if vin == "" { - httpx.WriteError(w, http.StatusBadRequest, "VIN_REQUIRED", "VIN 不能为空", "", traceID(r)) + keyword := firstNonEmpty(r.URL.Query().Get("keyword"), r.URL.Query().Get("vin")) + keyword = strings.TrimSpace(keyword) + if keyword == "" { + httpx.WriteError(w, http.StatusBadRequest, "VEHICLE_KEY_REQUIRED", "车辆关键词不能为空", "", traceID(r)) return } - data, err := h.service.VehicleDetail(r.Context(), vin, r.URL.Query().Get("protocol")) + data, err := h.service.VehicleDetail(r.Context(), keyword, r.URL.Query().Get("protocol")) h.write(w, r, data, err) } diff --git a/vehicle-data-platform/apps/api/internal/platform/handler_test.go b/vehicle-data-platform/apps/api/internal/platform/handler_test.go index 4b2155f3..bd6c16e8 100644 --- a/vehicle-data-platform/apps/api/internal/platform/handler_test.go +++ b/vehicle-data-platform/apps/api/internal/platform/handler_test.go @@ -76,6 +76,19 @@ func TestHandlerVehicleDetailResolvesPlateToVIN(t *testing.T) { } } +func TestHandlerVehicleDetailAcceptsKeyword(t *testing.T) { + handler := NewHandler(NewService(NewMockStore())) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/vehicles/detail?keyword=粤AG18312", nil) + handler.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String()) + } + if !strings.Contains(rec.Body.String(), `"vin":"LB9A32A24R0LS1426"`) { + t.Fatalf("response should resolve keyword to VIN: %s", rec.Body.String()) + } +} + func TestHandlerRealtimeLocations(t *testing.T) { handler := NewHandler(NewService(NewMockStore())) rec := httptest.NewRecorder() diff --git a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx index 03cd41fe..1038db11 100644 --- a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx +++ b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx @@ -1,13 +1,13 @@ import { Button, Card, Descriptions, Form, Select, Space, Table, Tabs, Tag, Toast, Typography } from '@douyinfe/semi-ui'; import { IconRefresh, IconSearch } from '@douyinfe/semi-icons'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { api } from '../api/client'; import type { QualityIssueRow, VehicleDetail as VehicleDetailData, VehicleSourceStatus } from '../api/types'; import { PageHeader } from '../components/PageHeader'; import { StatusTag } from '../components/StatusTag'; type VehicleQuery = { - vin: string; + keyword: string; protocol?: string; }; @@ -27,29 +27,44 @@ export function VehicleDetail({ onOpenHistory: (vin: string) => void; onOpenMileage: (vin: string) => void; }) { - const [query, setQuery] = useState({ vin }); + const [query, setQuery] = useState({ keyword: vin }); const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(false); + const requestSeq = useRef(0); const load = (nextQuery = query) => { - const vin = nextQuery.vin.trim(); - if (!vin) { - Toast.warning('请输入 VIN'); + const keyword = nextQuery.keyword.trim(); + if (!keyword) { + Toast.warning('请输入 VIN / 车牌 / 手机号'); return; } setLoading(true); - const params = new URLSearchParams({ vin }); + const params = new URLSearchParams({ keyword }); if (nextQuery.protocol?.trim()) { params.set('protocol', nextQuery.protocol.trim()); } + const seq = requestSeq.current + 1; + requestSeq.current = seq; api.vehicleDetail(params) - .then(setDetail) - .catch((error: Error) => Toast.error(error.message)) - .finally(() => setLoading(false)); + .then((nextDetail) => { + if (requestSeq.current === seq) { + setDetail(nextDetail); + } + }) + .catch((error: Error) => { + if (requestSeq.current === seq) { + Toast.error(error.message); + } + }) + .finally(() => { + if (requestSeq.current === seq) { + setLoading(false); + } + }); }; useEffect(() => { - const nextQuery = { vin }; + const nextQuery = { keyword: vin }; setQuery(nextQuery); load(nextQuery); }, [vin]); @@ -57,6 +72,7 @@ export function VehicleDetail({ const identity = detail?.identity; const summary = detail?.realtimeSummary; const latest = detail?.realtime[0]; + const resolvedVIN = detail?.vin || summary?.vin || identity?.vin || latest?.vin || query.keyword; const protocols = useMemo(() => detail?.sources ?? [], [detail?.sources]); const latestRaw = detail?.raw.items[0]; const qualityCount = detail?.quality.total ?? 0; @@ -68,11 +84,11 @@ export function VehicleDetail({
{ - const nextQuery = { vin: String(values.vin ?? ''), protocol: String(values.protocol ?? '') }; + const nextQuery = { keyword: String(values.keyword ?? ''), protocol: String(values.protocol ?? '') }; setQuery(nextQuery); load(nextQuery); }}> - + GB32960 JT808 @@ -81,8 +97,8 @@ export function VehicleDetail({ - - + +
@@ -92,7 +108,7 @@ export function VehicleDetail({