feat: Sprint 2 — Stepper/InlineAlert组件 + 首页TaskCenter任务中心 + /workspace/next-actions API + Money.tsx BatchDetail集成工作流步骤条和质量门禁
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* InlineAlert 内联告警组件 — 用于表单内和工作流中展示提示、警告、错误
|
||||
* 支持多种语义类型和可关闭模式
|
||||
*/
|
||||
import { ReactNode, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { Info, AlertTriangle, XCircle, CheckCircle, X } from 'lucide-react'
|
||||
|
||||
type AlertType = 'info' | 'warning' | 'error' | 'success'
|
||||
|
||||
interface InlineAlertProps {
|
||||
type: AlertType
|
||||
title?: string
|
||||
children?: ReactNode
|
||||
closable?: boolean
|
||||
onClose?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const config: Record<AlertType, { icon: typeof Info; bg: string; text: string; border: string; iconColor: string }> = {
|
||||
info: { icon: Info, bg: 'bg-info/5', text: 'text-info', border: 'border-info/20', iconColor: 'text-info' },
|
||||
warning: { icon: AlertTriangle, bg: 'bg-warning/5', text: 'text-warning', border: 'border-warning/20', iconColor: 'text-warning' },
|
||||
error: { icon: XCircle, bg: 'bg-danger/5', text: 'text-danger', border: 'border-danger/20', iconColor: 'text-danger' },
|
||||
success: { icon: CheckCircle, bg: 'bg-success/5', text: 'text-success', border: 'border-success/20', iconColor: 'text-success' },
|
||||
}
|
||||
|
||||
/**
|
||||
* 内联告警 — 工作流中的质量门禁提示
|
||||
*/
|
||||
export function InlineAlert({ type, title, children, closable, onClose, className }: InlineAlertProps) {
|
||||
const [closed, setClosed] = useState(false)
|
||||
if (closed) return null
|
||||
|
||||
const c = config[type]
|
||||
const Icon = c.icon
|
||||
|
||||
return (
|
||||
<div className={clsx('flex items-start gap-2 rounded-md border px-3 py-2 text-sm', c.bg, c.border, c.text, className)}>
|
||||
<Icon className={clsx('w-4 h-4 mt-0.5 shrink-0', c.iconColor)} />
|
||||
<div className="flex-1 min-w-0">
|
||||
{title && <div className="font-medium">{title}</div>}
|
||||
{children && <div className="text-xs mt-0.5 opacity-90">{children}</div>}
|
||||
</div>
|
||||
{closable && (
|
||||
<button
|
||||
onClick={() => { setClosed(true); onClose?.() }}
|
||||
className="shrink-0 opacity-60 hover:opacity-100 transition-opacity"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Stepper 步骤条组件 — 用于多步骤工作流引导
|
||||
* 支持横向/纵向布局、可点击步骤导航、完成/当前/待办状态
|
||||
*/
|
||||
import { ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { Check } from 'lucide-react'
|
||||
|
||||
export interface Step {
|
||||
key: string
|
||||
title: string
|
||||
description?: string
|
||||
status: 'complete' | 'current' | 'pending' | 'error'
|
||||
}
|
||||
|
||||
interface StepperProps {
|
||||
steps: Step[]
|
||||
orientation?: 'horizontal' | 'vertical'
|
||||
onStepClick?: (key: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Stepper 步骤条 — 展示工作流进度和步骤导航
|
||||
*/
|
||||
export function Stepper({ steps, orientation = 'horizontal', onStepClick, className }: StepperProps) {
|
||||
const currentIndex = steps.findIndex(s => s.status === 'current')
|
||||
|
||||
if (orientation === 'vertical') {
|
||||
return (
|
||||
<div className={clsx('flex flex-col', className)}>
|
||||
{steps.map((step, i) => {
|
||||
const isLast = i === steps.length - 1
|
||||
const isClickable = onStepClick && (step.status === 'complete' || step.status === 'current')
|
||||
return (
|
||||
<div key={step.key} className="flex gap-3">
|
||||
{/* 左侧指示器 + 连接线 */}
|
||||
<div className="flex flex-col items-center">
|
||||
<button
|
||||
onClick={isClickable ? () => onStepClick?.(step.key) : undefined}
|
||||
disabled={!isClickable}
|
||||
className={clsx(
|
||||
'flex items-center justify-center w-8 h-8 rounded-full text-xs font-medium transition-colors shrink-0',
|
||||
step.status === 'complete' && 'bg-success text-white',
|
||||
step.status === 'current' && 'bg-brand-600 text-white ring-4 ring-brand-600/20',
|
||||
step.status === 'pending' && 'bg-surface-muted text-ink-400',
|
||||
step.status === 'error' && 'bg-danger text-white',
|
||||
isClickable && 'cursor-pointer hover:opacity-80',
|
||||
)}
|
||||
>
|
||||
{step.status === 'complete' ? <Check className="w-4 h-4" /> : i + 1}
|
||||
</button>
|
||||
{!isLast && (
|
||||
<div className={clsx('w-0.5 flex-1 min-h-[24px] my-1', step.status === 'complete' ? 'bg-success' : 'bg-border-subtle')} />
|
||||
)}
|
||||
</div>
|
||||
{/* 右侧内容 */}
|
||||
<div className={clsx('pb-4', isLast && 'pb-0')}>
|
||||
<div
|
||||
onClick={isClickable ? () => onStepClick?.(step.key) : undefined}
|
||||
className={clsx(
|
||||
'text-sm font-medium',
|
||||
step.status === 'current' && 'text-ink-900',
|
||||
step.status === 'complete' && 'text-ink-700',
|
||||
step.status === 'pending' && 'text-ink-400',
|
||||
step.status === 'error' && 'text-danger',
|
||||
isClickable && 'cursor-pointer hover:text-ink-900',
|
||||
)}
|
||||
>
|
||||
{step.title}
|
||||
</div>
|
||||
{step.description && (
|
||||
<div className="text-xs text-ink-500 mt-0.5">{step.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 横向布局
|
||||
return (
|
||||
<div className={clsx('flex items-center', className)}>
|
||||
{steps.map((step, i) => {
|
||||
const isLast = i === steps.length - 1
|
||||
const isClickable = onStepClick && (step.status === 'complete' || step.status === 'current')
|
||||
return (
|
||||
<div key={step.key} className="flex items-center flex-1 last:flex-none">
|
||||
{/* 圆点 + 标题 */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
onClick={isClickable ? () => onStepClick?.(step.key) : undefined}
|
||||
disabled={!isClickable}
|
||||
className={clsx(
|
||||
'flex items-center justify-center w-7 h-7 rounded-full text-xs font-medium transition-colors shrink-0',
|
||||
step.status === 'complete' && 'bg-success text-white',
|
||||
step.status === 'current' && 'bg-brand-600 text-white ring-4 ring-brand-600/20',
|
||||
step.status === 'pending' && 'bg-surface-muted text-ink-400',
|
||||
step.status === 'error' && 'bg-danger text-white',
|
||||
isClickable && 'cursor-pointer hover:opacity-80',
|
||||
)}
|
||||
>
|
||||
{step.status === 'complete' ? <Check className="w-3.5 h-3.5" /> : i + 1}
|
||||
</button>
|
||||
<div className="hidden sm:block">
|
||||
<div
|
||||
onClick={isClickable ? () => onStepClick?.(step.key) : undefined}
|
||||
className={clsx(
|
||||
'text-sm font-medium whitespace-nowrap',
|
||||
step.status === 'current' && 'text-ink-900',
|
||||
step.status === 'complete' && 'text-ink-700',
|
||||
step.status === 'pending' && 'text-ink-400',
|
||||
step.status === 'error' && 'text-danger',
|
||||
isClickable && 'cursor-pointer hover:text-ink-900',
|
||||
)}
|
||||
>
|
||||
{step.title}
|
||||
</div>
|
||||
{step.description && (
|
||||
<div className="text-xs text-ink-500 whitespace-nowrap">{step.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* 连接线 */}
|
||||
{!isLast && (
|
||||
<div className={clsx('h-0.5 flex-1 mx-2', step.status === 'complete' ? 'bg-success' : 'bg-border-subtle')} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user