2025-06-14 16:53:18 +08:00
import { useEffect , useState , useMemo } from 'react' ;
import { bitable , FieldType , IDateTimeFieldMeta , IDateTimeField , ITable , ITextField , ISelectionField , INumberField , IOpenSegment , IRecord } from '@lark-base-open/js-sdk' ;
import { Steps , Button , Typography , Form , Toast , Spin , Empty } from '@douyinfe/semi-ui' ;
2024-07-10 12:55:22 +00:00
import './App.css' ;
2025-06-14 16:53:18 +08:00
// 步骤数据结构定义
interface StepItem {
name : string ;
fieldId : string ; // 原始的fieldId, 代表流程名称本身
finished : boolean ;
parallelSteps? : StepItem [ ] ; // 并行子步骤数组
requireAllSubSteps? : boolean ; // 是否所有子步骤都完成才标记主步骤完成
expectTimeFieldId? : string ;
expectTimeValue? : number ;
actualTimeFieldId? : string ;
actualTimeValue? : number ;
startTimeFieldId? : string ;
startTimeValue? : number ;
processNodeValue? : string ;
order? : number ; // 流程顺序,相同顺序的步骤视为并行
parallelGroup? : number ; // 并行组标识, 用于UI分组显示
isMainStep? : boolean ; // 是否为主步骤(整数顺序)
}
// 流程配置项结构
interface ProcessConfigItem {
processName : string ;
order : number ;
processNodeValue : string ;
timeFieldId : string ;
originalRecordId : string ;
posLink : string ;
}
// 常量定义
const POS_TABLE_ID = 'tblubooNX1JQKg4l' ;
const PROCESS_TABLE_ID = 'tbl9Uh2nqhIBvQTY' ;
const POS_LINK_FIELD_ID_IN_PROCESS_TABLE = 'fldoG0YGGB' ;
const PROCESS_NAME_FIELD_NAME = '流程名称' ;
const PROCESS_ORDER_FIELD_NAME = '流程顺序' ;
const PROCESS_NODE_FIELD_ID = 'fld7uf58lq' ;
const FIELD_ID_FIELD_ID = 'fldOqrKaDz' ;
const PROCESS_NODE_EXPECT_TIME = '预计完成时间' ;
const PROCESS_NODE_ACTUAL_TIME = '实际完成时间' ;
const PROCESS_NODE_START_TIME = '实际开始时间' ;
// 格式化时间戳为可读格式
const formatTimestamp = ( timestamp : number | undefined ) : string = > {
if ( timestamp === undefined || timestamp === null ) return '-' ;
return new Date ( timestamp ) . toLocaleString ( 'zh-CN' , { year : 'numeric' , month : '2-digit' , day : '2-digit' , hour : '2-digit' , minute : '2-digit' } ) ;
} ;
// 步骤管理服务类
class StepService {
// 保存步骤完成时间
static async saveStepCompletionTime (
posTable : ITable ,
selectedRecordId : string ,
stepItem : StepItem ,
isSubStep : boolean = false
) : Promise < number | null > {
if ( ! selectedRecordId || ! stepItem . actualTimeFieldId ) return null ;
try {
const timeField = await posTable . getFieldById < IDateTimeField > ( stepItem . actualTimeFieldId ) ;
const now = Date . now ( ) ;
await timeField . setValue ( selectedRecordId , now ) ;
stepItem . actualTimeValue = now ;
stepItem . finished = true ;
console . log ( ` ${ isSubStep ? '子步骤' : '步骤' } " ${ stepItem . name } " 完成时间已保存到字段 ${ stepItem . actualTimeFieldId } ` ) ;
return now ;
} catch ( e ) {
console . error ( ` Error saving ${ isSubStep ? 'sub-step' : 'step' } completion time: ` , e ) ;
throw e ;
}
}
// 更新后续步骤的开始时间
static async updateSubsequentStepsStartTimes (
posTable : ITable ,
selectedRecordId : string ,
steps : StepItem [ ] ,
stepIndex : number ,
actualTimeValue : number
) {
if ( ! selectedRecordId ) return ;
try {
// 从当前步骤的下一个步骤开始更新
for ( let i = stepIndex + 1 ; i < steps . length ; i ++ ) {
const nextStep = steps [ i ] ;
// 如果下一步骤已有开始时间,且早于当前完成时间,则不更新
if ( nextStep . startTimeValue && nextStep . startTimeValue <= actualTimeValue ) {
continue ;
}
// 如果下一步骤有开始时间字段ID, 更新该字段
if ( nextStep . startTimeFieldId ) {
const startTimeField = await posTable . getFieldById < IDateTimeField > ( nextStep . startTimeFieldId ) ;
await startTimeField . setValue ( selectedRecordId , actualTimeValue ) ;
nextStep . startTimeValue = actualTimeValue ;
console . log ( ` Updated step ${ i } ( ${ nextStep . name } ) start time to ${ new Date ( actualTimeValue ) . toLocaleString ( ) } ` ) ;
}
// 如果下一步骤有并行子步骤,也更新它们的开始时间
if ( nextStep . parallelSteps ) {
for ( const subStep of nextStep . parallelSteps ) {
if ( subStep . startTimeFieldId ) {
const subStartTimeField = await posTable . getFieldById < IDateTimeField > ( subStep . startTimeFieldId ) ;
await subStartTimeField . setValue ( selectedRecordId , actualTimeValue ) ;
subStep . startTimeValue = actualTimeValue ;
console . log ( ` Updated sub-step ${ subStep . name } start time to ${ new Date ( actualTimeValue ) . toLocaleString ( ) } ` ) ;
}
}
}
}
} catch ( e ) {
console . error ( "Error updating subsequent steps start times:" , e ) ;
Toast . warning ( "更新后续步骤开始时间失败" ) ;
}
}
// 处理子步骤完成的逻辑
static async handleSubStepCompletion (
newSteps : StepItem [ ] ,
stepIndex : number ,
subStepIndex : number ,
posTable : ITable ,
selectedRecordId : string ,
refreshSteps : ( value : boolean ) = > void
) {
const parentStep = newSteps [ stepIndex ] ;
const subStep = parentStep . parallelSteps ! [ subStepIndex ] ;
// 如果父步骤要求所有子步骤完成才标记为完成
if ( parentStep . requireAllSubSteps && parentStep . parallelSteps ? . every ( ps = > ps . finished ) ) {
parentStep . finished = true ;
// 如果父步骤也有完成时间字段,记录完成时间
if ( parentStep . actualTimeFieldId ) {
try {
const completionTime = await StepService . saveStepCompletionTime ( posTable , selectedRecordId , parentStep ) ;
if ( completionTime ) {
// 更新父步骤后的步骤开始时间
await StepService . updateSubsequentStepsStartTimes (
posTable , selectedRecordId , newSteps , stepIndex , completionTime
) ;
// 强制刷新步骤数据
refreshSteps ( true ) ;
}
} catch ( e ) {
console . error ( "Error saving parent step completion time:" , e ) ;
}
} else {
// 如果没有完成时间字段,也需要更新后续步骤
refreshSteps ( true ) ;
}
} else if ( ! parentStep . requireAllSubSteps && parentStep . parallelSteps ? . some ( ps = > ps . finished ) ) {
// 任一子步骤完成即可推进主步骤
parentStep . finished = true ;
// 如果父步骤有完成时间字段,记录完成时间
if ( parentStep . actualTimeFieldId ) {
try {
const completionTime = await StepService . saveStepCompletionTime ( posTable , selectedRecordId , parentStep ) ;
if ( completionTime ) {
// 更新父步骤后的步骤开始时间
await StepService . updateSubsequentStepsStartTimes (
posTable , selectedRecordId , newSteps , stepIndex , completionTime
) ;
// 强制刷新步骤数据
refreshSteps ( true ) ;
}
} catch ( e ) {
console . error ( "Error saving parent step completion time:" , e ) ;
}
} else {
// 如果没有完成时间字段,也需要更新后续步骤
refreshSteps ( true ) ;
}
}
}
// 查找上一个主步骤的完成时间
static findPreviousMainStepFinishTime ( steps : StepItem [ ] , currentIndex : number ) : number | undefined {
for ( let i = currentIndex - 1 ; i >= 0 ; i -- ) {
const step = steps [ i ] ;
// 如果是主步骤(整数顺序)且已完成
if ( step . isMainStep && step . finished && step . actualTimeValue ) {
return step . actualTimeValue ;
}
}
return undefined ;
}
// 更新步骤的开始时间为上一个主步骤的完成时间
static async updateStepStartTimeFromPreviousMainStep (
posTable : ITable ,
selectedRecordId : string ,
steps : StepItem [ ] ,
stepIndex : number
) {
if ( ! selectedRecordId ) return ;
const currentStep = steps [ stepIndex ] ;
if ( ! currentStep . startTimeFieldId ) return ;
try {
// 查找上一个主步骤的完成时间
const previousFinishTime = StepService . findPreviousMainStepFinishTime ( steps , stepIndex ) ;
if ( previousFinishTime ) {
// 更新当前步骤的开始时间
const startTimeField = await posTable . getFieldById < IDateTimeField > ( currentStep . startTimeFieldId ) ;
await startTimeField . setValue ( selectedRecordId , previousFinishTime ) ;
currentStep . startTimeValue = previousFinishTime ;
console . log ( ` Updated step ${ stepIndex } ( ${ currentStep . name } ) start time to previous main step's finish time: ${ new Date ( previousFinishTime ) . toLocaleString ( ) } ` ) ;
// 如果当前步骤有并行子步骤,也更新它们的开始时间
if ( currentStep . parallelSteps ) {
for ( const subStep of currentStep . parallelSteps ) {
if ( subStep . startTimeFieldId ) {
const subStartTimeField = await posTable . getFieldById < IDateTimeField > ( subStep . startTimeFieldId ) ;
await subStartTimeField . setValue ( selectedRecordId , previousFinishTime ) ;
subStep . startTimeValue = previousFinishTime ;
console . log ( ` Updated sub-step ${ subStep . name } start time to previous main step's finish time: ${ new Date ( previousFinishTime ) . toLocaleString ( ) } ` ) ;
}
}
}
}
} catch ( e ) {
console . error ( ` Error updating step ${ stepIndex } start time from previous main step: ` , e ) ;
Toast . warning ( "更新步骤开始时间失败" ) ;
}
}
}
// 判断并行步骤是否完成
const isParallelFinished = ( parallelSteps : StepItem [ ] , requireAll : boolean = true ) = > {
if ( ! parallelSteps || parallelSteps . length === 0 ) return true ;
if ( requireAll ) {
return parallelSteps . every ( step = > step . finished ) ;
} else {
return parallelSteps . some ( step = > step . finished ) ;
}
} ;
// 步骤UI组件
const StepCard = ( {
step ,
idx ,
currentStep ,
loading ,
onFinishStep ,
formatTimestamp ,
isCurrentStep ,
isActiveParallelGroup ,
isAnySubStepFinished , // 判断并行组中是否有子步骤已完成
canAccessSubSteps // 判断是否可以访问子步骤
} : {
step : StepItem ;
idx : number ;
currentStep : number ;
loading : boolean ;
onFinishStep : ( stepIndex : number , subStepIndex? : number ) = > void ;
formatTimestamp : ( timestamp : number | undefined ) = > string ;
isCurrentStep : boolean ;
isActiveParallelGroup : boolean ;
isAnySubStepFinished : boolean ;
canAccessSubSteps : boolean ;
} ) = > {
return (
< div className = "step-content-container" style = { { minWidth : 300 , marginTop : step.parallelSteps ? 10 : 0 } } >
{ /* 并行步骤组标题 */ }
{ step . parallelSteps && (
< div className = "parallel-group-header" style = { {
padding : '10px 16px' ,
backgroundColor : '#f5f7fa' ,
borderRadius : '4px' ,
marginBottom : '16px' ,
borderLeft : '4px solid #165DFF'
} } >
< Typography.Text strong style = { { fontSize : 16 , color : '#165DFF' } } >
并 行 处 理 组 : { step . name }
< / Typography.Text >
< Typography.Text style = { { fontSize : 14 , color : '#86909c' , marginTop : 4 , display : 'block' } } >
{ step . requireAllSubSteps ? '所有子步骤完成后自动推进' : '任一子步骤完成后自动推进' }
< / Typography.Text >
< / div >
) }
{ /* 普通步骤的时间信息展示 */ }
{ ! step . parallelSteps && (
< div className = "step-time-info" style = { {
margin : '16px 0 8px 0' ,
padding : '12px' ,
border : '1px solid #eee' ,
borderRadius : '4px'
} } >
< Typography.Text strong style = { { color : '#000' , fontSize : 14 , marginBottom : 8 , display : 'block' } } >
预 计 完 成 时 间 : { formatTimestamp ( step . expectTimeValue ) }
< / Typography.Text >
< Typography.Text strong style = { { color : '#000' , fontSize : 14 , marginBottom : 8 , display : 'block' } } >
实 际 开 始 时 间 : { formatTimestamp ( step . startTimeValue ) }
< / Typography.Text >
< Typography.Text strong style = { { color : '#000' , fontSize : 14 , marginBottom : 8 , display : 'block' } } >
实 际 完 成 时 间 : { formatTimestamp ( step . actualTimeValue ) }
< / Typography.Text >
< Typography.Text strong style = { {
color : step.actualTimeValue && step . expectTimeValue && step . actualTimeValue > step . expectTimeValue ? '#f5222d' :
( step . actualTimeValue && step . expectTimeValue && step . actualTimeValue <= step . expectTimeValue ? '#52c41a' : '#000' ) ,
fontSize : 14 , marginBottom : 0 , display : 'block'
} } >
耗 时 : { step . startTimeValue && step . actualTimeValue ?
( ( step . actualTimeValue - step . startTimeValue ) > 0 ?
( ( step . actualTimeValue - step . startTimeValue ) / 1000 / 60 / 60 ) . toFixed ( 2 ) + ' 小时' : '0 小时' ) : '-' }
< / Typography.Text >
< / div >
) }
{ /* 并行子步骤 */ }
{ step . parallelSteps && (
< div className = "parallel-steps-container" style = { {
display : 'flex' ,
flexDirection : 'row' ,
gap : 16 ,
marginTop : 10 ,
flexWrap : 'wrap'
} } >
{ step . parallelSteps . map ( ( sub , subIdx ) = > (
< div
key = { subIdx }
className = "sub-step-card"
style = { {
flex : 1 ,
minWidth : 280 ,
border : '1px solid #eee' ,
borderRadius : 4 ,
padding : 12 ,
backgroundColor : '#fafafa' ,
transition : 'all 0.3s' ,
position : 'relative'
} }
>
{ /* 完成状态标识 */ }
{ sub . finished && (
< div className = "sub-step-finished-badge" style = { {
position : 'absolute' ,
top : '8px' ,
right : '8px' ,
backgroundColor : '#52c41a' ,
color : '#fff' ,
fontSize : '12px' ,
padding : '2px 6px' ,
borderRadius : '4px'
} } >
已 完 成
< / div >
) }
< Typography.Text strong className = "sub-step-title" style = { { fontSize : 15 } } > { sub . name } < / Typography.Text >
< div className = "sub-step-time-info" style = { { margin : '10px 0 8px 0' } } >
< Typography.Text strong style = { { color : '#000' , fontSize : 13 , marginBottom : 8 , display : 'block' } } >
预 计 完 成 时 间 : { formatTimestamp ( sub . expectTimeValue || step . expectTimeValue ) }
< / Typography.Text >
< Typography.Text strong style = { { color : '#000' , fontSize : 13 , marginBottom : 8 , display : 'block' } } >
实 际 开 始 时 间 : { formatTimestamp ( sub . startTimeValue || step . startTimeValue ) }
< / Typography.Text >
< Typography.Text strong style = { { color : '#000' , fontSize : 13 , marginBottom : 8 , display : 'block' } } >
实 际 完 成 时 间 : { formatTimestamp ( sub . actualTimeValue ) }
< / Typography.Text >
< Typography.Text strong style = { {
color : sub.actualTimeValue && ( sub . expectTimeValue || step . expectTimeValue ) &&
sub . actualTimeValue > ( sub . expectTimeValue || step . expectTimeValue ) ? '#f5222d' :
( sub . actualTimeValue && ( sub . expectTimeValue || step . expectTimeValue ) &&
sub . actualTimeValue <= ( sub . expectTimeValue || step . expectTimeValue ) ? '#52c41a' : '#000' ) ,
fontSize : 13 , marginBottom : 0 , display : 'block'
} } >
耗 时 : { sub . startTimeValue && sub . actualTimeValue ?
( ( sub . actualTimeValue - sub . startTimeValue ) > 0 ?
( ( sub . actualTimeValue - sub . startTimeValue ) / 1000 / 60 / 60 ) . toFixed ( 2 ) + ' 小时' : '0 小时' ) :
( step . startTimeValue && sub . actualTimeValue ?
( ( sub . actualTimeValue - step . startTimeValue ) / 1000 / 60 / 60 ) . toFixed ( 2 ) + ' 小时' : '-' ) }
< / Typography.Text >
< / div >
{ /* 允许活动并行组中的子步骤被点击,或并行组中已有子步骤完成 */ }
{ ! sub . finished && ( isCurrentStep || isActiveParallelGroup || isAnySubStepFinished || canAccessSubSteps ) && (
< Button
theme = "solid"
size = "small"
loading = { loading }
onClick = { ( ) = > onFinishStep ( idx , subIdx ) }
style = { { marginTop : 8 } }
>
完 成 此 子 步 骤
< / Button >
) }
{ /* 只有当父步骤未完成且不在当前活动状态时才显示等待提示 */ }
{ ! sub . finished && ! ( isCurrentStep || isActiveParallelGroup || isAnySubStepFinished || canAccessSubSteps ) && (
< div style = { { marginTop : 8 , padding : '6px 12px' , backgroundColor : '#f2f3f5' , borderRadius : 4 , textAlign : 'center' } } >
< Typography.Text style = { { fontSize : 13 , color : '#86909c' } } >
等 待 前 置 步 骤 完 成
< / Typography.Text >
< / div >
) }
< / div >
) ) }
< / div >
) }
{ /* 当前步骤的完成按钮 */ }
{ idx === currentStep && ! step . finished && ! step . parallelSteps && (
< Button
theme = "solid"
size = "default"
loading = { loading }
onClick = { ( ) = > onFinishStep ( idx ) }
style = { { marginTop : 16 } }
>
完 成 此 步 骤
< / Button >
) }
< / div >
) ;
} ;
2024-07-10 12:55:22 +00:00
export default function App() {
2025-06-14 16:53:18 +08:00
const [ dateFields , setDateFields ] = useState < IDateTimeFieldMeta [ ] > ( [ ] ) ;
const [ steps , setSteps ] = useState < StepItem [ ] > ( [ ] ) ;
const [ currentStep , setCurrentStep ] = useState ( 0 ) ;
const [ loading , setLoading ] = useState ( false ) ;
const [ selectedRecordId , setSelectedRecordId ] = useState < string | undefined > ( ) ;
const [ refreshSteps , setRefreshSteps ] = useState < boolean > ( false ) ;
const [ isChangingRecord , setIsChangingRecord ] = useState ( false ) ; // 记录是否正在切换记录
const [ hasNoProcess , setHasNoProcess ] = useState ( false ) ; // 记录是否没有配置流程
// 缓存所有流程配置数据
const [ allProcessConfigs , setAllProcessConfigs ] = useState < ProcessConfigItem [ ] > ( [ ] ) ;
const [ processConfigLoading , setProcessConfigLoading ] = useState ( true ) ;
// 缓存字段元数据,避免重复获取
const [ fieldMetadataCache , setFieldMetadataCache ] = useState < Map < string , any > > ( new Map ( ) ) ;
// 缓存按POS记录ID分组的配置
const [ configsByPosLink , setConfigsByPosLink ] = useState < Map < string , ProcessConfigItem [ ] > > ( new Map ( ) ) ;
// 预加载所有流程配置数据
async function fetchAllProcessConfigs() {
setProcessConfigLoading ( true ) ;
try {
// 检查缓存中是否已有配置数据
if ( allProcessConfigs . length > 0 ) {
console . log ( 'Using cached process configs' ) ;
return ;
}
const processTable = await bitable . base . getTableById ( PROCESS_TABLE_ID ) ;
// 优化: 批量获取所有字段元数据, 减少API调用
const allFields = await processTable . getFieldMetaList ( ) ;
const fieldMap = new Map ( allFields . map ( field = > [ field . id , field ] ) ) ;
setFieldMetadataCache ( fieldMap ) ;
// 验证字段是否存在 - 使用优化后的字段映射
const requiredFieldIds = [
POS_LINK_FIELD_ID_IN_PROCESS_TABLE ,
PROCESS_NODE_FIELD_ID ,
FIELD_ID_FIELD_ID
] ;
const requiredFieldNames = [ PROCESS_NAME_FIELD_NAME , PROCESS_ORDER_FIELD_NAME ] ;
// 检查必需字段是否存在
const missingFields : string [ ] = [ ] ;
requiredFieldIds . forEach ( id = > {
if ( ! fieldMap . has ( id ) ) {
missingFields . push ( ` 字段ID: ${ id } ` ) ;
}
2024-07-10 12:55:22 +00:00
} ) ;
2025-06-14 16:53:18 +08:00
requiredFieldNames . forEach ( name = > {
const field = allFields . find ( f = > f . name === name ) ;
if ( ! field ) {
missingFields . push ( ` 字段名: ${ name } ` ) ;
}
} ) ;
if ( missingFields . length > 0 ) {
const errorMsg = ` 订单流程表中缺少关键字段: ${ missingFields . join ( ', ' ) } ` ;
console . error ( errorMsg ) ;
Toast . error ( errorMsg ) ;
setProcessConfigLoading ( false ) ;
return ;
}
// 使用更高效的分页获取记录
const allRecords : IRecord [ ] = [ ] ;
let pageToken : string | undefined ;
do {
const response = await processTable . getRecords ( {
pageSize : 5000 , // 最大页大小
pageToken
} ) ;
allRecords . push ( . . . response . records ) ;
pageToken = response . pageToken ;
} while ( pageToken ) ;
console . log ( ` Fetched ${ allRecords . length } process config records ` ) ;
const allConfigs : ProcessConfigItem [ ] = [ ] ;
// 优化:使用高效的字段访问方式处理记录
for ( const record of allRecords ) {
const fields = record . fields ;
const recordId = record . recordId ;
// 处理POS链接字段 - 优化文本提取逻辑
let posLinkText = '' ;
const posLinkFieldValue = fields [ POS_LINK_FIELD_ID_IN_PROCESS_TABLE ] ;
if ( Array . isArray ( posLinkFieldValue ) ) {
posLinkText = ( posLinkFieldValue [ 0 ] as IOpenSegment ) ? . text ? . trim ( ) || '' ;
} else if ( typeof posLinkFieldValue === 'object' && posLinkFieldValue ) {
posLinkText = ( posLinkFieldValue as IOpenSegment ) . text ? . trim ( ) || '' ;
}
// 处理流程名称字段 - 使用字段ID直接访问
const processNameField = allFields . find ( f = > f . name === PROCESS_NAME_FIELD_NAME ) ;
let processName = '' ;
if ( processNameField ) {
const fieldValue = fields [ processNameField . id ] ;
processName = Array . isArray ( fieldValue )
? ( fieldValue [ 0 ] as IOpenSegment ) ? . text ? . trim ( ) || ''
: ( fieldValue as IOpenSegment ) ? . text ? . trim ( ) || '' ;
}
// 处理顺序字段
const processOrderField = allFields . find ( f = > f . name === PROCESS_ORDER_FIELD_NAME ) ;
const order = processOrderField
? ( fields [ processOrderField . id ] as number | undefined ) || 0
: 0 ;
// 处理流程节点字段
const processNodeValue = fields [ PROCESS_NODE_FIELD_ID ]
? ( Array . isArray ( fields [ PROCESS_NODE_FIELD_ID ] )
? ( fields [ PROCESS_NODE_FIELD_ID ] [ 0 ] as IOpenSegment ) ? . text ? . trim ( ) || ''
: ( fields [ PROCESS_NODE_FIELD_ID ] as IOpenSegment ) ? . text ? . trim ( ) || '' )
: '' ;
// 处理时间字段ID字段
const timeFieldId = fields [ FIELD_ID_FIELD_ID ]
? ( Array . isArray ( fields [ FIELD_ID_FIELD_ID ] )
? ( fields [ FIELD_ID_FIELD_ID ] [ 0 ] as IOpenSegment ) ? . text ? . trim ( ) || ''
: ( fields [ FIELD_ID_FIELD_ID ] as IOpenSegment ) ? . text ? . trim ( ) || '' )
: '' ;
allConfigs . push ( {
processName ,
order ,
processNodeValue ,
timeFieldId ,
originalRecordId : recordId ,
posLink : posLinkText
} ) ;
}
// 按POS记录ID分组配置, 便于快速查找
const configsByPosMap = new Map < string , ProcessConfigItem [ ] > ( ) ;
allConfigs . forEach ( config = > {
if ( ! configsByPosMap . has ( config . posLink ) ) {
configsByPosMap . set ( config . posLink , [ ] ) ;
}
configsByPosMap . get ( config . posLink ) ? . push ( config ) ;
} ) ;
setAllProcessConfigs ( allConfigs ) ;
setConfigsByPosLink ( configsByPosMap ) ;
console . log ( 'Fetched all process configs:' , allConfigs . length , 'items' ) ;
} catch ( error ) {
console . error ( 'Failed to fetch all process configs:' , error ) ;
Toast . error ( '加载流程配置数据失败,请检查网络连接或表格权限' ) ;
} finally {
setProcessConfigLoading ( false ) ;
2024-07-10 12:55:22 +00:00
}
2025-06-14 16:53:18 +08:00
}
useEffect ( ( ) = > {
fetchAllProcessConfigs ( ) ;
2024-07-10 12:55:22 +00:00
} , [ ] ) ;
2025-06-14 16:53:18 +08:00
// 初始化日期字段
2024-07-10 12:55:22 +00:00
useEffect ( ( ) = > {
2025-06-14 16:53:18 +08:00
async function fetchDateFieldsForUI() {
try {
const activeTable = await bitable . base . getActiveTable ( ) ;
const fields = await activeTable . getFieldMetaListByType < IDateTimeFieldMeta > ( FieldType . DateTime ) ;
setDateFields ( fields ) ;
} catch ( e ) {
console . error ( "Failed to fetch date fields for UI: " , e ) ;
Toast . error ( "获取可选日期字段列表失败" ) ;
}
}
fetchDateFieldsForUI ( ) ;
2024-07-10 12:55:22 +00:00
} , [ ] ) ;
2025-06-14 16:53:18 +08:00
// 优化:初始化步骤时处理并行组
const initializeStepsWithParallel = ( configs : ProcessConfigItem [ ] ) = > {
if ( configs . length === 0 ) return [ ] ;
// 使用Map按order分组, 提高查询效率
const stepsByOrder = new Map < number , StepItem [ ] > ( ) ;
// 初始化步骤
configs . forEach ( config = > {
const step : StepItem = {
name : config.processName ,
fieldId : config.timeFieldId ,
finished : false ,
order : config.order ,
processNodeValue : config.processNodeValue ,
isMainStep : config.order === Math . floor ( config . order ) // 标记是否为主步骤(整数顺序)
} ;
if ( ! stepsByOrder . has ( config . order ) ) {
stepsByOrder . set ( config . order , [ ] ) ;
}
stepsByOrder . get ( config . order ) ? . push ( step ) ;
} ) ;
// 处理并行组
const processedSteps : StepItem [ ] = [ ] ;
stepsByOrder . forEach ( ( groupSteps , order ) = > {
if ( groupSteps . length === 1 ) {
processedSteps . push ( groupSteps [ 0 ] ) ;
} else {
const parallelGroupStep : StepItem = {
name : ` 并行组 ${ order } ` ,
fieldId : ` parallel_group_ ${ order } ` ,
finished : false ,
order ,
parallelSteps : groupSteps ,
requireAllSubSteps : true ,
parallelGroup : order ,
isMainStep : true // 并行组作为一个整体视为主步骤
} ;
// 继承第一个子步骤的时间字段
if ( groupSteps . length > 0 ) {
const firstSubStep = groupSteps [ 0 ] ;
parallelGroupStep . expectTimeFieldId = firstSubStep . expectTimeFieldId ;
parallelGroupStep . expectTimeValue = firstSubStep . expectTimeValue ;
parallelGroupStep . startTimeFieldId = firstSubStep . startTimeFieldId ;
parallelGroupStep . startTimeValue = firstSubStep . startTimeValue ;
}
processedSteps . push ( parallelGroupStep ) ;
}
} ) ;
return processedSteps . sort ( ( a , b ) = > ( a . order || 0 ) - ( b . order || 0 ) ) ;
} ;
// 刷新步骤数据
const refreshProcessSteps = async ( ) = > {
if ( selectedRecordId ) {
await loadProcessSteps ( selectedRecordId ) ;
}
} ;
// 从缓存中加载流程步骤
const loadProcessSteps = async ( currentPosRecordId : string ) = > {
console . log ( 'loadProcessSteps triggered with currentPosRecordId:' , currentPosRecordId ) ;
if ( ! currentPosRecordId ) {
setSteps ( [ ] ) ;
setCurrentStep ( 0 ) ;
setHasNoProcess ( false ) ;
console . log ( 'No POS record selected, steps cleared.' ) ;
return ;
}
setLoading ( true ) ;
setIsChangingRecord ( true ) ;
setHasNoProcess ( false ) ; // 重置状态
console . log ( 'Loading process steps from cached configs...' ) ;
try {
const posTable = await bitable . base . getTableById ( POS_TABLE_ID ) ;
// 从缓存中快速筛选当前记录的配置
const filteredConfigs = configsByPosLink . get ( currentPosRecordId ) ||
allProcessConfigs . filter ( config = > config . posLink === currentPosRecordId ) ;
console . log ( 'Filtered configs for current record:' , filteredConfigs . length ) ;
if ( filteredConfigs . length === 0 ) {
console . log ( 'No configs found for current record' ) ;
setSteps ( [ ] ) ;
setCurrentStep ( 0 ) ;
setHasNoProcess ( true ) ; // 设置为没有配置流程
setLoading ( false ) ;
setIsChangingRecord ( false ) ;
return ;
}
// 按流程顺序分组配置
const stepsByOrder = new Map < number , StepItem [ ] > ( ) ;
const tempStepsMap = new Map < string , StepItem > ( ) ;
// 首先处理每个步骤的基本信息
for ( const config of filteredConfigs ) {
const stepName = config . processName ;
const order = config . order ;
// 如果该步骤不存在,创建新步骤
if ( ! tempStepsMap . has ( stepName ) ) {
tempStepsMap . set ( stepName , {
name : stepName ,
fieldId : stepName ,
finished : false ,
order ,
parallelSteps : undefined ,
requireAllSubSteps : true , // 默认需要所有子步骤完成
isMainStep : order === Math . floor ( order ) // 标记是否为主步骤
} ) ;
}
const stepItem = tempStepsMap . get ( stepName ) ! ;
// 根据节点类型设置相应的时间字段
if ( config . timeFieldId ) {
try {
const posTimeField = await posTable . getFieldById < IDateTimeField > ( config . timeFieldId ) ;
const timeValue = await posTimeField . getValue ( currentPosRecordId ) as number | null ;
if ( config . processNodeValue ? . includes ( '预计' ) ) {
stepItem . expectTimeFieldId = config . timeFieldId ;
stepItem . expectTimeValue = timeValue || undefined ;
} else if ( config . processNodeValue ? . includes ( '实际' ) && ! config . processNodeValue ? . includes ( '开始' ) ) {
stepItem . actualTimeFieldId = config . timeFieldId ;
stepItem . actualTimeValue = timeValue || undefined ;
if ( timeValue ) stepItem . finished = true ;
} else if ( config . processNodeValue === PROCESS_NODE_START_TIME || config . processNodeValue ? . includes ( '实际开始' ) ) {
stepItem . startTimeFieldId = config . timeFieldId ;
stepItem . startTimeValue = timeValue || undefined ;
}
} catch ( e ) {
console . warn ( ` Error processing time field ${ config . timeFieldId } for step ${ stepName } (node: ${ config . processNodeValue } ): ` , e ) ;
Toast . warning ( ` 处理步骤 ${ stepName } 的时间字段 ${ config . timeFieldId } 出错 ` ) ;
}
}
}
// 按顺序分组步骤
const loadedSteps = Array . from ( tempStepsMap . values ( ) ) . sort ( ( a , b ) = > ( a . order || 0 ) - ( b . order || 0 ) ) ;
// 处理并行组 - 相同整数顺序的步骤形成并行组
const parallelGroups = new Map < number , StepItem [ ] > ( ) ;
loadedSteps . forEach ( step = > {
const mainOrder = Math . floor ( step . order || 0 ) ; // 使用order的整数部分作为分组键
if ( ! parallelGroups . has ( mainOrder ) ) {
parallelGroups . set ( mainOrder , [ ] ) ;
}
parallelGroups . get ( mainOrder ) ? . push ( step ) ;
} ) ;
// 重构步骤数组,将并行组封装为主步骤
const revisedSteps : StepItem [ ] = [ ] ;
parallelGroups . forEach ( ( groupSteps , mainOrder ) = > {
// 条件: 组内多于一个步骤, 或者组内只有一个步骤但其原始order包含小数
const shouldFormParallelBlock = groupSteps . length > 1 ||
( groupSteps . length === 1 && groupSteps [ 0 ] . order !== Math . floor ( groupSteps [ 0 ] . order || 0 ) ) ;
if ( ! shouldFormParallelBlock && groupSteps . length === 1 ) {
// 单个普通步骤( order为整数且不成组) 直接添加
revisedSteps . push ( groupSteps [ 0 ] ) ;
} else {
// 形成并行组 (包含单个小数order的步骤, 如16.1自己也形成一个"并行组")
let requiresAll = true ; // 默认需要所有子步骤完成
// 检查组内是否有任何一个步骤的order以.2结尾
if ( groupSteps . some ( s = > s . order && s . order . toString ( ) . endsWith ( '.2' ) ) ) {
requiresAll = false ;
2024-07-10 12:55:22 +00:00
}
2025-06-14 16:53:18 +08:00
// 父并行步骤的名称
// 如果并行块仅由一个原始步骤构成( 例如, order为16.1的步骤单独形成一个并行块),则父步骤名称可以就是该原始步骤的名称。
// 否则,组合所有子步骤名称。
const parallelStepName = groupSteps . length === 1
? groupSteps [ 0 ] . name
: ` 并行处理组 ${ mainOrder } ` ;
revisedSteps . push ( {
name : parallelStepName ,
fieldId : ` parallel_ ${ mainOrder } ` ,
finished : requiresAll
? groupSteps . every ( s = > s . finished )
: groupSteps . some ( s = > s . finished ) , // 完成状态根据requireAllSubSteps判断
order : mainOrder , // 父并行步骤使用整数order进行排序
parallelSteps : groupSteps.sort ( ( a , b ) = > ( a . order || 0 ) - ( b . order || 0 ) ) , // 子步骤按原始order排序
requireAllSubSteps : requiresAll ,
// 尝试从第一个子步骤或具有整数order的子步骤继承时间字段
expectTimeFieldId : groupSteps.find ( s = > s . order === mainOrder ) ? . expectTimeFieldId || groupSteps [ 0 ] ? . expectTimeFieldId ,
expectTimeValue : groupSteps.find ( s = > s . order === mainOrder ) ? . expectTimeValue || groupSteps [ 0 ] ? . expectTimeValue ,
startTimeFieldId : groupSteps.find ( s = > s . order === mainOrder ) ? . startTimeFieldId || groupSteps [ 0 ] ? . startTimeFieldId ,
startTimeValue : groupSteps.find ( s = > s . order === mainOrder ) ? . startTimeValue || groupSteps [ 0 ] ? . startTimeValue ,
parallelGroup : mainOrder , // 用于UI分组显示的标识
isMainStep : true // 并行组作为一个整体视为主步骤
} ) ;
}
} ) ;
// 按顺序排序最终的步骤列表 (并行组和普通步骤)
const sortedSteps = revisedSteps . sort ( ( a , b ) = > ( a . order || 0 ) - ( b . order || 0 ) ) ;
// 处理步骤间的开始时间关联
for ( let i = 0 ; i < sortedSteps . length ; i ++ ) {
const currentStep = sortedSteps [ i ] ;
// 第一个步骤的开始时间不依赖于前一步骤
if ( i === 0 ) continue ;
// 查找上一个已完成的主步骤(包括并行组)
let previousMainStepIndex = i - 1 ;
while ( previousMainStepIndex >= 0 && ! sortedSteps [ previousMainStepIndex ] . isMainStep ) {
previousMainStepIndex -- ;
}
// 如果找到了上一个主步骤
if ( previousMainStepIndex >= 0 ) {
const previousMainStep = sortedSteps [ previousMainStepIndex ] ;
// 如果前一个主步骤已完成,且当前步骤没有开始时间,设置当前步骤的开始时间为前一步骤的完成时间
if ( previousMainStep . finished && previousMainStep . actualTimeValue &&
( ! currentStep . startTimeValue || currentStep . startTimeValue < previousMainStep . actualTimeValue ) ) {
currentStep . startTimeValue = previousMainStep . actualTimeValue ;
// 如果当前步骤有开始时间字段ID, 也更新该字段的值
if ( currentStep . startTimeFieldId && selectedRecordId ) {
try {
const startTimeField = await posTable . getFieldById < IDateTimeField > ( currentStep . startTimeFieldId ) ;
await startTimeField . setValue ( selectedRecordId , previousMainStep . actualTimeValue ) ;
console . log ( ` Set step ${ i } ( ${ currentStep . name } ) start time to previous main step's finish time: ${ new Date ( previousMainStep . actualTimeValue ) . toLocaleString ( ) } ` ) ;
} catch ( e ) {
console . warn ( ` Failed to update start time field for step ${ i } ( ${ currentStep . name } ): ` , e ) ;
}
}
}
}
}
console . log ( 'Loaded steps:' , JSON . stringify ( sortedSteps , null , 2 ) ) ;
setSteps ( sortedSteps ) ;
// 确定当前步骤
const firstUnfinished = sortedSteps . findIndex ( s = > ! s . finished ) ;
setCurrentStep ( firstUnfinished === - 1 ? sortedSteps.length : firstUnfinished ) ;
} catch ( error ) {
console . error ( 'Failed to load process steps:' , error ) ;
Toast . error ( '加载流程步骤失败,请检查控制台获取更多信息。' ) ;
setSteps ( [ ] ) ;
} finally {
setLoading ( false ) ;
setIsChangingRecord ( false ) ;
console . log ( 'Finished loading process steps.' ) ;
}
} ;
// 监听表格选择变化
useEffect ( ( ) = > {
const off = bitable . base . onSelectionChange ( async ( event ) = > {
if ( event . data && event . data . tableId === POS_TABLE_ID && event . data . recordId ) {
setSelectedRecordId ( event . data . recordId ) ;
// 只在配置数据加载完成后才加载步骤
if ( ! processConfigLoading ) {
await loadProcessSteps ( event . data . recordId ) ;
}
} else if ( ! event . data . recordId ) {
setSelectedRecordId ( undefined ) ;
setSteps ( [ ] ) ;
setCurrentStep ( 0 ) ;
setHasNoProcess ( false ) ;
}
} ) ;
return ( ) = > {
off ( ) ;
} ;
} , [ POS_TABLE_ID , processConfigLoading ] ) ;
// 监听刷新标志
useEffect ( ( ) = > {
if ( refreshSteps && selectedRecordId ) {
refreshProcessSteps ( ) ;
setRefreshSteps ( false ) ;
}
} , [ refreshSteps , selectedRecordId ] ) ;
// 处理步骤完成
const handleFinishStep = async ( stepIndex : number , subStepIndex? : number ) = > {
if ( ! selectedRecordId ) {
Toast . error ( '没有选中的POS记录' ) ;
return ;
}
const newSteps = [ . . . steps ] ;
let stepToComplete : StepItem ;
let isSubStep = typeof subStepIndex === 'number' ;
// 确定要完成的步骤
if ( isSubStep && newSteps [ stepIndex ] && newSteps [ stepIndex ] . parallelSteps ) {
stepToComplete = newSteps [ stepIndex ] . parallelSteps ! [ subStepIndex ! ] ;
} else {
stepToComplete = newSteps [ stepIndex ] ;
}
// 检查步骤是否存在
if ( ! stepToComplete ) {
Toast . error ( '找不到要完成的步骤' ) ;
return ;
}
// 检查步骤是否已经完成
if ( stepToComplete . finished && stepToComplete . actualTimeValue ) {
Toast . info ( ` 步骤 " ${ stepToComplete . name } " 已于 ${ formatTimestamp ( stepToComplete . actualTimeValue ) } 完成 ` ) ;
return ;
}
setLoading ( true ) ;
try {
const posTable = await bitable . base . getTableById ( POS_TABLE_ID ) ;
// 如果没有配置实际完成时间字段,仅标记为完成
if ( ! stepToComplete . actualTimeFieldId ) {
Toast . warning ( ` 步骤 " ${ stepToComplete . name } " 未配置实际完成时间字段ID, 无法自动记录时间。将仅标记为完成。 ` ) ;
stepToComplete . finished = true ;
} else {
// 保存步骤完成时间
const completionTime = await StepService . saveStepCompletionTime (
posTable , selectedRecordId , stepToComplete , isSubStep
) ;
Toast . success ( ` 步骤 " ${ stepToComplete . name } " 时间已记录 ` ) ;
// 如果是主步骤完成,更新后续步骤的开始时间
if ( ! isSubStep && completionTime ) {
await StepService . updateSubsequentStepsStartTimes (
posTable , selectedRecordId , newSteps , stepIndex , completionTime
) ;
// 强制刷新步骤数据
setRefreshSteps ( true ) ;
}
}
// 处理子步骤完成的特殊逻辑
if ( isSubStep ) {
const parentStep = newSteps [ stepIndex ] ;
// 如果父步骤要求所有子步骤完成才标记为完成
if ( parentStep . requireAllSubSteps && parentStep . parallelSteps ? . every ( ps = > ps . finished ) ) {
parentStep . finished = true ;
// 如果父步骤也有完成时间字段,记录完成时间
if ( parentStep . actualTimeFieldId ) {
try {
const completionTime = await StepService . saveStepCompletionTime ( posTable , selectedRecordId , parentStep ) ;
if ( completionTime ) {
// 更新父步骤后的步骤开始时间
await StepService . updateSubsequentStepsStartTimes (
posTable , selectedRecordId , newSteps , stepIndex , completionTime
) ;
// 强制刷新步骤数据
setRefreshSteps ( true ) ;
}
} catch ( e ) {
console . error ( "Error saving parent step completion time:" , e ) ;
}
} else {
// 如果没有完成时间字段,也需要更新后续步骤
setRefreshSteps ( true ) ;
}
} else if ( ! parentStep . requireAllSubSteps && parentStep . parallelSteps ? . some ( ps = > ps . finished ) ) {
// 任一子步骤完成即可推进主步骤
parentStep . finished = true ;
// 如果父步骤有完成时间字段,记录完成时间
if ( parentStep . actualTimeFieldId ) {
try {
const completionTime = await StepService . saveStepCompletionTime ( posTable , selectedRecordId , parentStep ) ;
if ( completionTime ) {
// 更新父步骤后的步骤开始时间
await StepService . updateSubsequentStepsStartTimes (
posTable , selectedRecordId , newSteps , stepIndex , completionTime
) ;
// 强制刷新步骤数据
setRefreshSteps ( true ) ;
}
} catch ( e ) {
console . error ( "Error saving parent step completion time:" , e ) ;
}
} else {
// 如果没有完成时间字段,也需要更新后续步骤
setRefreshSteps ( true ) ;
}
}
}
// 更新步骤状态和当前步骤
setSteps ( newSteps ) ;
const firstUnfinished = newSteps . findIndex ( s = > ! s . finished ) ;
setCurrentStep ( firstUnfinished === - 1 ? newSteps.length : firstUnfinished ) ;
} catch ( e ) {
console . error ( "Error finishing step:" , e ) ;
Toast . error ( ` 完成步骤 " ${ stepToComplete . name } " 失败 ` ) ;
} finally {
setLoading ( false ) ;
}
} ;
// 确定当前并行组是否为活动组
const isActiveParallelGroup = ( step : StepItem ) : boolean = > {
// 如果当前步骤是并行组,则该并行组是活动的
if ( step . parallelSteps && currentStep === steps . findIndex ( s = > s === step ) ) {
return true ;
}
// 如果当前步骤是并行组中的子步骤,则其父并行组是活动的活动的
if ( currentStep < steps . length && steps [ currentStep ] . parallelSteps ) {
const currentParallelStep = steps [ currentStep ] ;
// 检查当前步骤的并行组ID是否与参数步骤的并行组ID相同
return currentParallelStep . parallelGroup === step . parallelGroup ;
}
return false ;
} ;
// 判断并行组中是否有任何子步骤已完成
const isAnySubStepFinished = ( step : StepItem ) : boolean = > {
if ( ! step . parallelSteps ) return false ;
return step . parallelSteps . some ( subStep = > subStep . finished ) ;
} ;
// 判断是否可以访问子步骤
const canAccessSubSteps = ( step : StepItem ) : boolean = > {
// 如果是当前步骤或活动并行组,当然可以访问
if ( isActiveParallelGroup ( step ) ) return true ;
// 如果是已完成的并行组,也可以访问
if ( step . finished ) return true ;
// 如果并行组中已有任何子步骤完成,也可以访问
if ( isAnySubStepFinished ( step ) ) return true ;
// 如果并行组的前置步骤(主步骤)已完成,也可以访问
const stepIndex = steps . findIndex ( s = > s === step ) ;
if ( stepIndex > 0 ) {
// 查找上一个主步骤
let previousMainStepIndex = stepIndex - 1 ;
while ( previousMainStepIndex >= 0 && ! steps [ previousMainStepIndex ] . isMainStep ) {
previousMainStepIndex -- ;
}
// 如果找到了上一个主步骤且它已完成
if ( previousMainStepIndex >= 0 && steps [ previousMainStepIndex ] . finished ) {
return true ;
}
}
return false ;
} ;
// 渲染加载状态
if ( processConfigLoading ) {
return (
< div className = "app-container" style = { { padding : 24 , minWidth : 410 } } >
< Typography.Title heading = { 4 } > 流 程 进 度 < / Typography.Title >
< div style = { { display : 'flex' , justifyContent : 'center' , alignItems : 'center' , height : '50vh' } } >
< Spin size = "large" tip = "加载中..." / >
< / div >
< / div >
) ;
}
// 渲染无流程记录提示
if ( hasNoProcess && selectedRecordId ) {
return (
< div className = "app-container" style = { { padding : 24 , minWidth : 410 } } >
< Typography.Title heading = { 4 } > 流 程 进 度 < / Typography.Title >
< div style = { { marginTop : 24 , textAlign : 'center' } } >
< Empty
description = {
< Typography.Text style = { { fontSize : 16 , color : '#4e5969' } } >
此 记 录 没 有 配 置 流 程
< / Typography.Text >
}
/ >
< Button
onClick = { async ( ) = > { // 修改为 async 函数
setHasNoProcess ( false ) ;
await fetchAllProcessConfigs ( ) ; // 首先重新获取所有流程配置
if ( selectedRecordId ) {
await loadProcessSteps ( selectedRecordId ) ; // 然后使用新的配置加载步骤
}
} }
style = { { marginTop : 20 } }
loading = { processConfigLoading || loading } // 当任一加载进行中时显示loading
>
刷 新 重 试
< / Button >
< / div >
< / div >
) ;
}
// 渲染切换记录时的加载状态
if ( isChangingRecord ) {
return (
< div className = "app-container" style = { { padding : 24 , minWidth : 410 } } >
< Typography.Title heading = { 4 } > 流 程 进 度 < / Typography.Title >
< div style = { {
display : 'flex' ,
flexDirection : 'column' ,
justifyContent : 'center' ,
alignItems : 'center' ,
height : '50vh' ,
backgroundColor : '#fafafa' ,
borderRadius : 8 ,
border : '1px solid #e5e6eb'
} } >
< Spin
size = "large"
style = { { marginBottom : 20 } }
/ >
< Typography.Title
heading = { 5 }
style = { {
fontSize : 18 ,
fontWeight : 500 ,
color : '#4e5969' ,
textAlign : 'center'
} }
>
正 在 加 载 流 程 数 据 . . .
< / Typography.Title >
< Typography.Text
style = { {
marginTop : 8 ,
color : '#86909c' ,
fontSize : 14
} }
>
请 稍 候 , 系 统 正 在 为 您 获 取 当 前 记 录 的 流 程 信 息
< / Typography.Text >
< / div >
< / div >
) ;
}
// 渲染主界面
return (
< div className = "app-container" style = { { padding : 24 , minWidth : 410 } } >
< Typography.Title heading = { 4 } > 流 程 进 度 < / Typography.Title >
< Steps direction = "vertical" type = "basic" current = { currentStep } style = { { margin : '28px 0' } } >
{ steps . map ( ( step , idx ) = > (
< Steps.Step
key = { step . name }
title = { step . name }
status = { step . finished ? 'finish' : idx === currentStep ? 'process' : 'wait' }
description = {
< StepCard
step = { step }
idx = { idx }
currentStep = { currentStep }
loading = { loading }
onFinishStep = { handleFinishStep }
formatTimestamp = { formatTimestamp }
isCurrentStep = { idx === currentStep }
isActiveParallelGroup = { isActiveParallelGroup ( step ) }
isAnySubStepFinished = { isAnySubStepFinished ( step ) }
canAccessSubSteps = { canAccessSubSteps ( step ) }
/ >
}
/ >
) ) }
< / Steps >
< / div >
) ;
}