@vention/machine-apps-components

中文文档
1.30.27发布于 2026/11/03MITTypeScriptES Module
原始使用文档
文档目录9 节
  1. Components
  2. NavigationBar
  3. StatusTopBar
  4. Logs
  5. Development
  6. Running Tests
  7. Testing Setup
  8. Building
  9. Linting

Machine Apps Components

Reusable components for machine applications.

Components

NavigationBar

A navigation bar component with support for:

  • Multiple navigation items with icons
  • Control Center button
  • Support button
  • Optional timer display

Props

interface NavigationItem {
  label: string
  path: string
  icon: ReactNode
  onClick?: () => void // Optional: Custom click handler (overrides default navigation)
}

interface NavigationBarProps {
  navigationItems: NavigationItem[]
  showTimer?: boolean // Default: true
}

Example Usage

Basic Usage
import { NavigationBar } from "@vention/machine-apps-components"
import { VentionIcon } from "@vention/machine-ui"

const NAV_ITEMS = [
  {
    label: "Operation",
    path: "/operation",
    icon: <VentionIcon size={32} type="category-filled" color="white" />,
  },
  {
    label: "Settings",
    path: "/settings",
    icon: <VentionIcon size={32} type="tool-1" color="white" />,
  },
]

// With timer (default)
<NavigationBar navigationItems={NAV_ITEMS} />

// Without timer
<NavigationBar navigationItems={NAV_ITEMS} showTimer={false} />
Custom Click Handlers

You can override the default navigation behavior by providing custom click handlers:

// Custom handler for navigation items
const NAV_ITEMS = [
  {
    label: "Settings",
    path: "/settings",
    icon: <VentionIcon size={32} type="tool-1" color="white" />,
    onClick: () => {
      // Show confirmation before navigating
      if (confirm("You have unsaved changes. Continue?")) {
        navigate("/settings")
      }
    },
  },
  {
    label: "Logs",
    path: "/logs",
    icon: <VentionIcon size={32} type="alarm-bell-filled" color="white" />,
    onClick: () => {
      // Do something before navigating
      localStorage.setItem("lastView", "operation")
      navigate("/logs")
    },
  },
]

Note: When a custom onClick handler is provided, it completely overrides the default navigation behavior. The handler is responsible for any navigation logic if needed.

StatusTopBar

A status bar component positioned at the top of the screen that displays:

  • Status indicator with label and colored dot
  • Configurable action buttons with visibility and disabled states
  • Custom styling for buttons

Key Features:

  • ✅ Declarative, config-based API
  • ✅ Fully controlled component (no internal state)
  • ✅ Type-safe button configurations
  • ✅ Easy to test and reason about

Props

interface ButtonConfig {
  id: string // Unique identifier for the button
  label: string // Button text
  onClick?: () => void // Click handler
  visible?: boolean // Show/hide button (default: true)
  disabled?: boolean // Enable/disable button (default: false)
  backgroundColor?: string // Button background color (e.g., "#4CAF50")
  backgroundColorHover?: string // Hover background color
  borderColor?: string // Border color
  textColor?: string // Text color
  width?: number // Button width in pixels (default: 208)
  height?: number // Button height in pixels (default: 80)
  icon?: ReactNode // Icon to display when enabled
  iconDisabled?: ReactNode // Icon to display when disabled
}

interface StatusTopBarProps {
  status?: {
    label: string // Status label text
    color: string // Color of the status dot
  }
  buttonConfigs?: ButtonConfig[] // Array of button configurations
}

Example Usage

Basic Usage
import { StatusTopBar, ButtonConfig } from "@vention/machine-apps-components"
import { VentionIcon } from "@vention/machine-ui"

