# Vehicle Data Platform Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build and deploy a new `vehicle-data-platform` project with a Go BFF backend and a React + TypeScript + Semi UI frontend for vehicle data management. **Architecture:** The platform is a new project folder under the existing repository. The Go API acts as a BFF that reads MySQL, Redis, TDengine, and local ops endpoints, then serves unified `/api/*` responses plus the built frontend. The frontend is a Semi UI console with dashboard, vehicles, realtime, vehicle detail, history, mileage, and quality/ops pages. **Tech Stack:** Go, React, TypeScript, Vite, Semi UI, ECharts, MySQL, Redis, TDengine, systemd. --- ## File Structure Create this structure: ```text vehicle-data-platform/ apps/ api/ go.mod cmd/platform-api/main.go internal/app/server.go internal/app/routes.go internal/config/config.go internal/httpx/response.go internal/httpx/response_test.go internal/platform/model.go internal/platform/mock_store.go internal/platform/service.go internal/platform/service_test.go internal/platform/handler.go internal/platform/handler_test.go internal/static/static.go web/ package.json index.html tsconfig.json vite.config.ts src/main.tsx src/App.tsx src/styles/tokens.css src/styles/global.css src/api/client.ts src/api/types.ts src/layout/AppShell.tsx src/components/StatusTag.tsx src/components/PageHeader.tsx src/components/DataEmpty.tsx src/pages/Dashboard.tsx src/pages/Vehicles.tsx src/pages/Realtime.tsx src/pages/VehicleDetail.tsx src/pages/History.tsx src/pages/Mileage.tsx src/pages/Quality.tsx src/test/App.test.tsx src/test/setup.ts deploy/ systemd/lingniu-vehicle-platform.service docs/ product-spec.md api-contract.md deployment.md package.json ``` Do not modify the existing Go gateway services unless a task explicitly says so. ## Task 1: Visual Concept And Frontend Design System **Files:** - Create: `vehicle-data-platform/docs/product-spec.md` - Create: `vehicle-data-platform/apps/web/src/styles/tokens.css` - Create: `vehicle-data-platform/apps/web/src/styles/global.css` - [ ] **Step 1: Write the product UI brief** Create `vehicle-data-platform/docs/product-spec.md` with: ```markdown # Vehicle Data Platform Product Spec ## Visual Direction The UI is a professional vehicle data operations console. It uses Semi UI as the component base, a restrained gray-white control-room palette, dense but readable tables, right-side drawers for detail, and blue-cyan accents for primary actions. It should feel closer to a cloud data console than a marketing dashboard. ## Primary Screens 1. Dashboard: KPI summary, protocol distribution, vehicle map preview, quality issues, and link health. 2. Vehicles: table-first identity and vehicle registry. 3. Realtime: table/map switch for current vehicle state. 4. Vehicle Detail: identity header and tabs for latest state, history, raw, mileage, and quality. 5. History: location and raw-frame query workspace. 6. Mileage: daily and range mileage analysis. 7. Quality: data quality and link health workspace. ## Interaction Rules - Tables are the default data surface. - Filters stay above the table. - Details open in a right drawer unless a full route is needed. - Error and empty states must be explicit. - No decorative hero, no oversized marketing card layout, no dark large-screen style. ``` - [ ] **Step 2: Create design tokens** Create `vehicle-data-platform/apps/web/src/styles/tokens.css`: ```css :root { --vp-bg: #f5f7fb; --vp-surface: #ffffff; --vp-surface-muted: #f9fafc; --vp-border: #e6e9f0; --vp-text: #172033; --vp-text-muted: #667085; --vp-text-subtle: #98a2b3; --vp-primary: #1664ff; --vp-primary-hover: #0f4fd6; --vp-cyan: #00a4b8; --vp-success: #12b76a; --vp-warning: #f79009; --vp-danger: #f04438; --vp-radius: 8px; --vp-shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06); --vp-shell-sidebar: 232px; --vp-shell-header: 56px; --vp-page-gutter: 24px; } ``` - [ ] **Step 3: Create global styling** Create `vehicle-data-platform/apps/web/src/styles/global.css`: ```css @import './tokens.css'; * { box-sizing: border-box; } body { margin: 0; min-width: 1280px; background: var(--vp-bg); color: var(--vp-text); font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } .vp-page { padding: var(--vp-page-gutter); } .vp-section { background: var(--vp-surface); border: 1px solid var(--vp-border); border-radius: var(--vp-radius); box-shadow: var(--vp-shadow-sm); } ``` - [ ] **Step 4: Commit** ```bash git add vehicle-data-platform/docs/product-spec.md vehicle-data-platform/apps/web/src/styles/tokens.css vehicle-data-platform/apps/web/src/styles/global.css git commit -m "docs(platform): define product UI direction" ``` ## Task 2: Scaffold Monorepo And Tooling **Files:** - Create: `vehicle-data-platform/package.json` - Create: `vehicle-data-platform/apps/web/package.json` - Create: `vehicle-data-platform/apps/web/index.html` - Create: `vehicle-data-platform/apps/web/tsconfig.json` - Create: `vehicle-data-platform/apps/web/vite.config.ts` - Create: `vehicle-data-platform/apps/web/src/test/setup.ts` - Create: `vehicle-data-platform/apps/api/go.mod` - [ ] **Step 1: Create root package file** Create `vehicle-data-platform/package.json`: ```json { "private": true, "scripts": { "web:dev": "npm --prefix apps/web run dev", "web:build": "npm --prefix apps/web run build", "web:test": "npm --prefix apps/web run test", "api:test": "cd apps/api && go test ./...", "build": "npm run web:build && cd apps/api && go build -o ../../dist/platform-api ./cmd/platform-api" } } ``` - [ ] **Step 2: Create web package** Create `vehicle-data-platform/apps/web/package.json`: ```json { "private": true, "type": "module", "scripts": { "dev": "vite --host 0.0.0.0 --port 20301", "build": "tsc -b && vite build", "test": "vitest run" }, "dependencies": { "@douyinfe/semi-icons": "^2.71.0", "@douyinfe/semi-ui": "^2.71.0", "@vitejs/plugin-react": "^4.3.4", "echarts": "^5.6.0", "react": "^18.3.1", "react-dom": "^18.3.1", "vite": "^6.0.0" }, "devDependencies": { "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "typescript": "^5.7.2", "vitest": "^2.1.8" } } ``` - [ ] **Step 3: Create Vite entry files** Create `vehicle-data-platform/apps/web/index.html`: ```html 车辆数据管理中台
``` Create `vehicle-data-platform/apps/web/tsconfig.json`: ```json { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["DOM", "DOM.Iterable", "ES2020"], "allowJs": false, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "module": "ESNext", "moduleResolution": "Node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": ["src"] } ``` Create `vehicle-data-platform/apps/web/vite.config.ts`: ```ts import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [react()], server: { proxy: { '/api': 'http://127.0.0.1:20300' } }, test: { environment: 'jsdom', setupFiles: './src/test/setup.ts' } }); ``` Create `vehicle-data-platform/apps/web/src/test/setup.ts`: ```ts import '@testing-library/jest-dom/vitest'; ``` - [ ] **Step 4: Create API module** Create `vehicle-data-platform/apps/api/go.mod`: ```go module lingniu-vehicle-platform/api go 1.23 require ( github.com/go-sql-driver/mysql v1.8.1 github.com/redis/go-redis/v9 v9.7.0 github.com/taosdata/driver-go/v3 v3.6.0 ) ``` - [ ] **Step 5: Verify scaffolding** Run: ```bash cd vehicle-data-platform/apps/api && go test ./... cd ../../apps/web && npm install && npm run test ``` Expected: - Go reports no packages or passing packages. - Web dependencies install and Vitest reports no tests or passing tests. - [ ] **Step 6: Commit** ```bash git add vehicle-data-platform/package.json vehicle-data-platform/apps/web vehicle-data-platform/apps/api/go.mod git commit -m "chore(platform): scaffold vehicle data platform" ``` ## Task 3: Backend Response Envelope And Config **Files:** - Create: `vehicle-data-platform/apps/api/internal/httpx/response.go` - Create: `vehicle-data-platform/apps/api/internal/httpx/response_test.go` - Create: `vehicle-data-platform/apps/api/internal/config/config.go` - [ ] **Step 1: Write response tests** Create `vehicle-data-platform/apps/api/internal/httpx/response_test.go`: ```go package httpx import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" ) func TestWriteOKWrapsData(t *testing.T) { rec := httptest.NewRecorder() WriteOK(rec, "trace-1", map[string]any{"online": 12}) if rec.Code != http.StatusOK { t.Fatalf("status = %d", rec.Code) } var body map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body["traceId"] != "trace-1" { t.Fatalf("traceId = %#v", body["traceId"]) } data := body["data"].(map[string]any) if data["online"].(float64) != 12 { t.Fatalf("data = %#v", data) } } func TestWriteErrorWrapsError(t *testing.T) { rec := httptest.NewRecorder() WriteError(rec, http.StatusBadRequest, "BAD_QUERY", "查询失败", "vin required", "trace-2") if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d", rec.Code) } if got := rec.Body.String(); !containsAll(got, []string{"BAD_QUERY", "查询失败", "vin required", "trace-2"}) { t.Fatalf("body = %s", got) } } func containsAll(value string, parts []string) bool { for _, part := range parts { if !strings.Contains(value, part) { return false } } return true } ``` - [ ] **Step 2: Run response tests and see failure** Run: ```bash cd vehicle-data-platform/apps/api go test ./internal/httpx ``` Expected: fail because `WriteOK` and `WriteError` are undefined. - [ ] **Step 3: Implement response helpers** Create `vehicle-data-platform/apps/api/internal/httpx/response.go`: ```go package httpx import ( "encoding/json" "net/http" "time" ) type Envelope struct { Data any `json:"data,omitempty"` Error *Error `json:"error,omitempty"` TraceID string `json:"traceId"` Timestamp int64 `json:"timestamp"` } type Error struct { Code string `json:"code"` Message string `json:"message"` Detail string `json:"detail,omitempty"` } func WriteOK(w http.ResponseWriter, traceID string, data any) { writeJSON(w, http.StatusOK, Envelope{Data: data, TraceID: traceID, Timestamp: time.Now().UnixMilli()}) } func WriteError(w http.ResponseWriter, status int, code string, message string, detail string, traceID string) { writeJSON(w, status, Envelope{Error: &Error{Code: code, Message: message, Detail: detail}, TraceID: traceID, Timestamp: time.Now().UnixMilli()}) } func writeJSON(w http.ResponseWriter, status int, value any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(value) } ``` - [ ] **Step 4: Implement config** Create `vehicle-data-platform/apps/api/internal/config/config.go`: ```go package config import ( "os" "strconv" "strings" "time" ) type Config struct { HTTPAddr string MySQLDSN string RedisAddr string RedisUsername string RedisPassword string RedisDB int TDengineDSN string TDengineDatabase string CapacityCheckBin string AuthToken string RequestTimeout time.Duration } func Load() Config { return Config{ HTTPAddr: env("HTTP_ADDR", ":20300"), MySQLDSN: env("MYSQL_DSN", ""), RedisAddr: env("REDIS_ADDR", "127.0.0.1:6379"), RedisUsername: env("REDIS_USERNAME", ""), RedisPassword: env("REDIS_PASSWORD", ""), RedisDB: envInt("REDIS_DB", 0), TDengineDSN: env("TDENGINE_DSN", ""), TDengineDatabase: env("TDENGINE_DATABASE", "lingniu_vehicle_ts"), CapacityCheckBin: env("CAPACITY_CHECK_BIN", "/opt/lingniu-go-native/current/capacity-check"), AuthToken: env("AUTH_TOKEN", ""), RequestTimeout: time.Duration(envInt("REQUEST_TIMEOUT_MS", 5000)) * time.Millisecond, } } func env(key string, fallback string) string { value := strings.TrimSpace(os.Getenv(key)) if value == "" { return fallback } return value } func envInt(key string, fallback int) int { value := strings.TrimSpace(os.Getenv(key)) if value == "" { return fallback } parsed, err := strconv.Atoi(value) if err != nil { return fallback } return parsed } ``` - [ ] **Step 5: Run tests** Run: ```bash cd vehicle-data-platform/apps/api go test ./... ``` Expected: pass. - [ ] **Step 6: Commit** ```bash git add vehicle-data-platform/apps/api/internal/httpx vehicle-data-platform/apps/api/internal/config git commit -m "feat(platform-api): add response envelope and config" ``` ## Task 4: Backend Platform Service With Mock Store **Files:** - Create: `vehicle-data-platform/apps/api/internal/platform/model.go` - Create: `vehicle-data-platform/apps/api/internal/platform/mock_store.go` - Create: `vehicle-data-platform/apps/api/internal/platform/service.go` - Create: `vehicle-data-platform/apps/api/internal/platform/service_test.go` - [ ] **Step 1: Write service tests** Create `vehicle-data-platform/apps/api/internal/platform/service_test.go`: ```go package platform import ( "context" "testing" ) func TestServiceDashboardSummary(t *testing.T) { service := NewService(NewMockStore()) summary, err := service.DashboardSummary(context.Background()) if err != nil { t.Fatal(err) } if summary.OnlineVehicles == 0 { t.Fatalf("online vehicles should be positive: %#v", summary) } if len(summary.Protocols) == 0 { t.Fatalf("protocols missing: %#v", summary) } } func TestServiceVehicleListPaginates(t *testing.T) { service := NewService(NewMockStore()) page, err := service.ListVehicles(context.Background(), VehicleQuery{Limit: 1, Offset: 0}) if err != nil { t.Fatal(err) } if len(page.Items) != 1 || page.Total < 1 { t.Fatalf("page = %#v", page) } } ``` - [ ] **Step 2: Run service tests and see failure** Run: ```bash cd vehicle-data-platform/apps/api go test ./internal/platform ``` Expected: fail because package types are undefined. - [ ] **Step 3: Implement models** Create `vehicle-data-platform/apps/api/internal/platform/model.go`: ```go package platform type Page[T any] struct { Items []T `json:"items"` Total int64 `json:"total"` Limit int `json:"limit"` Offset int `json:"offset"` } type ProtocolStat struct { Protocol string `json:"protocol"` Online int64 `json:"online"` Total int64 `json:"total"` } type DashboardSummary struct { OnlineVehicles int64 `json:"onlineVehicles"` ActiveToday int64 `json:"activeToday"` FrameToday int64 `json:"frameToday"` IssueVehicles int64 `json:"issueVehicles"` KafkaLag int64 `json:"kafkaLag"` Protocols []ProtocolStat `json:"protocols"` LinkHealth []LinkHealth `json:"linkHealth"` } type LinkHealth struct { Name string `json:"name"` Status string `json:"status"` Detail string `json:"detail,omitempty"` } type VehicleQuery struct { Keyword string Protocol string Online string Limit int Offset int } type VehicleRow struct { VIN string `json:"vin"` Plate string `json:"plate"` Phone string `json:"phone"` OEM string `json:"oem"` Protocol string `json:"protocol"` Online bool `json:"online"` LastSeen string `json:"lastSeen"` LocationText string `json:"locationText"` BindingScore int `json:"bindingScore"` } ``` - [ ] **Step 4: Implement mock store** Create `vehicle-data-platform/apps/api/internal/platform/mock_store.go`: ```go package platform import "context" type Store interface { DashboardSummary(context.Context) (DashboardSummary, error) ListVehicles(context.Context, VehicleQuery) (Page[VehicleRow], error) } type MockStore struct { vehicles []VehicleRow } func NewMockStore() *MockStore { return &MockStore{vehicles: []VehicleRow{ {VIN: "LB9A32A24R0LS1426", Plate: "粤AG18312", Phone: "13307795425", OEM: "G7s", Protocol: "JT808", Online: true, LastSeen: "2026-07-03 20:00:00", LocationText: "广东 广州", BindingScore: 100}, {VIN: "LNXNEGRR7SR318212", Plate: "川A00001", Phone: "", OEM: "Hyundai", Protocol: "GB32960", Online: true, LastSeen: "2026-07-03 20:00:03", LocationText: "四川 成都", BindingScore: 80}, }} } func (s *MockStore) DashboardSummary(context.Context) (DashboardSummary, error) { return DashboardSummary{ OnlineVehicles: 2, ActiveToday: 2, FrameToday: 138240, IssueVehicles: 1, KafkaLag: 0, Protocols: []ProtocolStat{ {Protocol: "GB32960", Online: 1, Total: 1}, {Protocol: "JT808", Online: 1, Total: 1}, {Protocol: "YUTONG_MQTT", Online: 0, Total: 0}, }, LinkHealth: []LinkHealth{ {Name: "Gateway", Status: "ok"}, {Name: "NATS", Status: "ok"}, {Name: "Kafka", Status: "ok"}, {Name: "TDengine", Status: "ok"}, {Name: "Redis", Status: "ok"}, {Name: "MySQL", Status: "ok"}, }, }, nil } func (s *MockStore) ListVehicles(_ context.Context, query VehicleQuery) (Page[VehicleRow], error) { limit := query.Limit if limit <= 0 { limit = 20 } offset := query.Offset if offset < 0 { offset = 0 } end := offset + limit if end > len(s.vehicles) { end = len(s.vehicles) } items := []VehicleRow{} if offset < len(s.vehicles) { items = s.vehicles[offset:end] } return Page[VehicleRow]{Items: items, Total: int64(len(s.vehicles)), Limit: limit, Offset: offset}, nil } ``` - [ ] **Step 5: Implement service** Create `vehicle-data-platform/apps/api/internal/platform/service.go`: ```go package platform import "context" type Service struct { store Store } func NewService(store Store) *Service { if store == nil { store = NewMockStore() } return &Service{store: store} } func (s *Service) DashboardSummary(ctx context.Context) (DashboardSummary, error) { return s.store.DashboardSummary(ctx) } func (s *Service) ListVehicles(ctx context.Context, query VehicleQuery) (Page[VehicleRow], error) { if query.Limit <= 0 { query.Limit = 20 } if query.Limit > 200 { query.Limit = 200 } if query.Offset < 0 { query.Offset = 0 } return s.store.ListVehicles(ctx, query) } ``` - [ ] **Step 6: Run tests** Run: ```bash cd vehicle-data-platform/apps/api go test ./internal/platform ``` Expected: pass. - [ ] **Step 7: Commit** ```bash git add vehicle-data-platform/apps/api/internal/platform git commit -m "feat(platform-api): add platform service model" ``` ## Task 5: Backend HTTP Routes And Static Serving **Files:** - Create: `vehicle-data-platform/apps/api/internal/platform/handler.go` - Create: `vehicle-data-platform/apps/api/internal/platform/handler_test.go` - Create: `vehicle-data-platform/apps/api/internal/app/server.go` - Create: `vehicle-data-platform/apps/api/internal/app/routes.go` - Create: `vehicle-data-platform/apps/api/cmd/platform-api/main.go` - [ ] **Step 1: Write handler tests** Create `vehicle-data-platform/apps/api/internal/platform/handler_test.go`: ```go package platform import ( "net/http" "net/http/httptest" "strings" "testing" ) func TestHandlerDashboardSummary(t *testing.T) { handler := NewHandler(NewService(NewMockStore())) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/dashboard/summary", nil) handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String()) } for _, want := range []string{"onlineVehicles", "protocols", "linkHealth"} { if !strings.Contains(rec.Body.String(), want) { t.Fatalf("response missing %s: %s", want, rec.Body.String()) } } } func TestHandlerVehicleList(t *testing.T) { handler := NewHandler(NewService(NewMockStore())) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/vehicles?limit=1", nil) handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String()) } for _, want := range []string{"LB9A32A24R0LS1426", "items", "total"} { if !strings.Contains(rec.Body.String(), want) { t.Fatalf("response missing %s: %s", want, rec.Body.String()) } } } ``` - [ ] **Step 2: Run handler tests and see failure** Run: ```bash cd vehicle-data-platform/apps/api go test ./internal/platform -run Handler ``` Expected: fail because `NewHandler` is undefined. - [ ] **Step 3: Implement handler** Create `vehicle-data-platform/apps/api/internal/platform/handler.go`: ```go package platform import ( "net/http" "strconv" "strings" "lingniu-vehicle-platform/api/internal/httpx" ) type Handler struct { service *Service } func NewHandler(service *Service) *Handler { if service == nil { service = NewService(NewMockStore()) } return &Handler{service: service} } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { traceID := r.Header.Get("X-Trace-Id") if traceID == "" { traceID = "local" } path := strings.Trim(r.URL.Path, "/") switch { case r.Method == http.MethodGet && path == "api/dashboard/summary": data, err := h.service.DashboardSummary(r.Context()) if err != nil { httpx.WriteError(w, http.StatusInternalServerError, "QUERY_FAILED", "查询失败", err.Error(), traceID) return } httpx.WriteOK(w, traceID, data) case r.Method == http.MethodGet && path == "api/vehicles": limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) data, err := h.service.ListVehicles(r.Context(), VehicleQuery{ Keyword: r.URL.Query().Get("keyword"), Protocol: r.URL.Query().Get("protocol"), Online: r.URL.Query().Get("online"), Limit: limit, Offset: offset, }) if err != nil { httpx.WriteError(w, http.StatusInternalServerError, "QUERY_FAILED", "查询失败", err.Error(), traceID) return } httpx.WriteOK(w, traceID, data) default: httpx.WriteError(w, http.StatusNotFound, "NOT_FOUND", "接口不存在", path, traceID) } } ``` - [ ] **Step 4: Implement app routes** Create `vehicle-data-platform/apps/api/internal/app/routes.go`: ```go package app import ( "net/http" "lingniu-vehicle-platform/api/internal/platform" ) func NewMux(service *platform.Service, static http.Handler) *http.ServeMux { mux := http.NewServeMux() api := platform.NewHandler(service) mux.Handle("/api/", api) if static != nil { mux.Handle("/", static) } return mux } ``` Create `vehicle-data-platform/apps/api/internal/app/server.go`: ```go package app import ( "net/http" "time" ) func NewServer(addr string, handler http.Handler) *http.Server { return &http.Server{ Addr: addr, Handler: handler, ReadHeaderTimeout: 5 * time.Second, } } ``` - [ ] **Step 5: Implement main** Create `vehicle-data-platform/apps/api/cmd/platform-api/main.go`: ```go package main import ( "log" "net/http" "os" "lingniu-vehicle-platform/api/internal/app" "lingniu-vehicle-platform/api/internal/config" "lingniu-vehicle-platform/api/internal/platform" ) func main() { cfg := config.Load() service := platform.NewService(platform.NewMockStore()) static := http.FileServer(http.Dir(os.Getenv("STATIC_DIR"))) server := app.NewServer(cfg.HTTPAddr, app.NewMux(service, static)) log.Printf("vehicle platform api listening on %s", cfg.HTTPAddr) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) } } ``` - [ ] **Step 6: Run tests and build** Run: ```bash cd vehicle-data-platform/apps/api go test ./... go build ./cmd/platform-api ``` Expected: pass. - [ ] **Step 7: Commit** ```bash git add vehicle-data-platform/apps/api git commit -m "feat(platform-api): expose dashboard and vehicle APIs" ``` ## Task 6: Frontend App Shell With Semi UI **Files:** - Create: `vehicle-data-platform/apps/web/src/main.tsx` - Create: `vehicle-data-platform/apps/web/src/App.tsx` - Create: `vehicle-data-platform/apps/web/src/layout/AppShell.tsx` - Create: `vehicle-data-platform/apps/web/src/components/PageHeader.tsx` - Create: `vehicle-data-platform/apps/web/src/components/StatusTag.tsx` - Create: `vehicle-data-platform/apps/web/src/components/DataEmpty.tsx` - Create: `vehicle-data-platform/apps/web/src/test/App.test.tsx` - [ ] **Step 1: Write app shell test** Create `vehicle-data-platform/apps/web/src/test/App.test.tsx`: ```tsx import { render, screen } from '@testing-library/react'; import App from '../App'; test('renders vehicle data platform navigation', () => { render(); expect(screen.getByText('车辆数据管理中台')).toBeInTheDocument(); expect(screen.getByText('总览工作台')).toBeInTheDocument(); expect(screen.getByText('车辆台账')).toBeInTheDocument(); expect(screen.getByText('实时监控')).toBeInTheDocument(); }); ``` - [ ] **Step 2: Run test and see failure** Run: ```bash cd vehicle-data-platform/apps/web npm run test ``` Expected: fail because `App` does not exist. - [ ] **Step 3: Implement entry and shell** Create `vehicle-data-platform/apps/web/src/main.tsx`: ```tsx import React from 'react'; import ReactDOM from 'react-dom/client'; import '@douyinfe/semi-ui/dist/css/semi.min.css'; import './styles/global.css'; import App from './App'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ); ``` Create `vehicle-data-platform/apps/web/src/App.tsx`: ```tsx import { useState } from 'react'; import { Dashboard } from './pages/Dashboard'; import { Vehicles } from './pages/Vehicles'; import { Realtime } from './pages/Realtime'; import { History } from './pages/History'; import { Mileage } from './pages/Mileage'; import { Quality } from './pages/Quality'; import { VehicleDetail } from './pages/VehicleDetail'; import { AppShell, PageKey } from './layout/AppShell'; export default function App() { const [page, setPage] = useState('dashboard'); return ( {page === 'dashboard' && } {page === 'vehicles' && } {page === 'realtime' && } {page === 'detail' && } {page === 'history' && } {page === 'mileage' && } {page === 'quality' && } ); } ``` Create `vehicle-data-platform/apps/web/src/layout/AppShell.tsx`: ```tsx import { Layout, Nav, Typography } from '@douyinfe/semi-ui'; import { IconHome, IconMapPin, IconServer, IconSetting, IconHistogram, IconSearch, IconList } from '@douyinfe/semi-icons'; import type { ReactNode } from 'react'; export type PageKey = 'dashboard' | 'vehicles' | 'realtime' | 'detail' | 'history' | 'mileage' | 'quality'; const items = [ { itemKey: 'dashboard', text: '总览工作台', icon: }, { itemKey: 'vehicles', text: '车辆台账', icon: }, { itemKey: 'realtime', text: '实时监控', icon: }, { itemKey: 'detail', text: '车辆详情', icon: }, { itemKey: 'history', text: '历史数据', icon: }, { itemKey: 'mileage', text: '里程与统计', icon: }, { itemKey: 'quality', text: '数据质量与链路健康', icon: } ]; export function AppShell({ active, onChange, children }: { active: PageKey; onChange: (key: PageKey) => void; children: ReactNode }) { return (
车辆数据管理中台