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'; import './App.css'; // 步骤数据结构定义 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 { if (!selectedRecordId || !stepItem.actualTimeFieldId) return null; try { const timeField = await posTable.getFieldById(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(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(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(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(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 (
{/* 并行步骤组标题 */} {step.parallelSteps && (
并行处理组: {step.name} {step.requireAllSubSteps ? '所有子步骤完成后自动推进' : '任一子步骤完成后自动推进'}
)} {/* 普通步骤的时间信息展示 */} {!step.parallelSteps && (
预计完成时间:{formatTimestamp(step.expectTimeValue)} 实际开始时间:{formatTimestamp(step.startTimeValue)} 实际完成时间:{formatTimestamp(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 小时') : '-'}
)} {/* 并行子步骤 */} {step.parallelSteps && (
{step.parallelSteps.map((sub, subIdx) => (
{/* 完成状态标识 */} {sub.finished && (
已完成
)} {sub.name}
预计完成时间:{formatTimestamp(sub.expectTimeValue || step.expectTimeValue)} 实际开始时间:{formatTimestamp(sub.startTimeValue || step.startTimeValue)} 实际完成时间:{formatTimestamp(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) + ' 小时' : '-')}
{/* 允许活动并行组中的子步骤被点击,或并行组中已有子步骤完成 */} {!sub.finished && (isCurrentStep || isActiveParallelGroup || isAnySubStepFinished || canAccessSubSteps) && ( )} {/* 只有当父步骤未完成且不在当前活动状态时才显示等待提示 */} {!sub.finished && !(isCurrentStep || isActiveParallelGroup || isAnySubStepFinished || canAccessSubSteps) && (
等待前置步骤完成
)}
))}
)} {/* 当前步骤的完成按钮 */} {idx === currentStep && !step.finished && !step.parallelSteps && ( )}
); }; export default function App() { const [dateFields, setDateFields] = useState([]); const [steps, setSteps] = useState([]); const [currentStep, setCurrentStep] = useState(0); const [loading, setLoading] = useState(false); const [selectedRecordId, setSelectedRecordId] = useState(); const [refreshSteps, setRefreshSteps] = useState(false); const [isChangingRecord, setIsChangingRecord] = useState(false); // 记录是否正在切换记录 const [hasNoProcess, setHasNoProcess] = useState(false); // 记录是否没有配置流程 // 缓存所有流程配置数据 const [allProcessConfigs, setAllProcessConfigs] = useState([]); const [processConfigLoading, setProcessConfigLoading] = useState(true); // 缓存字段元数据,避免重复获取 const [fieldMetadataCache, setFieldMetadataCache] = useState>(new Map()); // 缓存按POS记录ID分组的配置 const [configsByPosLink, setConfigsByPosLink] = useState>(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}`); } }); 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(); 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); } } useEffect(() => { fetchAllProcessConfigs(); }, []); // 初始化日期字段 useEffect(() => { async function fetchDateFieldsForUI() { try { const activeTable = await bitable.base.getActiveTable(); const fields = await activeTable.getFieldMetaListByType(FieldType.DateTime); setDateFields(fields); } catch (e) { console.error("Failed to fetch date fields for UI: ", e); Toast.error("获取可选日期字段列表失败"); } } fetchDateFieldsForUI(); }, []); // 优化:初始化步骤时处理并行组 const initializeStepsWithParallel = (configs: ProcessConfigItem[]) => { if (configs.length === 0) return []; // 使用Map按order分组,提高查询效率 const stepsByOrder = new Map(); // 初始化步骤 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(); const tempStepsMap = new Map(); // 首先处理每个步骤的基本信息 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) // 标记是否为主步骤 }); } else { // 如果步骤已存在,使用最小的order值 const existingStep = tempStepsMap.get(stepName)!; existingStep.order = Math.min(existingStep.order || 0, order); // 重新计算是否为主步骤 existingStep.isMainStep = existingStep.order === Math.floor(existingStep.order); } const stepItem = tempStepsMap.get(stepName)!; // 根据节点类型设置相应的时间字段 if (config.timeFieldId) { try { const posTimeField = await posTable.getFieldById(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(); 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; } // 父并行步骤的名称 // 如果并行块仅由一个原始步骤构成(例如,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(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 (
流程进度
); } // 渲染无流程记录提示 if (hasNoProcess && selectedRecordId) { return (
流程进度
此记录没有配置流程 } />
); } // 渲染切换记录时的加载状态 if (isChangingRecord) { return (
流程进度
正在加载流程数据... 请稍候,系统正在为您获取当前记录的流程信息
); } // 渲染主界面 return (
流程进度 {steps.map((step, idx) => ( } /> ))}
); }