function App() {
  const buttonConfigs: ButtonConfig[] = [
    {
      id: "start",
      label: "Start",
      onClick: () => console.log("Start clicked"),
      backgroundColor: "#2196F3",
      textColor: "white",
      icon: <VentionIcon size={32} type="player-play-filled" color="white" />,
      visible: true,
      disabled: false,
    },
    {
      id: "stop",
      label: "Stop",
      onClick: () => console.log("Stop clicked"),
      backgroundColor: "#F44336",
      textColor: "white",
      icon: <VentionIcon size={32} type="player-stop-filled" color="white" />,
      visible: false, // Hidden by default
      disabled: false,
    },
  ]

  return (
    <StatusTopBar
      status={{
        label: "Machine state: Ready",
        color: "#4CAF50",
      }}
      buttonConfigs={buttonConfigs}
    />
  )
}
Dynamic State Management

The component is fully controlled - all button states are derived from your application state:

import { StatusTopBar, ButtonConfig } from "@vention/machine-apps-components"
import { useMemo } from "react"

function MachineApp() {
  const { machineState, isRunning, canStart } = useMachineState()

  const buttonConfigs: ButtonConfig[] = useMemo(() => {
    const configs: ButtonConfig[] = []

    // Start button - visible when not running
    if (!isRunning) {
      configs.push({
        id: "start",
        label: "Start",
        onClick: handleStart,
        backgroundColor: "#2196F3",
        textColor: "white",
        visible: true,
        disabled: !canStart, // Disabled if prerequisites not met
      })
    }

    // Stop button - visible when running
    if (isRunning) {
      configs.push({
        id: "stop",
        label: "Stop",
        onClick: handleStop,
        backgroundColor: "#F44336",
        textColor: "white",
        visible: true,
        disabled: false,
      })
    }

    // Home button - always visible
    configs.push({
      id: "home",
      label: "Home",
      onClick: handleHome,
      borderColor: "#E2E8F0",
      textColor: "#1A202C",
      visible: true,
      disabled: isRunning, // Can't home while running
    })

    return configs
  }, [isRunning, canStart])

  return (
    <StatusTopBar
      status={{
        label: `Machine state: ${machineState}`,
        color: getStatusColor(machineState),
      }}
      buttonConfigs={buttonConfigs}
    />
  )
}
Conditional Buttons

Easily show/hide buttons based on application state:

const buttonConfigs: ButtonConfig[] = useMemo(() => {
  const configs: ButtonConfig[] = []

  // Operation page buttons
  if (currentPage === "operation") {
    configs.push({
      id: "start",
      label: isRunning ? "Stop" : "Start",
      onClick: isRunning ? handleStop : handleStart,
      backgroundColor: isRunning ? "#F44336" : "#2196F3",
      textColor: "white",
      visible: true,
      disabled: false,
    })
  }

  // Calibration page buttons
  if (currentPage === "calibration") {
    configs.push({
      id: "freedrive",
      label: isFreeDriveEnabled ? "Disable Free Drive" : "Enable Free Drive",
      onClick: toggleFreeDrive,
      backgroundColor: isFreeDriveEnabled ? "#CBD5E0" : undefined,
      borderColor: !isFreeDriveEnabled ? "#E2E8F0" : undefined,
      textColor: "black",
      visible: true,
      disabled: false,
    })
  }

  return configs
}, [currentPage, isRunning, isFreeDriveEnabled])

Migration from Imperative API

If you're migrating from the old compound component pattern:

Before (compound components):

<StatusTopBar statusLabel="Ready" dotColor="#4CAF50" visibleButtons={["start"]} disabledButtons={["stop"]}>
  <StatusTopBar.Button id="start" label="Start" onClick={handleStart} backgroundColor="#2196F3" textColor="white" />
  <StatusTopBar.Button id="stop" label="Stop" onClick={handleStop} backgroundColor="#F44336" textColor="white" />
</StatusTopBar>

After (config-based):

<StatusTopBar
  status={{ label: "Ready", color: "#4CAF50" }}
  buttonConfigs={[
    { id: "start", label: "Start", onClick: handleStart, backgroundColor: "#2196F3", textColor: "white", visible: true },
    { id: "stop", label: "Stop", onClick: handleStop, backgroundColor: "#F44336", textColor: "white", visible: false },
  ]}
