机器应用程序组件
用于机器应用程序的可重用组件。
组件
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与machineUiThemeMemoryRouter进行路由
示例测试:
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