@vention/machine-apps-components

中文文档
1.30.27发布于 2026/11/03MITTypeScriptES Module
中文使用文档
文档目录9 节
  1. 组件
  2. NavigationBar
  3. StatusTopBar
  4. 日志
  5. 开发
  6. 运行测试
  7. 测试设置
  8. 建筑
  9. 产品毛羽

机器应用程序组件

用于机器应用程序的可重用组件。

组件

NavigationBar

一个导航栏组件,支持:

  • 带有图标的多个导航项
  • 控制中心按钮
  • 支持按钮
  • 可选定时器显示

道具

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
}

示例使用

基本用法
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 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")
    },
  },
]

注意: 当一个习惯 onClick 处理程序,它将完全覆盖默认的导航行为。如果需要,处理程序负责任何导航逻辑。

StatusTopBar

位于屏幕顶部的状态栏组件,显示:

  • 带标签和彩色圆点的状态指示灯
  • 具有可见性和禁用状态的可配置操作按钮
  • 按钮的自定义样式

主要特点:

  • ✅声明式的,基于配置的API
  • ✅完全受控组件(无内部状态)
  • ✅类型安全按钮配置
  • ✅易于测试和推理

道具

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
}

示例使用

基本用法
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}
    />
  )
}
动态状态管理

组件是完全受控的——所有的按钮状态都来自于你的应用状态:

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}
    />
  )
}
有条件的按钮

根据应用程序状态轻松显示/隐藏按钮:

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])

从命令式API迁移

如果你正在从旧的复合组件模式迁移:

前(复合成分):

<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>

后(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 },
  ]}
/>

新API的好处:

  • 按钮状态的单一真实来源
  • 不需要refs或命令式方法
  • 更容易测试(只需通过不同的配置)
  • 更好的TypeScript支持
  • 更可预测的行为

日志

用于过滤、排序和显示机器日志的可重用日志组件。

出口

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"

类型

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

一个组合组件,它将过滤器UI和表显示与加载状态和错误处理结合在一起。这个分量是a 纯表示组件 -它显示数据并管理UI状态,同时将所有数据操作(过滤,排序,分页)委托给用户 dataFetcher 函数。

特点:

  • 🔄自动加载状态,获取数据时带有旋转器
  • ❌内置错误显示与VentionAlert
  • 🔍日期范围和日志类型的过滤器UI
  • ↕️排序UI为最新/最旧
  • 📅日期格式(YYYY-MM-DD h:mm:ssa)
  • 🎨基于类型的图标(错误、警告、信息)
  • 📄分页支持(标准分页或无限滚动)
  • 🎯编程控制的命令式API
  • 🔔用于集成的事件回调
  • 🎨可定制的样式和空状态
  • ⚡性能优化与React.memo
它是如何工作的

组件调用您的 dataFetcher 函数,每当过滤器、排序顺序或页面更改时。你实现了过滤、排序和分页逻辑,并返回结果:

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,
  }}
/>

重要: 当使用 dataFetcher,你有责任:

  • ✅按日期范围和类型过滤日志
  • ✅按日期(最新/最老)排序日志
  • ✅对结果进行分页
  • ✅返回分页元数据(totalCount, totalPages, hasMore)

组件处理:

  • ✅呈现过滤器UI
  • ✅过滤器状态管理
  • ✅给你打电话 dataFetcher 当过滤器/页面发生变化时
  • ✅加载和错误状态
  • ✅显示结果
完整的客户端示例

下面是一个完整的示例,展示了如何在客户端实现过滤、排序和分页:

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,
      }}
    />
  )
}
道具
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
}
州
  • 加载:显示带有“Loading logs…”消息的VentionSpinner
  • 错误:显示带有错误标题和描述的VentionAlert
  • 空:显示可自定义的空状态消息
  • 成功:呈现带有数据的可过滤/可排序表
最初的过滤器
<LogsPanel
  data={logs}
  initialFilters={{
    fromDate: "2025-09-01",
    toDate: "2025-09-30",
    logType: "error",
    sortOrder: "latest",
  }}
/>
分页
// 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} />
事件回调

当用户与组件交互时获得通知:

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.
  }}
/>
命令式的API

使用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} />
    </>
  )
}

可用的方法:

  • refresh() -手动重新获取当前过滤器/页面的数据
  • resetFilters() —将所有过滤器重置为默认值
  • applyFilters(filters) —可编程设置过滤器
  • getCurrentFilters() -获取当前过滤器状态
  • exportLogs() —获取当前显示的日志数组
定制

自定义外观和行为:

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" />}
/>

定制道具:

  • className —在根容器中添加自定义CSS类
  • tableHeight -设置自定义表格高度(例如,“500px”,600)
  • emptyStateMessage -当没有日志时自定义消息(默认:“你没有日志”)
  • emptyStateIcon -自定义React节点显示上述空消息

LogsTable

只有表组件。如果您想自己处理过滤/排序或使用过滤器组合自己的自定义布局,则非常有用。

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" 
/>

道具:

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

只有过滤器UI。当您想要完全控制过滤逻辑时,请使用此功能。

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

只有分页控件。将此用于自定义分页实现。

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

开发

运行测试

# 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

测试设置

这个库使用带有jdom环境的Vitest来测试React组件。中提供了测试实用程序 src/test-utils.tsx 用必要的提供程序包装组件:

  • ThemeProvider 与 machineUiTheme
  • MemoryRouter 进行路由

示例测试:

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

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

建筑

nx build machine-apps-components

产品毛羽

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 和提交记录判断。