From 6de10eef90685e031f72f3480d8f38e29e552090 Mon Sep 17 00:00:00 2001 From: mairuiming Date: Sat, 14 Jun 2025 16:53:18 +0800 Subject: [PATCH] 1 --- src/App.tsx | 1251 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 1187 insertions(+), 64 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index eaa2062..217c7b7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,73 +1,1196 @@ +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'; -import { bitable, ITableMeta } from "@lark-base-open/js-sdk"; -import { Button, Form } from '@douyinfe/semi-ui'; -import { BaseFormApi } from '@douyinfe/semi-foundation/lib/es/form/interface'; -import { useState, useEffect, useRef, useCallback } from 'react'; + +// 步骤数据结构定义 +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 [tableMetaList, setTableMetaList] = useState(); - const formApi = useRef(); - const addRecord = useCallback(async ({ table: tableId }: { table: string }) => { - if (tableId) { - const table = await bitable.base.getTableById(tableId); - table.addRecord({ - fields: {}, + 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(() => { - Promise.all([bitable.base.getTableMetaList(), bitable.base.getSelection()]) - .then(([metaList, selection]) => { - setTableMetaList(metaList); - formApi.current?.setValues({ table: selection.tableId }); - }); + fetchAllProcessConfigs(); }, []); - return ( -
-

- Edit src/App.tsx and save to reload -

-
formApi.current = baseFormApi}> - - - - - - - - { - Array.isArray(tableMetaList) && tableMetaList.map(({ name, id }) => { - return ( - - {name} - - ); - }) + // 初始化日期字段 + 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) // 标记是否为主步骤 + }); + } + + 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} 出错`); } - - -
-
- ) -} \ No newline at end of file + } + } + + // 按顺序分组步骤 + 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) => ( + + } + /> + ))} + +
+ ); +}