/>

Benefits of the new API:

  • Single source of truth for button state
  • No need for refs or imperative methods
  • Easier to test (just pass different configs)
  • Better TypeScript support
  • More predictable behavior

Logs

Reusable logs components for filtering, sorting, and displaying machine logs.

Exports

import { LogsPanel, LogsTable, LogFilterForm, LogsPagination } from "@vention/machine-apps-components"
import type {
  LogEntry,
  LogType,
  LogFilterFormValues,
  SortOrder,
  LogsPanelHandle,
  PaginationConfig,
  PaginationMode,
  FetchParams,
  FetchResult,
} from "@vention/machine-apps-components"

Types

type LogType = "error" | "warning" | "info"

interface LogEntry {
  id: string
  date: string // ISO or locale string parsable by Date
  level: LogType
  code: string
  message: string
  description: string
}

type SortOrder = "latest" | "oldest"

interface LogFilterFormValues {
  fromDate?: string // YYYY-MM-DD
  toDate?: string // YYYY-MM-DD
  logType?: LogType
  sortOrder: SortOrder
}

type PaginationMode = "pagination" | "infinite-scroll" | "none"

interface PaginationConfig {
  mode: PaginationMode
  pageSize?: number // Default: 10
  initialPage?: number // Default: 1
}

// Parameters passed to the dataFetcher
interface FetchParams {
  filters: LogFilterFormValues // Current filter state
  page: number // Current page number
  pageSize: number // Items per page
}

// Expected return type from dataFetcher
interface FetchResult {
  logs: LogEntry[] // The log entries for this page
  totalCount: number // Total number of logs (after filtering)
  totalPages: number // Total number of pages
  currentPage: number // Current page number
  hasMore: boolean // Whether there are more pages available
}

// Type definition for data fetcher
type DataFetcher = (params: FetchParams) => Promise<FetchResult>

interface LogsPanelHandle {
  refresh: () => Promise<void>
  resetFilters: () => void
  applyFilters: (filters: Partial<LogFilterFormValues>) => void
  getCurrentFilters: () => LogFilterFormValues
  exportLogs: () => LogEntry[]
}

LogsPanel

A composite component that combines filter UI and table display with loading states and error handling. The component is a pure presentation component - it displays data and manages UI state, while delegating all data operations (filtering, sorting, pagination) to your dataFetcher function.

Features:

  • 🔄 Automatic loading state with spinner when fetching data
  • ❌ Built-in error display with VentionAlert
  • 🔍 Filter UI for date range and log type
  • ↕️ Sort UI for latest/oldest
  • 📅 Date formatting (YYYY-MM-DD h:mm:ssa)
  • 🎨 Type-based icons (error, warning, info)
  • 📄 Pagination support (standard pagination or infinite scroll)
  • 🎯 Imperative API for programmatic control
  • 🔔 Event callbacks for integration
  • 🎨 Customizable styling and empty states
  • ⚡ Performance optimized with React.memo
How It Works

The component calls your dataFetcher function whenever filters, sort order, or page changes. You implement the filtering, sorting, and pagination logic, and return the results:

import { LogsPanel } from "@vention/machine-apps-components"
import type { FetchParams, FetchResult } from "@vention/machine-apps-components"

const fetchLogs = async (params: FetchParams): Promise<FetchResult> => {
  // params contains:
  // - params.filters.fromDate
  // - params.filters.toDate
  // - params.filters.logType
  // - params.filters.sortOrder
  // - params.page
  // - params.pageSize

  const response = await fetch(
    `/api/logs?${new URLSearchParams({
      fromDate: params.filters.fromDate || "",
      toDate: params.filters.toDate || "",
      logType: params.filters.logType || "",
      sortOrder: params.filters.sortOrder,
      page: params.page.toString(),
      pageSize: params.pageSize.toString(),
    })}`
  )

  const data = await response.json()

  return {
    logs: data.logs,
    totalCount: data.total,
    totalPages: Math.ceil(data.total / params.pageSize),
    currentPage: params.page,
    hasMore: params.page < Math.ceil(data.total / params.pageSize),
  }
}

;<LogsPanel
  dataFetcher={fetchLogs}
  pagination={{
    mode: "pagination",
    pageSize: 20,
  }}
/>

Important: When using dataFetcher, you are responsible for:

  • ✅ Filtering logs by date range and type
  • ✅ Sorting logs by date (latest/oldest)
  • ✅ Paginating the results
  • ✅ Returning pagination metadata (totalCount, totalPages, hasMore)

The component handles:

  • ✅ Rendering the filter UI
  • ✅ Managing filter state
  • ✅ Calling your dataFetcher when filters/page changes
  • ✅ Loading and error states
  • ✅ Displaying the results
Complete Client-Side Example

Here's a complete example showing how to implement filtering, sorting, and pagination on the client-side:

import { useCallback, useMemo } from "react"
import { LogsPanel } from "@vention/machine-apps-components"
import type { FetchParams, FetchResult, LogEntry } from "@vention/machine-apps-components"
import dayjs from "dayjs"

function LogsPage() {
  // Your data source (could come from props, context, etc.)
  const allLogs: LogEntry[] = useMemo(
    () => [
      { id: "1", date: "2025-10-07T14:00:00Z", type: "error", code: "ERR_001", message: "Error", description: "..." },
      { id: "2", date: "2025-10-07T13:00:00Z", type: "warning", code: "WARN_001", message: "Warning", description: "..." },
      { id: "3", date: "2025-10-07T12:00:00Z", type: "info", code: "INFO_001", message: "Info", description: "..." },
      // ... more logs
    ],
    []
  )

  const fetchLogs = useCallback(
    async (params: FetchParams): Promise<FetchResult> => {
      // Simulate network delay (optional)
      await new Promise(resolve => setTimeout(resolve, 500))

      // 1. Apply filtering
      let filtered = [...allLogs]

      // Filter by date range
      if (params.filters.fromDate) {
        const fromTimestamp = dayjs(params.filters.fromDate).valueOf()
        filtered = filtered.filter(log => dayjs(log.date).valueOf() >= fromTimestamp)
      }

      if (params.filters.toDate) {
        const toTimestamp = dayjs(params.filters.toDate).endOf("day").valueOf()
        filtered = filtered.filter(log => dayjs(log.date).valueOf() <= toTimestamp)
      }

      // Filter by log type
      if (params.filters.logType) {
        filtered = filtered.filter(log => log.type === params.filters.logType)
      }

      // 2. Apply sorting
      filtered.sort((a, b) => {
        const aTime = dayjs(a.date).valueOf()
        const bTime = dayjs(b.date).valueOf()
        return params.filters.sortOrder === "latest" ? bTime - aTime : aTime - bTime
      })

      // 3. Apply pagination
      const totalCount = filtered.length
      const totalPages = Math.ceil(totalCount / params.pageSize)
      const start = (params.page - 1) * params.pageSize
      const end = start + params.pageSize
      const paginated = filtered.slice(start, end)

      // 4. Return result
      return {
        logs: paginated,
        totalCount,
        totalPages,
        currentPage: params.page,
        hasMore: end < totalCount,
      }
    },
    [allLogs]
  )

  return (
    <LogsPanel
      dataFetcher={fetchLogs}
      pagination={{
        mode: "pagination",
        pageSize: 10,
      }}
    />
  )
}
Props
interface LogsPanelProps {
  // Required - Data fetcher function
  dataFetcher: (params: FetchParams) => Promise<FetchResult>

  // Optional - Initial state
  initialFilters?: Partial<LogFilterFormValues>

  // Optional - Error handling
  onError?: (error: unknown) => void

  // Optional - Pagination
  pagination?: PaginationConfig

  // Optional - Event callbacks
  onFilterChange?: (filters: LogFilterFormValues) => void
  onLogClick?: (log: LogEntry) => void

  // Optional - Customization
  className?: string
  tableHeight?: string | number
  emptyStateMessage?: string
  emptyStateIcon?: ReactNode
}
States
  • Loading: Shows VentionSpinner with "Loading logs..." message
  • Error: Displays VentionAlert with error title and description
  • Empty: Shows customizable empty state message
  • Success: Renders filterable/sortable table with data
Initial Filters
<LogsPanel
  data={logs}
  initialFilters={{
    fromDate: "2025-09-01",
    toDate: "2025-09-30",
    logType: "error",
    sortOrder: "latest",
  }}
/>
Pagination
// Standard pagination (default: 10 items per page)
<LogsPanel
  data={logs}
  pagination={{
    mode: "pagination",
    pageSize: 20,
    initialPage: 1,
  }}
/>

// Infinite scroll (loads more as you scroll)
<LogsPanel
  data={logs}
  pagination={{
    mode: "infinite-scroll",
    pageSize: 10,
  }}
/>

// No pagination (show all logs)
<LogsPanel
  data={logs}
  pagination={{ mode: "none" }}
/>

// Default behavior (no pagination prop = show all logs)
<LogsPanel data={logs} />
Event Callbacks

Get notified when user interacts with the component:

import { LogsPanel } from "@vention/machine-apps-components"
import type { LogEntry, LogFilterFormValues } from "@vention/machine-apps-components"
;<LogsPanel
  data={logs}
  onFilterChange={(filters: LogFilterFormValues) => {
    console.log("User changed filters:", filters)
    // Track analytics, sync to URL params, etc.
  }}
  onLogClick={(log: LogEntry) => {
    console.log("User clicked log:", log)
    // Show details modal, navigate to details page, etc.
  }}
/>
Imperative API

Control the component programmatically using a ref:

import { useRef } from "react"
import { LogsPanel } from "@vention/machine-apps-components"
import type { LogsPanelHandle } from "@vention/machine-apps-components"

function MyComponent() {
  const logsPanelRef = useRef<LogsPanelHandle>(null)

  const handleRefresh = async () => {
    // Manually refresh async data
    await logsPanelRef.current?.refresh()
  }

  const handleResetFilters = () => {
    // Reset all filters to default
    logsPanelRef.current?.resetFilters()
  }

  const handleShowErrors = () => {
    // Programmatically apply filters
    logsPanelRef.current?.applyFilters({ logType: "error" })
  }

  const handleExport = () => {
    // Get current filtered/sorted logs
    const logs = logsPanelRef.current?.exportLogs()
    if (logs) {
      // Export to CSV, JSON, etc.
      downloadAsCSV(logs)
    }
  }

  const handleGetFilters = () => {
    // Get current filter state
    const filters = logsPanelRef.current?.getCurrentFilters()
    console.log("Current filters:", filters)
  }

  return (
    <>
      <div>
        <button onClick={handleRefresh}>Refresh</button>
        <button onClick={handleResetFilters}>Reset Filters</button>
        <button onClick={handleShowErrors}>Show Errors Only</button>
        <button onClick={handleExport}>Export Logs</button>
      </div>
      <LogsPanel ref={logsPanelRef} data={fetchLogs} />
    </>
  )
}

Available Methods:

  • refresh() - Manually re-fetch data with current filters/page
  • resetFilters() - Reset all filters to their default values
  • applyFilters(filters) - Programmatically set filters
  • getCurrentFilters() - Get the current filter state
  • exportLogs() - Get currently displayed logs array
Customization

Customize the appearance and behavior:

import { LogsPanel } from "@vention/machine-apps-components"
import { VentionIcon } from "@vention/machine-ui"
;<LogsPanel
  data={logs}
  className="my-custom-logs-panel"
  tableHeight="600px"
  emptyStateMessage="No logs match your filters"
  emptyStateIcon={<VentionIcon type="inbox" size={48} color="gray" />}
/>

Customization Props:

  • className - Add custom CSS class to the root container
  • tableHeight - Set custom table height (e.g., "500px", 600)
  • emptyStateMessage - Custom message when no logs (default: "You have no logs")
  • emptyStateIcon - Custom React node to display above empty message

LogsTable

Just the table component. Useful if you want to handle filtering/sorting yourself or compose your own custom layout with filters.

import { LogsTable } from "@vention/machine-apps-components"
import type { LogEntry } from "@vention/machine-apps-components"

<LogsTable 
  logs={logs} 
  onLogClick={log => showDetailsModal(log)} 
  tableHeight="500px" 
  emptyStateMessage="No logs available" 
/>

Props:

interface LogsTableProps {
  logs?: LogEntry[]
  isLoading?: boolean
  error?: string | null
  onLoadMoreLogs?: () => void // For infinite scroll
  hasMoreLogs?: boolean // For infinite scroll
  onLogClick?: (log: LogEntry) => void
  tableHeight?: string | number
  emptyStateMessage?: string
  emptyStateIcon?: ReactNode
}

LogFilterForm

Just the filter UI. Use this when you want full control over the filtering logic.

import { LogFilterForm } from "@vention/machine-apps-components"
import type { LogFilterFormValues } from "@vention/machine-apps-components"

const handleFilterChange = (filters: LogFilterFormValues) => {
  // Apply filters to your own data source
  const filtered = myLogs.filter(log => {
    // Your custom filter logic
  })
}

const handleReset = () => {
  // Handle reset
}

;<LogFilterForm onFilterChange={handleFilterChange} onReset={handleReset} initialFilters={{ logType: "error" }} />

LogsPagination

Just the pagination controls. Use this for custom pagination implementations.

import { LogsPagination } from "@vention/machine-apps-components"
;<LogsPagination currentPage={currentPage} totalPages={totalPages} onPageChange={page => setCurrentPage(page)} />

Development

Running Tests

# Run tests
nx test machine-apps-components

# Run tests with coverage
nx test machine-apps-components --coverage

# Run tests in watch mode
cd projects/machine-code/libs/machine-apps-components
npx vitest

Testing Setup

This library uses Vitest with jsdom environment for testing React components. Test utilities are provided in src/test-utils.tsx that wrap components with necessary providers:

  • ThemeProvider with machineUiTheme
  • MemoryRouter for routing

Example test:

import { renderWithProviders } from "../../test-utils"
import { NavigationBar } from "./navigation-bar"

it("should render correctly", () => {
  renderWithProviders(<NavigationBar navigationItems={mockNavigationItems} />)
  expect(screen.getByText("Operation")).toBeDefined()
})

Building

nx build machine-apps-components

Linting

nx lint machine-apps-components

版本与发布记录

@vention/machine-apps-components 共发布 285 个版本,首次发布于 2025/10/28;过去 12 个月发布了 279 个版本。

版本发布日期距今
1.30.27latest2026/11/03今天
1.30.262026/10/313 天前
1.30.252026/10/304 天前
1.30.242026/10/304 天前
1.30.232026/10/295 天前
1.30.222026/10/295 天前

其他发布通道:dev → 0.0.0-dev.4527.a3d343ca7。

关于 @vention/machine-apps-components 的常见问题

如何安装 @vention/machine-apps-components?

在项目目录运行 npm install @vention/machine-apps-components,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(17.0.2)、@mui/material(^5.0.0)、react-router-dom(^6.0.0)、@vention/machine-ui(^6.7.2)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@vention/machine-apps-components 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@vention/machine-apps-components 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@vention/machine-apps-components 可以商用吗?

@vention/machine-apps-components 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@vention/machine-apps-components 还在维护吗?

最新版本 1.30.27 发布于 2026/11/03(今天);至今共发布 285 个版本,其中过去 12 个月发布了 279 个;首次发布于 2025/10/28。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。