diff --git a/src/App.tsx b/src/App.tsx index 36c52cc..f38aa28 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,16 @@ import { executePricingQuery, executeSecondaryProcessQuery, executePricingDetail const { Title, Text } = Typography; +// 统一的日期格式常量 +const DATE_FORMATS = {import { bitable, FieldType } from '@lark-base-open/js-sdk'; +import { Button, Typography, List, Card, Space, Divider, Spin, Table, Select, Modal, DatePicker, InputNumber } from '@douyinfe/semi-ui'; +import { useState, useEffect } from 'react'; +import { addDays, format } from 'date-fns'; +import { zhCN } from 'date-fns/locale'; +import { executePricingQuery, executeSecondaryProcessQuery, executePricingDetailsQuery } from './services/apiService'; + +const { Title, Text } = Typography; + // 统一的日期格式常量 const DATE_FORMATS = { DISPLAY_WITH_TIME: 'yyyy-MM-dd HH:mm', // 显示格式:2026-02-20 16:54 @@ -5762,6 +5772,6630 @@ export default function App() { // 计算最终交期余量:基础余量 - 扣减天数 finalDaysDiff = baseDaysDiff - deliveryMarginDeductions; + return ( + <> + {expectedDate && ( + <> + {/* 客户期望日期 */} +
+ + 客户期望日期 + + + {formatDate(expectedDate)} + + + {getDayOfWeek(expectedDate)} + +
+ + {/* 减号 */} +
+ - +
+ + {/* 最后流程完成 */} +
+ + {effectiveLastProcess === timelineResults[timelineResults.length - 1] + ? '最后流程完成' + : `有效流程完成 (${effectiveLastProcess.nodeName})`} + + + {formatDate(adjustedCompletionDate)} + + + {getDayOfWeek(adjustedCompletionDate)} + + {effectiveLastProcess !== timelineResults[timelineResults.length - 1] && ( + + (最后流程时效为0,使用此流程) + + )} + {completionDateAdjustment > 0 && ( + + (已调整 +{completionDateAdjustment}天) + + )} +
+ + {/* 加号 */} +
+ + +
+ + {/* 缓冲期 */} +
+ + 缓冲期 + + + {dynamicBufferDays}天 + + + 固定缓冲 + +
+ + + + {/* 等号 */} +
+ = +
+ + )} + + {!expectedDate && ( + <> + {/* 最后流程完成 */} +
+ + {effectiveLastProcess === timelineResults[timelineResults.length - 1] + ? '最后流程完成' + : `有效流程完成 (${effectiveLastProcess.nodeName})`} + + + {formatDate(effectiveLastProcess.estimatedEnd)} + + + {getDayOfWeek(effectiveLastProcess.estimatedEnd)} + + {effectiveLastProcess !== timelineResults[timelineResults.length - 1] && ( + + (最后流程时效为0,使用此流程) + + )} +
+ + {/* 加号 */} +
+ + +
+ + {/* 缓冲期 */} +
+ + 缓冲期 + + + {dynamicBufferDays}天 + + + 固定缓冲 + +
+ + + + {/* 减号 */} +
+ - +
+ + {/* 今天 */} +
+ + 今天 + + + {formatDate(new Date())} + + + {getDayOfWeek(new Date())} + +
+ + {/* 等号 */} +
+ = +
+ + )} + + {/* 结果日期说明(位于等式下方) */} +
+ + 结果日期(最后流程完成 + 缓冲期):{formatDate(deliveryDate)},{getDayOfWeek(deliveryDate)} + +
+ + {/* 交期余量结果 */} +
= 0 ? '#f6ffed' : '#fff2f0', + borderRadius: '8px', + border: `2px solid ${finalDaysDiff >= 0 ? '#52c41a' : '#ff4d4f'}` + }}> + + 交期余量 + + = 0 ? '#52c41a' : '#ff4d4f', + display: 'block' + }}> + {finalDaysDiff >= 0 ? '+' : ''}{finalDaysDiff}天 + + = 0 ? '#52c41a' : '#ff4d4f', + fontWeight: 'bold' + }}> + {finalDaysDiff >= 0 ? '✅ 时间充裕' : '⚠️ 时间紧张'} + +
+ + ); + })()} + + +
+ {(() => { + // 计算动态缓冲期和总调整量 + const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); + const baseBuferDays = baseBufferDays; + const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments); + + return ( + + 💡 说明: + {expectedDate + ? `交期余量 = 客户期望日期 - (最后流程完成日期 + ${dynamicBufferDays}天缓冲期)` + : `交期余量 = (最后流程完成日期 + ${dynamicBufferDays}天缓冲期) - 今天` + } +
+ {expectedDate + ? '• 正值表示有充裕时间,负值表示可能延期' + : '• 显示预计交付日期距离今天的天数' + } +
+ • 缓冲期 = 基础{baseBuferDays}天 - 节点调整总量(最小0天),包含质检、包装、物流等后续环节 + {totalAdjustments > 0 && ( + <> +
+ • 当前节点总调整量:+{totalAdjustments}天,缓冲期相应减少 + + )} +
+ ); + })()} +
+ + + )} + +
+ 📊 计算说明: + + 共找到 {timelineResults.length} 个匹配的节点。时间按流程顺序计算,上一个节点的完成时间等于下一个节点的开始时间。每个节点使用其特定的休息日配置和计算方式进行工作日计算。 + +
+ 🗓️ 计算规则说明: + + • 内部计算:按9小时工作制(9:00-18:00),超时自动顺延至下个工作日 +
+ • 外部计算:按24小时制计算,适用于外部供应商等不受工作时间限制的节点 +
+ • 根据每个节点的休息日配置自动跳过相应的休息日 +
+ • 可为每个节点配置“跳过日期”,这些日期将不参与工作日计算 +
+ • 时效值以"工作日"为单位计算,确保预期时间的准确性 +
+ • 使用 +1/-1 按钮调整整天,+0.5/-0.5 按钮调整半天,系统会自动重新计算所有后续节点 +
+ • 不同节点可配置不同的休息日和计算方式(如:内部节点按工作时间,外部节点按自然时间) +
+
+ {Object.keys(timelineAdjustments).length > 0 && ( +
+ 当前调整: +
+ {Object.entries(timelineAdjustments).map(([nodeIndex, adjustment]) => { + const nodeName = timelineResults[parseInt(nodeIndex)]?.nodeName; + return ( + + {nodeName}: {adjustment > 0 ? '+' : ''}{adjustment.toFixed(1)} 天 + + ); + })} +
+
+ )} +
+ + )} + + + + + + {/* 标签选择部分,仅在生成模式显示 */} + {mode === 'generate' && labelOptions && Object.keys(labelOptions).length > 0 && ( + +
+ {Array.from({ length: 10 }, (_, i) => i + 1).map(num => { + const labelKey = `标签${num}`; + const options = labelOptions[labelKey] || []; + const isMultiSelect = labelKey === '标签7' || labelKey === '标签8' || labelKey === '标签10'; + + return ( +
+ {labelKey} + +
+ ); + })} +
+ + {/* 客户期望日期选择 */} +
+ 客户期望日期 + setExpectedDate(date)} + format="yyyy-MM-dd" + disabledDate={(date) => { + // 禁用今天之前的日期 + const today = new Date(); + today.setHours(0, 0, 0, 0); + return date < today; + }} + /> + {expectedDate && ( + + 已选择:{formatDate(expectedDate, 'CHINESE_DATE')} + + )} +
+ + {Object.keys(selectedLabels).length > 0 && ( +
+ + + + 已选择 {Object.keys(selectedLabels).length} 个标签 + + + +
+ 当前选择的标签: +
+ {Object.entries(selectedLabels).map(([key, value]) => { + const displayValue = Array.isArray(value) ? value.join(', ') : value; + return ( + + {key}: {displayValue} + + ); + })} +
+
+
+ )} +
+ )} + + {mode === 'batch' && ( +
+ {/* 批量处理配置 */} +
+ + 批量处理配置 + +
+ {/* 第一行:表和视图选择 */} +
+
+ 数据表: + +
+
+ 视图: + +
+
+ + {/* 第二行:处理行数和操作按钮 */} +
+
+ 处理行数: + setBatchRowCount(value || 10)} + min={1} + max={100} + style={{ width: 100 }} + /> +
+ +
+ {batchProcessing && ( +
+
+ + 进度: {batchProgress.current}/{batchProgress.total} + +
+ )} + +
+
+
+
+
+ )} + + {mode === 'generate' && ( +
+ +
+
+ 版单数据 + + 选择需要生成时效的版单记录 + + {selectedRecords.length > 0 && ( + + 已选择 {selectedRecords.length} 条记录 + + )} +
+ + + + + {selectedRecords.length > 0 && ( + + )} + +
+ + {/* 已选择记录的详细信息 */} + {selectedRecords.length > 0 && recordDetails.length > 0 && ( +
+
+
+ 主记录: + + {recordDetails[0].displayValue || recordDetails[0].id} + +
+ {recordDetails.length > 1 && ( +
+ + 其他 {recordDetails.length - 1} 条 +
+ )} +
+
+ )} + + {/* 加载状态 */} + {loading && ( +
+ +
+ 正在加载版单数据... +
+
+ )} + + {/* 空状态提示 */} + {selectedRecords.length === 0 && !loading && ( +
+ 请选择版单记录开始操作 +
+ )} +
+ + {/* 批量处理配置 - 移动到版单数据下方 */} + {mode === 'batch' && ( +
+ + 批量处理配置 + +
+ {/* 第一行:表和视图选择 */} +
+
+ 数据表: + +
+
+ 视图: + +
+
+ + {/* 第二行:处理行数和操作按钮 */} +
+
+ 处理行数: + setBatchRowCount(value || 10)} + min={1} + max={100} + style={{ width: 100 }} + /> +
+ +
+ {batchProcessing && ( +
+
+ + 进度: {batchProgress.current}/{batchProgress.total} + +
+ )} + +
+
+
+
+ )} +
+ )} + {mode === 'generate' && ( + <> + {/* 面料数据查询结果 */} + {queryResults.length > 0 && ( + <> + + 面料数据查询结果 ({queryResults.length} 条) + ({ ...item, key: index }))} + pagination={{ pageSize: 10 }} + style={{ marginTop: '10px' }} + /> + + )} + {/* 二次工艺查询结果 */} + {secondaryProcessResults.length > 0 && ( + <> + + 二次工艺查询结果 ({secondaryProcessResults.length} 条) +
({ ...item, key: index }))} + pagination={{ pageSize: 10 }} + style={{ marginTop: '10px' }} + /> + + )} + {/* 工艺价格查询结果 */} + {pricingDetailsResults.length > 0 && ( + <> + + 工艺价格查询结果 ({pricingDetailsResults.length} 条) +
({ ...item, key: index }))} + pagination={{ pageSize: 10 }} + style={{ marginTop: '10px' }} + /> + + )} + + )} + + ); +} + DISPLAY_WITH_TIME: 'yyyy-MM-dd HH:mm', // 显示格式:2026-02-20 16:54 + DISPLAY_DATE_ONLY: 'yyyy-MM-dd', // 日期格式:2026-02-20 + STORAGE_FORMAT: 'yyyy-MM-dd HH:mm:ss', // 存储格式:2026-02-20 16:54:00 + CHINESE_DATE: 'yyyy年MM月dd日', // 中文格式:2026年02月20日 +} as const; + +// 统一的星期显示 +const WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] as const; + +// 统一的字段提取函数 +const extractText = (val: any) => { + if (Array.isArray(val) && val.length > 0) { + const item = val[0]; + return typeof item === 'string' ? item : (item?.text || item?.name || ''); + } else if (typeof val === 'string') { + return val; + } else if (val && typeof val === 'object') { + return val.text || val.name || ''; + } + return ''; +}; + +export default function App() { + const [selectedRecords, setSelectedRecords] = useState([]); + const [recordDetails, setRecordDetails] = useState([]); + const [loading, setLoading] = useState(false); + const [queryResults, setQueryResults] = useState([]); + const [queryLoading, setQueryLoading] = useState(false); + const [secondaryProcessResults, setSecondaryProcessResults] = useState([]); + const [secondaryProcessLoading, setSecondaryProcessLoading] = useState(false); + const [pricingDetailsResults, setPricingDetailsResults] = useState([]); + const [pricingDetailsLoading, setPricingDetailsLoading] = useState(false); + + // 标签相关状态 + const [labelOptions, setLabelOptions] = useState<{[key: string]: any[]}>({}); + const [selectedLabels, setSelectedLabels] = useState<{[key: string]: string | string[]}>({}); + const [labelLoading, setLabelLoading] = useState(false); + + // 客户期望日期状态 + const [expectedDate, setExpectedDate] = useState(null); + // 起始时间状态(从货期记录表获取,新记录则使用当前时间) + const [startTime, setStartTime] = useState(null); + + // 预览相关状态(已移除未使用的 previewLoading 状态) + + // 时效计算相关状态 + const [timelineVisible, setTimelineVisible] = useState(false); + const [timelineLoading, setTimelineLoading] = useState(false); + const [timelineResults, setTimelineResults] = useState([]); + const [timelineAdjustments, setTimelineAdjustments] = useState<{[key: number]: number}>({}); + // 交期余量扣减状态:记录从交期余量中扣减的天数 + const [deliveryMarginDeductions, setDeliveryMarginDeductions] = useState(0); + // 最后流程完成日期调整状态:记录最后流程完成日期增加的天数 + const [completionDateAdjustment, setCompletionDateAdjustment] = useState(0); + // 实际完成日期状态:记录每个节点的实际完成日期 + const [actualCompletionDates, setActualCompletionDates] = useState<{[key: number]: Date | null}>({}); + // 基础缓冲期天数(可配置),用于计算动态缓冲期,默认14天 + const [baseBufferDays, setBaseBufferDays] = useState(14); + // 快照回填来源(foreign_id、款式、颜色、文本2) + const [currentForeignId, setCurrentForeignId] = useState(null); + const [currentStyleText, setCurrentStyleText] = useState(''); + const [currentColorText, setCurrentColorText] = useState(''); + const [currentText2, setCurrentText2] = useState(''); + const [currentVersionNumber, setCurrentVersionNumber] = useState(null); + // 功能入口模式与调整相关状态 + const [mode, setMode] = useState<'generate' | 'adjust' | null>(null); + const [modeSelectionVisible, setModeSelectionVisible] = useState(true); + const [adjustLoading, setAdjustLoading] = useState(false); + // 删除未使用的 deliveryRecords 状态 + const [selectedDeliveryRecordId, setSelectedDeliveryRecordId] = useState(''); + + // 批量处理相关状态 + const [batchProcessing, setBatchProcessing] = useState(false); + const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0 }); + const [batchRowCount, setBatchRowCount] = useState(10); // 默认处理10行 + + // 批量处理当前记录信息(用于保存时传递正确的数据) + const [currentBatchRecord, setCurrentBatchRecord] = useState<{ + selectedRecords: string[], + recordDetails: any[], + labels: Record, + expectedDate: any, + startTime: any + } | null>(null); + + // 批量处理表和视图选择状态 + const [selectedBatchTableId, setSelectedBatchTableId] = useState(''); + const [selectedBatchViewId, setSelectedBatchViewId] = useState(''); + const [availableTables, setAvailableTables] = useState>([]); + const [availableViews, setAvailableViews] = useState>([]); + const [tablesLoading, setTablesLoading] = useState(false); + const [viewsLoading, setViewsLoading] = useState(false); + + // 全局变量重置:在切换功能或切换版单/批量数据时,清空页面与计算相关状态 + const resetGlobalState = (opts?: { resetMode?: boolean }) => { + // 运行时加载状态 + setLoading(false); + setQueryLoading(false); + setSecondaryProcessLoading(false); + setPricingDetailsLoading(false); + setLabelLoading(false); + setAdjustLoading(false); + setTimelineLoading(false); + setBatchProcessing(false); + setBatchProgress({ current: 0, total: 0 }); + + // 页面与计算数据 + setSelectedRecords([]); + setRecordDetails([]); + setSelectedLabels({}); + setExpectedDate(null); + setStartTime(null); + setTimelineVisible(false); + setTimelineResults([]); + setTimelineAdjustments({}); + setIsRestoringSnapshot(false); + + // 当前记录与批量信息 + setCurrentBatchRecord(null); + + // 当前回填状态 + setCurrentForeignId(null); + setCurrentStyleText(''); + setCurrentColorText(''); + setCurrentText2(''); + setCurrentVersionNumber(null); + + // 可选:重置模式 + if (opts?.resetMode) { + setMode(null); + setModeSelectionVisible(true); + } + }; + + // 指定的数据表ID和视图ID + const TABLE_ID = 'tblPIJ7unndydSMu'; + const VIEW_ID = 'vewb28sjuX'; + + // 标签表ID + const LABEL_TABLE_ID = 'tblPnQscqwqopJ8V'; + + // 流程配置表ID + const PROCESS_CONFIG_TABLE_ID = 'tblMygOc6T9o4sYU'; + const NODE_NAME_FIELD_ID = 'fld0g9L9Fw'; // 节点名称字段 + const PROCESS_LABEL_FIELD_ID = 'fldrVTa23X'; // 流程配置表的标签字段 + const PROCESS_ORDER_FIELD_ID = 'fldbfJQ4Zs'; // 流程顺序字段ID + const WEEKEND_DAYS_FIELD_ID = 'fld2BvjbIN'; // 休息日字段ID(多选项:0-6代表周一到周日) + const START_DATE_RULE_FIELD_ID = 'fld0KsQ2j3'; // 起始日期调整规则字段ID + const DATE_ADJUSTMENT_RULE_FIELD_ID = 'fld0KsQ2j3'; // 日期调整规则字段ID + const EXCLUDED_DATES_FIELD_ID = 'fldGxzC5uG'; // 不参与计算日期(多选,格式:yyyy-MM-dd) + + // 时效数据表相关常量 + const TIMELINE_TABLE_ID = 'tblPnQscqwqopJ8V'; // 时效数据表ID + const TIMELINE_FIELD_ID = 'fldniJmZe3'; // 时效值字段ID + const TIMELINE_NODE_FIELD_ID = 'fldeIZzokl'; // 时效表中的节点名称字段ID + const CALCULATION_METHOD_FIELD_ID = 'fldxfLZNUu'; // 时效计算方式字段ID + + // 新表ID(批量生成表) + const BATCH_TABLE_ID = 'tbl673YuipMIJnXL'; // 新创建的多维表格ID + + // 已移除:调整模式不再加载货期记录列表 + + // 入口选择处理 + const chooseMode = (m: 'generate' | 'adjust') => { + // 切换功能时重置全局变量,但保留新的mode + resetGlobalState({ resetMode: false }); + setMode(m); + setModeSelectionVisible(false); + }; + + // 根据货期记录ID读取节点详情并还原流程数据 + const loadProcessDataFromDeliveryRecord = async (deliveryRecordId: string) => { + if (!deliveryRecordId) { + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'warning', message: '请先选择一条货期记录' }); + } + return; + } + setTimelineLoading(true); + try { + const deliveryTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID); + const deliveryRecord = await deliveryTable.getRecordById(deliveryRecordId); + const nodeDetailsVal = deliveryRecord?.fields?.[DELIVERY_NODE_DETAILS_FIELD_ID]; + + // 优先使用货期记录表中的快照字段进行一键还原(新方案) + try { + const deliverySnapVal = deliveryRecord?.fields?.[DELIVERY_SNAPSHOT_JSON_FIELD_ID]; + let deliverySnapStr: string | null = null; + if (typeof deliverySnapVal === 'string') { + deliverySnapStr = deliverySnapVal; + } else if (Array.isArray(deliverySnapVal)) { + const texts = deliverySnapVal + .filter((el: any) => el && el.type === 'text' && typeof el.text === 'string') + .map((el: any) => el.text); + deliverySnapStr = texts.length > 0 ? texts.join('') : null; + } else if (deliverySnapVal && typeof deliverySnapVal === 'object') { + if ((deliverySnapVal as any).text && typeof (deliverySnapVal as any).text === 'string') { + deliverySnapStr = (deliverySnapVal as any).text; + } else if ((deliverySnapVal as any).type === 'text' && typeof (deliverySnapVal as any).text === 'string') { + deliverySnapStr = (deliverySnapVal as any).text; + } + } + + if (deliverySnapStr && deliverySnapStr.trim() !== '') { + setIsRestoringSnapshot(true); + const snapshot = JSON.parse(deliverySnapStr); + + // 恢复页面与全局状态 + if (snapshot.selectedLabels) setSelectedLabels(snapshot.selectedLabels); + if (!mode && snapshot.mode) setMode(snapshot.mode); + if (snapshot.foreignId) setCurrentForeignId(snapshot.foreignId); + if (snapshot.styleText) setCurrentStyleText(snapshot.styleText); + if (snapshot.colorText) setCurrentColorText(snapshot.colorText); + if (snapshot.text2) setCurrentText2(snapshot.text2); + + if (snapshot.generationModeState) { + const genState = snapshot.generationModeState; + if (genState.currentForeignId) setCurrentForeignId(genState.currentForeignId); + if (genState.currentStyleText) setCurrentStyleText(genState.currentStyleText); + if (genState.currentColorText) setCurrentColorText(genState.currentColorText); + if (genState.currentText2) setCurrentText2(genState.currentText2); + if (genState.currentVersionNumber !== undefined) setCurrentVersionNumber(genState.currentVersionNumber); + if (genState.recordDetails && Array.isArray(genState.recordDetails)) { + setRecordDetails(genState.recordDetails); + } + } + + if (snapshot.version !== undefined) { + let vNum: number | null = null; + if (typeof snapshot.version === 'number') { + vNum = snapshot.version; + } else if (typeof snapshot.version === 'string') { + const match = snapshot.version.match(/\d+/); + if (match) vNum = parseInt(match[0], 10); + } + if (vNum !== null && !isNaN(vNum)) setCurrentVersionNumber(vNum); + } + + if (snapshot.timelineAdjustments) setTimelineAdjustments(snapshot.timelineAdjustments); + if (snapshot.expectedDateTimestamp) { + setExpectedDate(new Date(snapshot.expectedDateTimestamp)); + } else if (snapshot.expectedDateString) { + setExpectedDate(new Date(snapshot.expectedDateString)); + } + + // 优先从快照恢复起始时间 + let startTimeRestored = false; + if (snapshot.startTimestamp) { + setStartTime(new Date(snapshot.startTimestamp)); + startTimeRestored = true; + } else if (snapshot.startString) { + const parsed = new Date(snapshot.startString); + if (!isNaN(parsed.getTime())) { + setStartTime(parsed); + startTimeRestored = true; + } + } + + if (!startTimeRestored) { + const startTimeValue = deliveryRecord?.fields?.[DELIVERY_START_TIME_FIELD_ID]; + if (startTimeValue) { + let extractedStartTime: Date | null = null; + if (typeof startTimeValue === 'number') { + extractedStartTime = new Date(startTimeValue); + } else if (Array.isArray(startTimeValue) && startTimeValue.length > 0) { + const timestamp = startTimeValue[0]; + if (typeof timestamp === 'number') extractedStartTime = new Date(timestamp); + } + if (extractedStartTime && !isNaN(extractedStartTime.getTime())) setStartTime(extractedStartTime); + } + } + + // 完整快照直接包含timelineResults,优先使用 + if (Array.isArray(snapshot.timelineResults)) { + setTimelineResults(snapshot.timelineResults); + setTimelineVisible(true); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'success', message: '已按货期记录快照还原流程数据' }); + } + setTimelineLoading(false); + setIsRestoringSnapshot(false); + return; + } + + // 兼容完整快照标识但没有直接timelineResults的情况 + if (snapshot.isCompleteSnapshot || snapshot.snapshotType === 'complete' || snapshot.isGlobalSnapshot) { + // 若没有timelineResults,视为旧格式,保持兼容:不在此分支拼装,后续走旧流程 + } else { + // 非完整快照则进入旧流程(从节点记录扫描快照) + } + } + } catch (e) { + console.warn('从货期记录快照字段还原失败,回退到旧流程:', e); + } + + let recordIds: string[] = []; + if (nodeDetailsVal && typeof nodeDetailsVal === 'object' && (nodeDetailsVal as any).recordIds) { + recordIds = (nodeDetailsVal as any).recordIds as string[]; + } else if (Array.isArray(nodeDetailsVal)) { + recordIds = nodeDetailsVal.map((item: any) => item?.recordId || item?.id || item).filter(Boolean); + } + + if (!recordIds || recordIds.length === 0) { + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'warning', message: '该货期记录未包含节点详情或为空' }); + } + setTimelineLoading(false); + return; + } + + const processTable = await bitable.base.getTable(PROCESS_DATA_TABLE_ID); + const records = await Promise.all(recordIds.map(id => processTable.getRecordById(id))); + + // 优先使用文本2快照一模一样还原 + try { + // 在所有记录中查找非空快照 + let snapStr: string | null = null; + for (const rec of records) { + const snapVal = rec?.fields?.[PROCESS_SNAPSHOT_JSON_FIELD_ID]; + let candidate: string | null = null; + if (typeof snapVal === 'string') { + candidate = snapVal; + } else if (Array.isArray(snapVal)) { + // 文本结构:拼接所有text片段 + const texts = snapVal + .filter((el: any) => el && el.type === 'text' && typeof el.text === 'string') + .map((el: any) => el.text); + candidate = texts.length > 0 ? texts.join('') : null; + } else if (snapVal && typeof snapVal === 'object') { + // 兼容 {text: '...'} 或 {type:'text', text:'...'} + if ((snapVal as any).text && typeof (snapVal as any).text === 'string') { + candidate = (snapVal as any).text; + } else if ((snapVal as any).type === 'text' && typeof (snapVal as any).text === 'string') { + candidate = (snapVal as any).text; + } + } + if (candidate && candidate.trim() !== '') { + snapStr = candidate; + break; + } + } + if (snapStr && snapStr.trim() !== '') { + setIsRestoringSnapshot(true); // 开始快照还原 + const snapshot = JSON.parse(snapStr); + // 恢复页面状态 + if (snapshot.selectedLabels) setSelectedLabels(snapshot.selectedLabels); + // 保留当前模式,不覆写为快照的模式(避免调整模式被还原为生成模式) + if (!mode && snapshot.mode) setMode(snapshot.mode); + // 快照回填的foreign_id/款式/颜色/版本 + if (snapshot.foreignId) setCurrentForeignId(snapshot.foreignId); + if (snapshot.styleText) setCurrentStyleText(snapshot.styleText); + if (snapshot.colorText) setCurrentColorText(snapshot.colorText); + if (snapshot.text2) setCurrentText2(snapshot.text2); + + // 恢复生成模式完整状态(如果存在) + if (snapshot.generationModeState) { + const genState = snapshot.generationModeState; + if (genState.currentForeignId) setCurrentForeignId(genState.currentForeignId); + if (genState.currentStyleText) setCurrentStyleText(genState.currentStyleText); + if (genState.currentColorText) setCurrentColorText(genState.currentColorText); + if (genState.currentText2) setCurrentText2(genState.currentText2); + if (genState.currentVersionNumber !== undefined) setCurrentVersionNumber(genState.currentVersionNumber); + if (genState.recordDetails && Array.isArray(genState.recordDetails)) { + // 恢复记录详情(如果需要的话) + console.log('恢复生成模式记录详情:', genState.recordDetails.length, '条记录'); + } + console.log('恢复生成模式状态:', { + hasSelectedLabels: genState.hasSelectedLabels, + labelSelectionComplete: genState.labelSelectionComplete, + recordCount: genState.recordDetails?.length || 0 + }); + } + + if (snapshot.version !== undefined) { + let vNum: number | null = null; + if (typeof snapshot.version === 'number') { + vNum = snapshot.version; + } else if (typeof snapshot.version === 'string') { + const match = snapshot.version.match(/\d+/); + if (match) { + vNum = parseInt(match[0], 10); + } + } + if (vNum !== null && !isNaN(vNum)) setCurrentVersionNumber(vNum); + } + if (snapshot.timelineAdjustments) setTimelineAdjustments(snapshot.timelineAdjustments); + if (snapshot.expectedDateTimestamp) { + setExpectedDate(new Date(snapshot.expectedDateTimestamp)); + } else if (snapshot.expectedDateString) { + setExpectedDate(new Date(snapshot.expectedDateString)); + } + + // 优先从快照恢复起始时间,如果快照中没有则从当前货期记录中获取 + let startTimeRestored = false; + if (snapshot.startTimestamp) { + setStartTime(new Date(snapshot.startTimestamp)); + startTimeRestored = true; + } else if (snapshot.startString) { + const parsed = new Date(snapshot.startString); + if (!isNaN(parsed.getTime())) { + setStartTime(parsed); + startTimeRestored = true; + } + } + + // 如果快照中没有起始时间信息,则从当前选中的货期记录中获取 + if (!startTimeRestored) { + const startTimeValue = deliveryRecord?.fields?.[DELIVERY_START_TIME_FIELD_ID]; + if (startTimeValue) { + let extractedStartTime: Date | null = null; + if (typeof startTimeValue === 'number') { + extractedStartTime = new Date(startTimeValue); + } else if (Array.isArray(startTimeValue) && startTimeValue.length > 0) { + const timestamp = startTimeValue[0]; + if (typeof timestamp === 'number') { + extractedStartTime = new Date(timestamp); + } + } + + if (extractedStartTime && !isNaN(extractedStartTime.getTime())) { + setStartTime(extractedStartTime); + } + } + } + + if (Array.isArray(snapshot.timelineResults)) { + // 兼容旧版本的完整快照格式 + setTimelineResults(snapshot.timelineResults); + setTimelineVisible(true); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'success', message: '已按快照一模一样还原流程数据' }); + } + setTimelineLoading(false); + setIsRestoringSnapshot(false); // 快照还原完成 + return; // 快照还原完成,退出函数 + } else if (snapshot.isCompleteSnapshot || snapshot.snapshotType === 'complete' || + snapshot.isGlobalSnapshot || snapshot.isCombinedSnapshot) { + // 处理完整快照格式:每个节点都包含完整数据 + console.log('检测到完整快照格式,直接使用快照数据'); + + // 如果是完整快照,直接使用其中的timelineResults + if (snapshot.isCompleteSnapshot && snapshot.timelineResults) { + console.log('使用完整快照中的timelineResults数据'); + setTimelineResults(snapshot.timelineResults); + setTimelineVisible(true); + + // 恢复智能缓冲期状态 + if (snapshot.bufferManagement) { + console.log('恢复智能缓冲期状态:', snapshot.bufferManagement); + } + + // 恢复连锁调整系统状态 + if (snapshot.chainAdjustmentSystem) { + console.log('恢复连锁调整系统状态:', snapshot.chainAdjustmentSystem); + } + + // 恢复生成模式状态 + if (snapshot.generationModeState) { + console.log('恢复生成模式状态:', snapshot.generationModeState); + const genState = snapshot.generationModeState; + + // 恢复foreign_id状态 + if (genState.currentForeignId) { + setCurrentForeignId(genState.currentForeignId); + console.log('恢复foreign_id状态:', genState.currentForeignId); + } + + // 恢复款式和颜色状态 + if (genState.currentStyleText) { + setCurrentStyleText(genState.currentStyleText); + console.log('恢复款式状态:', genState.currentStyleText); + } + if (genState.currentColorText) { + setCurrentColorText(genState.currentColorText); + console.log('恢复颜色状态:', genState.currentColorText); + } + + // 恢复text2状态 + if (genState.currentText2) { + setCurrentText2(genState.currentText2); + console.log('恢复text2状态:', genState.currentText2); + } + + // 恢复版本号状态 + if (genState.currentVersionNumber !== undefined) { + setCurrentVersionNumber(genState.currentVersionNumber); + console.log('恢复版本号状态:', genState.currentVersionNumber); + } + + // 恢复记录详情状态 + if (genState.recordDetails && Array.isArray(genState.recordDetails)) { + setRecordDetails(genState.recordDetails); + console.log('恢复记录详情状态,记录数量:', genState.recordDetails.length); + } + } + + // 恢复时间线计算状态 + if (snapshot.timelineCalculationState) { + console.log('恢复时间线计算状态:', snapshot.timelineCalculationState); + } + + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'success', + message: '已按完整快照还原流程数据' + }); + } + setTimelineLoading(false); + setIsRestoringSnapshot(false); + return; + } + + // 兼容旧版本分散快照格式的处理逻辑 + // 新版本的分散快照格式:需要从所有节点收集数据 + console.log('检测到新版本快照格式,开始收集所有节点的快照数据'); + + try { + // 收集所有节点的快照数据 + const nodeSnapshots: any[] = []; + let globalSnapshotData = snapshot.isCompleteSnapshot ? snapshot : null; + + // 遍历所有记录,收集快照数据 + for (const record of records) { + const fields = record?.fields || {}; + const snapshotField = fields[PROCESS_SNAPSHOT_JSON_FIELD_ID]; + + if (snapshotField) { + let nodeSnapStr = ''; + + // 解析快照字段(支持多种格式) + if (typeof snapshotField === 'string') { + nodeSnapStr = snapshotField; + } else if (Array.isArray(snapshotField)) { + const texts = snapshotField + .filter((el: any) => el && el.type === 'text' && typeof el.text === 'string') + .map((el: any) => el.text); + nodeSnapStr = texts.length > 0 ? texts.join('') : ''; + } else if (snapshotField && typeof snapshotField === 'object') { + if ((snapshotField as any).text && typeof (snapshotField as any).text === 'string') { + nodeSnapStr = (snapshotField as any).text; + } + } + + if (nodeSnapStr && nodeSnapStr.trim() !== '') { + try { + const nodeSnapshot = JSON.parse(nodeSnapStr); + + // 批量模式现在使用扁平化结构,直接从快照中提取全局数据 + if (nodeSnapshot.isCompleteSnapshot && !globalSnapshotData) { + globalSnapshotData = { + version: nodeSnapshot.version, + foreignId: nodeSnapshot.foreignId, + styleText: nodeSnapshot.styleText, + colorText: nodeSnapshot.colorText, + text2: nodeSnapshot.text2, + mode: nodeSnapshot.mode, + selectedLabels: nodeSnapshot.selectedLabels, + expectedDateTimestamp: nodeSnapshot.expectedDateTimestamp, + expectedDateString: nodeSnapshot.expectedDateString, + startTimestamp: nodeSnapshot.startTimestamp, + startString: nodeSnapshot.startString, + timelineAdjustments: nodeSnapshot.timelineAdjustments, + // 恢复智能缓冲期管理状态 + bufferManagement: nodeSnapshot.bufferManagement, + // 恢复连锁调整系统状态 + chainAdjustmentSystem: nodeSnapshot.chainAdjustmentSystem, + // 恢复生成模式状态 + generationModeState: nodeSnapshot.generationModeState, + // 恢复时间线计算状态 + timelineCalculationState: nodeSnapshot.timelineCalculationState, + totalNodes: nodeSnapshot.totalNodes + }; + } + + // 扁平化结构中,每个快照都包含完整的节点数据 + if (nodeSnapshot.isCompleteSnapshot) { + // 确保adjustedTimelineValue有正确的默认值 + const adjustedTimelineValue = nodeSnapshot.adjustedTimelineValue !== undefined ? + nodeSnapshot.adjustedTimelineValue : nodeSnapshot.timelineValue; + + // 处理日期格式,优先使用时间戳格式 + let estimatedStart = nodeSnapshot.estimatedStart; + let estimatedEnd = nodeSnapshot.estimatedEnd; + + // 如果有时间戳格式的日期,使用时间戳重新格式化 + if (nodeSnapshot.estimatedStartTimestamp) { + try { + estimatedStart = formatDate(new Date(nodeSnapshot.estimatedStartTimestamp)); + } catch (error) { + console.warn('时间戳格式开始时间转换失败:', error); + } + } + + if (nodeSnapshot.estimatedEndTimestamp) { + try { + estimatedEnd = formatDate(new Date(nodeSnapshot.estimatedEndTimestamp)); + } catch (error) { + console.warn('时间戳格式结束时间转换失败:', error); + } + } + + // 调试日志 + console.log(`节点 ${nodeSnapshot.nodeName} 快照还原:`, { + timelineValue: nodeSnapshot.timelineValue, + adjustedTimelineValue: adjustedTimelineValue, + originalAdjustedTimelineValue: nodeSnapshot.adjustedTimelineValue, + estimatedStart: estimatedStart, + estimatedEnd: estimatedEnd, + hasTimestamps: { + start: Boolean(nodeSnapshot.estimatedStartTimestamp), + end: Boolean(nodeSnapshot.estimatedEndTimestamp) + }, + nodeCalculationState: nodeSnapshot.nodeCalculationState + }); + + nodeSnapshots.push({ + processOrder: nodeSnapshot.processOrder, + nodeName: nodeSnapshot.nodeName || nodeSnapshot.currentNodeName, + matchedLabels: nodeSnapshot.matchedLabels, + timelineValue: nodeSnapshot.timelineValue, + estimatedStart: estimatedStart, + estimatedEnd: estimatedEnd, + timelineRecordId: nodeSnapshot.timelineRecordId, + allMatchedRecords: nodeSnapshot.allMatchedRecords, + isAccumulated: nodeSnapshot.isAccumulated, + weekendDaysConfig: nodeSnapshot.weekendDaysConfig, + excludedDates: nodeSnapshot.excludedDates, + actualExcludedDates: nodeSnapshot.actualExcludedDates, + actualExcludedDatesCount: nodeSnapshot.actualExcludedDatesCount, + calculationMethod: nodeSnapshot.calculationMethod, + ruleDescription: nodeSnapshot.ruleDescription, + skippedWeekends: nodeSnapshot.skippedWeekends, + actualDays: nodeSnapshot.actualDays, + adjustedTimelineValue: adjustedTimelineValue, + adjustment: nodeSnapshot.adjustment || 0, + adjustmentDescription: nodeSnapshot.adjustmentDescription || '', + startDateRule: nodeSnapshot.startDateRule, + dateAdjustmentRule: nodeSnapshot.dateAdjustmentRule, + // 恢复节点计算状态 + nodeCalculationState: nodeSnapshot.nodeCalculationState, + // 恢复连锁调整节点状态 + chainAdjustmentNode: nodeSnapshot.chainAdjustmentNode + }); + } + } catch (parseError) { + console.warn('解析节点快照失败:', parseError); + } + } + } + } + + // 按流程顺序排序节点快照 + nodeSnapshots.sort((a, b) => (a.processOrder || 0) - (b.processOrder || 0)); + + console.log('收集到的节点快照数量:', nodeSnapshots.length); + console.log('全局快照数据:', globalSnapshotData); + + // 验证数据完整性 + if (globalSnapshotData && globalSnapshotData.totalNodes && + nodeSnapshots.length === globalSnapshotData.totalNodes) { + + // 重组完整的 timelineResults + setTimelineResults(nodeSnapshots); + setTimelineVisible(true); + + // 恢复智能缓冲期状态(如果存在) + if (globalSnapshotData.bufferManagement) { + console.log('恢复智能缓冲期状态:', globalSnapshotData.bufferManagement); + // 这里可以添加额外的状态恢复逻辑,如果需要的话 + } + + // 恢复连锁调整系统状态(如果存在) + if (globalSnapshotData.chainAdjustmentSystem) { + console.log('恢复连锁调整系统状态:', globalSnapshotData.chainAdjustmentSystem); + // 这里可以添加额外的状态恢复逻辑,如果需要的话 + } + + // 恢复生成模式状态(如果存在) + if (globalSnapshotData.generationModeState) { + console.log('恢复生成模式状态:', globalSnapshotData.generationModeState); + const genState = globalSnapshotData.generationModeState; + + // 确保生成模式的关键状态被正确恢复 + if (genState.currentForeignId && !currentForeignId) { + setCurrentForeignId(genState.currentForeignId); + } + if (genState.currentStyleText && !currentStyleText) { + setCurrentStyleText(genState.currentStyleText); + } + if (genState.currentColorText && !currentColorText) { + setCurrentColorText(genState.currentColorText); + } + if (genState.currentText2 && !currentText2) { + setCurrentText2(genState.currentText2); + } + if (genState.currentVersionNumber !== undefined && !currentVersionNumber) { + setCurrentVersionNumber(genState.currentVersionNumber); + } + } + + // 恢复时间线计算状态(如果存在) + if (globalSnapshotData.timelineCalculationState) { + console.log('恢复时间线计算状态:', globalSnapshotData.timelineCalculationState); + } + + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'success', + message: `已从 ${nodeSnapshots.length} 个节点快照还原完整流程数据(包含智能缓冲期和连锁调整状态)` + }); + } + setTimelineLoading(false); + setIsRestoringSnapshot(false); // 快照还原完成 + return; // 快照还原完成,退出函数 + } else { + console.warn('快照数据不完整,降级为基于字段的还原'); + console.log('期望节点数:', globalSnapshotData?.totalNodes, '实际节点数:', nodeSnapshots.length); + } + + } catch (collectError) { + console.warn('收集节点快照数据失败,降级为基于字段的还原:', collectError); + } + } + } + } catch (snapError) { + console.warn('解析快照失败,降级为基于字段的还原:', snapError); + setIsRestoringSnapshot(false); // 快照还原失败,重置标志 + } + + // 如果到达这里,说明没有成功的快照还原,重置标志 + setIsRestoringSnapshot(false); + + const results = records.map((rec: any) => { + const fields = rec?.fields || {}; + const processOrder = fields[PROCESS_ORDER_FIELD_ID_DATA]; + const nodeName = fields[PROCESS_NAME_FIELD_ID]; + const startTs = fields[ESTIMATED_START_DATE_FIELD_ID]; + const endTs = fields[ESTIMATED_END_DATE_FIELD_ID]; + const startDate = typeof startTs === 'number' ? new Date(startTs) : (startTs ? new Date(startTs) : null); + const endDate = typeof endTs === 'number' ? new Date(endTs) : (endTs ? new Date(endTs) : null); + return { + processOrder: typeof processOrder === 'number' ? processOrder : parseInt(processOrder) || undefined, + nodeName: typeof nodeName === 'string' ? nodeName : (nodeName?.text || ''), + estimatedStart: startDate ? format(startDate, DATE_FORMATS.STORAGE_FORMAT) : '未找到时效数据', + estimatedEnd: endDate ? format(endDate, DATE_FORMATS.STORAGE_FORMAT) : '未找到时效数据', + timelineRecordId: rec?.id || rec?.recordId || null, + timelineValue: undefined, + calculationMethod: undefined, + weekendDaysConfig: [], + matchedLabels: [], + skippedWeekends: 0, + actualDays: undefined, + startDateRule: undefined, + dateAdjustmentRule: undefined, + ruleDescription: undefined, + adjustmentDescription: undefined + }; + }); + + // 如果没有快照恢复起始时间,则从当前货期记录中获取起始时间 + const startTimeValue = deliveryRecord?.fields?.[DELIVERY_START_TIME_FIELD_ID]; + if (startTimeValue) { + let extractedStartTime: Date | null = null; + if (typeof startTimeValue === 'number') { + extractedStartTime = new Date(startTimeValue); + } else if (Array.isArray(startTimeValue) && startTimeValue.length > 0) { + const timestamp = startTimeValue[0]; + if (typeof timestamp === 'number') { + extractedStartTime = new Date(timestamp); + } + } + + if (extractedStartTime && !isNaN(extractedStartTime.getTime())) { + setStartTime(extractedStartTime); + } + } + + const sorted = results.sort((a, b) => (a.processOrder || 0) - (b.processOrder || 0)); + setTimelineResults(sorted); + setTimelineVisible(true); + + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'success', message: '已从货期记录还原流程数据' }); + } + } catch (error) { + console.error('从货期记录还原流程数据失败:', error); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'error', message: `还原失败: ${(error as Error).message}` }); + } + } finally { + setTimelineLoading(false); + } + }; + + // 流程数据表相关常量 + const PROCESS_DATA_TABLE_ID = 'tblsJzCxXClj5oK5'; // 流程数据表ID + const FOREIGN_ID_FIELD_ID = 'fld3oxJmpr'; // foreign_id字段 + const PROCESS_NAME_FIELD_ID = 'fldR79qEG3'; // 流程名称字段 + const PROCESS_ORDER_FIELD_ID_DATA = 'fldmND6vjT'; // 流程顺序字段 + const ESTIMATED_START_DATE_FIELD_ID = 'fldlzvHjYP'; // 预计开始日期字段 + const ESTIMATED_END_DATE_FIELD_ID = 'fldaPtY7Jk'; // 预计完成日期字段 + const PROCESS_SNAPSHOT_JSON_FIELD_ID = 'fldSHTxfnC'; // 文本2:用于保存计算页面快照(JSON) + const PROCESS_VERSION_FIELD_ID = 'fldwk5X7Yw'; // 版本字段 + const PROCESS_TIMELINESS_FIELD_ID = 'fldEYCXnWt'; // 时效字段(天) + + // 货期记录表相关常量 + const DELIVERY_RECORD_TABLE_ID = 'tblwiA49gksQrnfg'; // 货期记录表ID + const DELIVERY_FOREIGN_ID_FIELD_ID = 'fld0gAIcHS'; // foreign_id字段(需要替换为实际字段ID) + const DELIVERY_LABELS_FIELD_ID = 'fldp0cDP2T'; // 标签汇总字段(需要替换为实际字段ID) + const DELIVERY_STYLE_FIELD_ID = 'fldJRFxwB1'; // 款式字段(需要替换为实际字段ID) + const DELIVERY_COLOR_FIELD_ID = 'fldhA1uBMy'; // 颜色字段(需要替换为实际字段ID) + const DELIVERY_CREATE_TIME_FIELD_ID = 'fldP4w79LQ'; // 生成时间字段(需要替换为实际字段ID) + const DELIVERY_EXPECTED_DATE_FIELD_ID = 'fldrjlzsxn'; // 预计交付日期字段(需要替换为实际字段ID) + const DELIVERY_NODE_DETAILS_FIELD_ID = 'fldu1KL9yC'; // 节点详情字段(需要替换为实际字段ID) + const DELIVERY_CUSTOMER_EXPECTED_DATE_FIELD_ID = 'fldYNluU8D'; // 客户期望日期字段 + const DELIVERY_ADJUSTMENT_INFO_FIELD_ID = 'fldNc6nNsz'; // 货期调整信息字段(需要替换为实际字段ID) + const DELIVERY_VERSION_FIELD_ID = 'fld5OmvZrn'; // 版本字段(新增) + const DELIVERY_SNAPSHOT_JSON_FIELD_ID = 'fldEYIvHeP'; // 货期记录表:完整快照(JSON) + // 起始时间字段(货期记录表新增) + const DELIVERY_START_TIME_FIELD_ID = 'fld727qCAv'; + // 文本2字段(货期记录表新增) + const DELIVERY_TEXT2_FIELD_ID = 'fldG6LZnmU'; + + // 已移除中国法定节假日相关常量和配置 + + // 这个变量声明也不需要了 + // const nodeNameToOptionId = new Map(); + + // 统一的日期格式化函数 + const formatDate = (date: Date | string, formatType: keyof typeof DATE_FORMATS = 'DISPLAY_WITH_TIME'): string => { + try { + if (!date) return ''; + + const dateObj = typeof date === 'string' ? parseDate(date) : date; + if (!dateObj || isNaN(dateObj.getTime())) { + return ''; + } + + return format(dateObj, DATE_FORMATS[formatType], { locale: zhCN }); + } catch (error) { + console.error('日期格式化失败:', error, { date, formatType }); + return ''; + } + }; + + // 统一的日期解析函数 + const parseDate = (dateStr: string): Date | null => { + try { + if (!dateStr || dateStr.includes('未找到') || dateStr.includes('时效值为0')) { + return null; + } + + // 如果是时间戳格式,直接转换 + if (/^\d{13}$/.test(dateStr)) { + const date = new Date(parseInt(dateStr)); + if (!isNaN(date.getTime())) { + return date; + } + } + + // 移除所有星期信息(支持"星期X"和"周X"格式) + let cleanStr = dateStr + .replace(/\s*星期[一二三四五六日天]\s*/g, ' ') + .replace(/\s*周[一二三四五六日天]\s*/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + + // 处理可能的格式问题:如果日期和时间连在一起了,添加空格 + cleanStr = cleanStr.replace(/(\d{4}-\d{2}-\d{2})(\d{2}:\d{2})/, '$1 $2'); + + // 尝试标准解析 + let date = new Date(cleanStr); + if (!isNaN(date.getTime())) { + return date; + } + + // 手动解析 "YYYY-MM-DD HH:mm" 或 "YYYY-MM-DD HH:mm:ss" 格式 + const match = cleanStr.match(/(\d{4})-(\d{2})-(\d{2})(?:\s+(\d{2}):(\d{2})(?::(\d{2}))?)?/); + if (match) { + const [, year, month, day, hour = '0', minute = '0', second = '0'] = match; + return new Date( + parseInt(year), + parseInt(month) - 1, + parseInt(day), + parseInt(hour), + parseInt(minute), + parseInt(second) + ); + } + + throw new Error(`无法解析日期格式: ${cleanStr}`); + } catch (error) { + console.error('日期解析失败:', { dateStr, error: error.message }); + return null; + } + }; + + // 获取星期几(统一格式) + const getDayOfWeek = (dateStr: string | Date): string => { + try { + const date = typeof dateStr === 'string' ? parseDate(dateStr) : dateStr; + if (!date || isNaN(date.getTime())) { + return ''; + } + return WEEKDAYS[date.getDay()]; + } catch (error) { + console.error('获取星期失败:', error, { dateStr }); + return ''; + } + }; + + // 重构计算实际跨度天数函数 + const calculateActualDays = (startDateStr: string | Date, endDateStr: string | Date): number => { + try { + const startDate = typeof startDateStr === 'string' ? parseDate(startDateStr) : startDateStr; + const endDate = typeof endDateStr === 'string' ? parseDate(endDateStr) : endDateStr; + + if (!startDate || !endDate || isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + console.log('日期解析失败:', { startDateStr, endDateStr, startDate, endDate }); + return 0; + } + + // 计算日期差异(只考虑日期部分) + const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()); + const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()); + + const diffTime = endDateOnly.getTime() - startDateOnly.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + console.log('自然日计算:', { + startDateStr: formatDate(startDate), + endDateStr: formatDate(endDate), + diffDays + }); + + return Math.max(0, diffDays); + } catch (error) { + console.error('计算自然日出错:', error, { startDateStr, endDateStr }); + return 0; + } + }; + + // 计算跳过的周末天数 - 支持空的休息日配置 + const calculateSkippedWeekends = (startDate: Date | string, endDate: Date | string, weekendDays: number[] = []): number => { + if (weekendDays.length === 0) return 0; // 如果没有配置休息日,则不跳过任何天数 + + try { + const start = typeof startDate === 'string' ? new Date(startDate) : startDate; + const end = typeof endDate === 'string' ? new Date(endDate) : endDate; + + if (isNaN(start.getTime()) || isNaN(end.getTime())) { + return 0; + } + + let count = 0; + const current = new Date(start); + + while (current <= end) { + if (weekendDays.includes(current.getDay())) { + count++; + } + current.setDate(current.getDate() + 1); + } + + return count; + } catch (error) { + return 0; + } + }; + + // 计算时间范围内实际跳过的自定义日期 + const calculateExcludedDatesInRange = (startDate: Date | string, endDate: Date | string, excludedDates: string[] = []): { count: number, dates: string[] } => { + if (!excludedDates || excludedDates.length === 0) { + return { count: 0, dates: [] }; + } + + try { + const start = typeof startDate === 'string' ? parseDate(startDate) : startDate; + const end = typeof endDate === 'string' ? parseDate(endDate) : endDate; + + if (!start || !end || isNaN(start.getTime()) || isNaN(end.getTime())) { + return { count: 0, dates: [] }; + } + + const actualExcludedDates: string[] = []; + + for (const excludedDateStr of excludedDates) { + const excludedDate = parseDate(excludedDateStr); + if (excludedDate && excludedDate >= start && excludedDate <= end) { + actualExcludedDates.push(excludedDateStr); + } + } + + return { count: actualExcludedDates.length, dates: actualExcludedDates }; + } catch (error) { + console.error('计算跳过日期失败:', error); + return { count: 0, dates: [] }; + } + }; + + // 已移除法定节假日跳过统计函数 + + // 已移除中国节假日判断函数 + + // 判断是否为自定义周末 - 支持空的休息日配置 + const isCustomWeekend = (date: Date, weekendDays: number[] = []): boolean => { + if (weekendDays.length === 0) return false; // 如果没有配置休息日,则不认为是周末 + return weekendDays.includes(date.getDay()); + }; + + // 判断是否为工作日 - 排除表格休息日、以及节点自定义不参与计算日期 + const isBusinessDay = (date: Date, weekendDays: number[] = [], excludedDates: string[] = []): boolean => { + try { + const dateStr = format(date, 'yyyy-MM-dd'); + const isExcluded = Array.isArray(excludedDates) && excludedDates.includes(dateStr); + return !isCustomWeekend(date, weekendDays) && !isExcluded; + } catch { + return !isCustomWeekend(date, weekendDays); + } + }; + + // 日期调整函数 + const adjustStartDateByRule = (date: Date, ruleJson: string): Date => { + if (!ruleJson || ruleJson.trim() === '') { + return date; + } + + try { + const config = JSON.parse(ruleJson); + const adjustedDate = new Date(date); + const currentDayOfWeek = adjustedDate.getDay(); // 0=周日, 1=周一, ..., 6=周六 + + // 转换为1-7格式(1=周一, 7=周日) + const dayOfWeek = currentDayOfWeek === 0 ? 7 : currentDayOfWeek; + + if (config.rules && Array.isArray(config.rules)) { + for (const rule of config.rules) { + if (rule.condition === 'dayOfWeek' && rule.value === dayOfWeek) { + if (rule.action === 'delayToNextWeek') { + // 计算下周目标日期 + const targetDay = rule.targetDay || 1; // 默认周一 + const daysToAdd = 7 - dayOfWeek + targetDay; + adjustedDate.setDate(adjustedDate.getDate() + daysToAdd); + + console.log(`应用规则: ${rule.description || '未知规则'}, 原日期: ${format(date, 'yyyy-MM-dd')}, 调整后: ${format(adjustedDate, 'yyyy-MM-dd')}`); + break; // 只应用第一个匹配的规则 + } + } + } + } + + return adjustedDate; + } catch (error) { + console.error('解析日期调整规则失败:', error); + return date; // 解析失败时返回原日期 + } + }; + + // JSON格式日期调整函数 - 修改为返回调整结果和描述 + const adjustStartDateByJsonRule = (date: Date, ruleJson: string): { adjustedDate: Date, description?: string } => { + if (!ruleJson || ruleJson.trim() === '') { + return { adjustedDate: date }; + } + + try { + const rules = JSON.parse(ruleJson); + const dayOfWeek = date.getDay(); // 0=周日, 1=周一, ..., 6=周六 + const dayKey = dayOfWeek === 0 ? '7' : dayOfWeek.toString(); // 转换为1-7格式 + + const rule = rules[dayKey]; + if (!rule) { + return { adjustedDate: date }; // 没有匹配的规则,返回原日期 + } + + const adjustedDate = new Date(date); + let description = rule.description || ''; + + switch (rule.action) { + case 'delayInSameWeek': + // 在本周内延期到指定星期几 + const targetDay = rule.targetDay; + const currentDay = dayOfWeek === 0 ? 7 : dayOfWeek; // 转换为1-7格式 + + if (targetDay > currentDay) { + // 延期到本周的目标日期 + adjustedDate.setDate(adjustedDate.getDate() + (targetDay - currentDay)); + } else { + // 如果目标日期已过,延期到下周的目标日期 + adjustedDate.setDate(adjustedDate.getDate() + (7 - currentDay + targetDay)); + } + break; + + case 'delayToNextWeek': + // 延期到下周的指定星期几 + const nextWeekTargetDay = rule.targetDay; + const daysToAdd = 7 - (dayOfWeek === 0 ? 7 : dayOfWeek) + nextWeekTargetDay; + adjustedDate.setDate(adjustedDate.getDate() + daysToAdd); + break; + + case 'delayDays': + // 直接延期指定天数 + const delayDays = rule.days || 0; + adjustedDate.setDate(adjustedDate.getDate() + delayDays); + break; + + default: + console.warn(`未知的调整动作: ${rule.action}`); + break; + } + + console.log(`应用调整规则: ${dayKey} -> ${JSON.stringify(rule)}, 原日期: ${date.toDateString()}, 调整后: ${adjustedDate.toDateString()}`); + return { adjustedDate, description }; + + } catch (error) { + console.error('解析日期调整规则失败:', error, '规则内容:', ruleJson); + return { adjustedDate: date }; // 解析失败时返回原日期 + } + }; + + // 调整到下一个工作时间开始点 + const adjustToNextWorkingHour = (date: Date, calculationMethod: string, weekendDays: number[] = [], excludedDates: string[] = []): Date => { + const result = new Date(date); + + if (calculationMethod === '内部') { + const hour = result.getHours(); + const minute = result.getMinutes(); + + // 如果是工作时间外(18:00之后或9:00之前),调整到下一个工作日的9:00 + if (hour >= 18 || hour < 9) { + // 如果是当天18:00之后,调整到下一个工作日 + if (hour >= 18) { + result.setDate(result.getDate() + 1); + } + + // 找到下一个工作日(考虑休息日和自定义跳过日期) + while (!isBusinessDay(result, weekendDays, excludedDates)) { + result.setDate(result.getDate() + 1); + } + + // 设置为9:00:00 + result.setHours(9, 0, 0, 0); + } + } + + return result; + }; + + // 内部工作时间计算函数 + const addInternalBusinessTime = (startDate: Date, businessDays: number, weekendDays: number[] = [], excludedDates: string[] = []): Date => { + const result = new Date(startDate); + let remainingDays = businessDays; + + // 处理整数天 + const wholeDays = Math.floor(remainingDays); + let addedDays = 0; + + while (addedDays < wholeDays) { + result.setDate(result.getDate() + 1); + if (isBusinessDay(result, weekendDays, excludedDates)) { + addedDays++; + } + } + + // 处理小数部分(按9小时工作制) + const fractionalDays = remainingDays - wholeDays; + if (fractionalDays > 0) { + const workingHoursToAdd = fractionalDays * 9; // 内部按9小时工作制 + let currentHour = result.getHours(); + let currentMinute = result.getMinutes(); + + // 确保在工作时间内开始 + if (currentHour < 9) { + currentHour = 9; + currentMinute = 0; + } else if (currentHour >= 18) { + // 跳到下一个工作日的9:00 + result.setDate(result.getDate() + 1); + while (!isBusinessDay(result, weekendDays, excludedDates)) { + result.setDate(result.getDate() + 1); + } + currentHour = 9; + currentMinute = 0; + } + + // 添加工作小时 + const totalMinutes = currentHour * 60 + currentMinute + workingHoursToAdd * 60; + const finalHour = Math.floor(totalMinutes / 60); + const finalMinute = totalMinutes % 60; + + // 如果超过18:00,需要跨到下一个工作日 + if (finalHour >= 18) { + const overflowHours = finalHour - 18; + const overflowMinutes = finalMinute; + + // 跳到下一个工作日 + result.setDate(result.getDate() + 1); + while (!isBusinessDay(result, weekendDays, excludedDates)) { + result.setDate(result.getDate() + 1); + } + + result.setHours(9 + overflowHours, overflowMinutes, 0, 0); + } else { + result.setHours(finalHour, finalMinute, 0, 0); + } + } + + return result; + }; + + // 添加工作日 - 使用表格配置的休息日与节点自定义跳过日期 + const addBusinessDaysWithHolidays = (startDate: Date, businessDays: number, weekendDays: number[] = [], excludedDates: string[] = []): Date => { + const result = new Date(startDate); + let addedDays = 0; + + // 处理小数天数:先添加整数天,再处理小数部分 + const wholeDays = Math.floor(businessDays); + const fractionalDays = businessDays - wholeDays; + + // 添加整数工作日 + while (addedDays < wholeDays) { + result.setDate(result.getDate() + 1); + if (isBusinessDay(result, weekendDays, excludedDates)) { + addedDays++; + } + } + + // 处理小数部分(转换为小时,按24小时制) + if (fractionalDays > 0) { + const hoursToAdd = fractionalDays * 24; // 1天=24小时 + result.setHours(result.getHours() + hoursToAdd); + } + + return result; + }; + + + + // 获取标签数据 + const fetchLabelOptions = async () => { + setLabelLoading(true); + try { + // 获取标签表 + const labelTable = await bitable.base.getTable(LABEL_TABLE_ID); + + // 1. 先获取所有字段的元数据 + const fieldMetaList = await labelTable.getFieldMetaList(); + + // 2. 筛选出标签1-标签10的字段 + const labelFields: {[key: string]: string} = {}; // 存储字段名到字段ID的映射 + + for (const fieldMeta of fieldMetaList) { + // 检查字段名是否匹配标签1-标签10的模式 + const match = fieldMeta.name.match(/^标签([1-9]|10)$/); + if (match) { + const labelKey = `标签${match[1]}`; + labelFields[labelKey] = fieldMeta.id; + } + } + + console.log('找到的标签字段:', labelFields); + + // 3. 处理标签数据 - 从字段选项获取而不是从记录数据获取 + const options: {[key: string]: any[]} = {}; + + // 初始化十个标签的选项数组 + for (let i = 1; i <= 10; i++) { + options[`标签${i}`] = []; + } + + // 4. 遍历标签字段,获取每个字段的选项 + for (const [labelKey, fieldId] of Object.entries(labelFields)) { + try { + const field = await labelTable.getField(fieldId); + const fieldMeta = await field.getMeta(); + + // 检查是否是选择字段(单选或多选) + if (fieldMeta.type === FieldType.SingleSelect || fieldMeta.type === FieldType.MultiSelect) { + const selectField = field as any; // 类型断言 + const fieldOptions = await selectField.getOptions(); + + // 转换为我们需要的格式 + options[labelKey] = fieldOptions.map((option: any) => ({ + label: option.name, + value: option.name + })); + } + } catch (error) { + console.warn(`获取${labelKey}字段选项失败:`, error); + // 如果获取选项失败,保持空数组 + options[labelKey] = []; + } + } + + console.log('处理后的标签选项:', options); + + setLabelOptions(options); + + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'success', + message: `标签选项加载成功,共找到 ${Object.keys(labelFields).length} 个标签字段` + }); + } + } catch (error) { + console.error('获取标签选项失败:', error); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'error', + message: '获取标签选项失败,请检查表格配置' + }); + } + } finally { + setLabelLoading(false); + } + }; + + // 组件加载时获取标签数据和初始化起始时间 + useEffect(() => { + const initializeData = async () => { + await fetchLabelOptions(); + await initializeStartTime(); + await loadAvailableTables(); // 加载可用表列表 + }; + + initializeData(); + }, []); + + // 初始化起始时间:从货期记录表获取,新记录则使用当前时间 + const initializeStartTime = async () => { + try { + const selection = await bitable.base.getSelection(); + const recordId = selection?.recordId || ''; + const tableId = selection?.tableId || ''; + + if (recordId && tableId === DELIVERY_RECORD_TABLE_ID) { + // 如果选中的是货期记录表的记录,从中获取起始时间 + const deliveryTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID); + const deliveryRecord = await deliveryTable.getRecordById(recordId); + const startTimeValue = deliveryRecord?.fields?.[DELIVERY_START_TIME_FIELD_ID]; + + if (startTimeValue) { + let extractedStartTime: Date | null = null; + if (typeof startTimeValue === 'number') { + extractedStartTime = new Date(startTimeValue); + } else if (Array.isArray(startTimeValue) && startTimeValue.length > 0) { + const timestamp = startTimeValue[0]; + if (typeof timestamp === 'number') { + extractedStartTime = new Date(timestamp); + } + } + + if (extractedStartTime && !isNaN(extractedStartTime.getTime())) { + setStartTime(extractedStartTime); + return; + } + } + } + + // 如果没有找到有效的起始时间,使用当前时间 + setStartTime(new Date()); + } catch (error) { + console.error('初始化起始时间失败:', error); + // 出错时使用当前时间作为默认值 + setStartTime(new Date()); + } + }; + + // 加载可用表列表 + const loadAvailableTables = async () => { + try { + setTablesLoading(true); + const tableMetaList = await bitable.base.getTableMetaList(); + const tables = tableMetaList.map(table => ({ + id: table.id, + name: table.name + })); + setAvailableTables(tables); + } catch (error) { + console.error('加载表列表失败:', error); + } finally { + setTablesLoading(false); + } + }; + + // 加载指定表的视图列表 + const loadAvailableViews = async (tableId: string) => { + if (!tableId) { + setAvailableViews([]); + return; + } + + try { + setViewsLoading(true); + const table = await bitable.base.getTable(tableId); + const viewMetaList = await table.getViewMetaList(); + const views = viewMetaList.map(view => ({ + id: view.id, + name: view.name + })); + setAvailableViews(views); + } catch (error) { + console.error('加载视图列表失败:', error); + setAvailableViews([]); + } finally { + setViewsLoading(false); + } + }; + + // 处理表选择变化 + const handleBatchTableChange = (tableId: string) => { + // 切换批量来源表时重置全局变量,避免旧状态残留 + resetGlobalState(); + setSelectedBatchTableId(tableId); + setSelectedBatchViewId(''); // 重置视图选择 + loadAvailableViews(tableId); // 加载新表的视图列表 + }; + + // 处理标签选择变化 + const handleLabelChange = (labelKey: string, value: string | string[]) => { + setSelectedLabels(prev => ({ + ...prev, + [labelKey]: value + })); + }; + + // 清空标签选择 + const handleClearLabels = () => { + setSelectedLabels({}); + setExpectedDate(null); // 清空客户期望日期 + }; + + // 计算预计开始和完成时间 + const handleCalculateTimeline = async ( + skipValidation: boolean = false, + overrideData?: { + selectedRecords?: string[], + recordDetails?: any[], + selectedLabels?: {[key: string]: string | string[]}, + expectedDate?: Date | null, + startTime?: Date | null + }, + showUI: boolean = true // 新增参数控制是否显示UI + ) => { + // 使用传入的数据或全局状态 + const currentSelectedRecords = overrideData?.selectedRecords || selectedRecords; + const currentRecordDetails = overrideData?.recordDetails || recordDetails; + const currentSelectedLabels = overrideData?.selectedLabels || selectedLabels; + const currentExpectedDate = overrideData?.expectedDate || expectedDate; + const currentStartTime = overrideData?.startTime || startTime; + + console.log('=== handleCalculateTimeline - 使用的数据 ==='); + console.log('currentSelectedRecords:', currentSelectedRecords); + console.log('currentSelectedLabels:', currentSelectedLabels); + console.log('currentExpectedDate:', currentExpectedDate); + console.log('currentStartTime:', currentStartTime); + + // 跳过验证(用于批量模式) + if (!skipValidation) { + // 检查是否选择了多条记录 + if (currentSelectedRecords.length > 1) { + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'warning', + message: '计算时效功能仅支持单条记录,请重新选择单条记录后再试' + }); + } + return; + } + + // 检查是否选择了记录 + if (currentSelectedRecords.length === 0) { + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'warning', + message: '请先选择一条记录' + }); + } + return; + } + + // 检查是否选择了标签 + const hasSelectedLabels = Object.values(currentSelectedLabels).some(value => { + return Array.isArray(value) ? value.length > 0 : Boolean(value); + }); + + if (!hasSelectedLabels) { + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'warning', + message: '请先选择至少一个标签' + }); + } + return; + } + + // 可选:检查是否选择了客户期望日期 + if (!currentExpectedDate) { + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'info', + message: '建议选择客户期望日期以便更好地进行时效计算' + }); + } + } + } + + // 移除冗余日志:客户期望日期输出 + setTimelineLoading(true); + // 生成模式:输出当前数据结构,便于与批量模式对比 + try { + console.group('=== 生成模式:计算时效 - 当前数据结构 ==='); + // 模式与核心输入 + console.log('mode:', mode); + console.log('selectedRecords:', currentSelectedRecords); + console.log('recordDetails:', currentRecordDetails); + console.log('selectedLabels:', currentSelectedLabels); + console.log('expectedDate:', currentExpectedDate); + console.groupEnd(); + } catch (logErr) { + console.warn('生成模式结构化日志输出失败:', logErr); + } + try { + // 构建业务选择的所有标签值集合(用于快速查找) + const businessLabelValues = new Set(); + for (const [labelKey, selectedValue] of Object.entries(currentSelectedLabels)) { + if (selectedValue) { + const values = Array.isArray(selectedValue) ? selectedValue : [selectedValue]; + values.forEach(value => { + if (typeof value === 'string' && value.trim()) { + businessLabelValues.add(value.trim()); + } + }); + } + } + + // 已移除冗余日志:业务选择标签值 + const timelineTable = await bitable.base.getTable(TIMELINE_TABLE_ID); + const timelineFieldMetaList = await timelineTable.getFieldMetaList(); + const timelineLabelFields: {[key: string]: string} = {}; + + // 构建时效表的标签字段映射(只执行一次) + let relationshipFieldId = ''; // 关系字段ID + let calculationMethodFieldId = ''; // 时效计算方式字段ID + + for (const fieldMeta of timelineFieldMetaList) { + const match = fieldMeta.name.match(/^标签([1-9]|10)$/); + if (match) { + const labelKey = `标签${match[1]}`; + timelineLabelFields[labelKey] = fieldMeta.id; + } + // 查找关系字段 + if (fieldMeta.name === '关系' || fieldMeta.id === 'fldaIDAhab') { + relationshipFieldId = fieldMeta.id; + } + // 查找时效计算方式字段 + if (fieldMeta.name === '时效计算方式' || fieldMeta.id === 'fldxfLZNUu') { + calculationMethodFieldId = fieldMeta.id; + } + } + + console.log('时效表标签字段映射:', timelineLabelFields); + console.log('关系字段ID:', relationshipFieldId); + + // 1. 先获取匹配的流程节点(复用预览功能的逻辑) + const processTable = await bitable.base.getTable(PROCESS_CONFIG_TABLE_ID); + const processRecordsResult = await processTable.getRecords({ + pageSize: 5000 + }); + + const processRecords = processRecordsResult.records || []; + const matchedProcessNodes: any[] = []; + + // 匹配流程配置节点 + for (const record of processRecords) { + const fields = record.fields; + const processLabels = fields[PROCESS_LABEL_FIELD_ID]; + const nodeName = fields[NODE_NAME_FIELD_ID]; + const processOrder = fields[PROCESS_ORDER_FIELD_ID]; // 获取流程顺序 + + if (!processLabels || !nodeName) continue; + + // 处理流程配置表中的标签数据 - 修复多选字段处理 + let processLabelTexts: string[] = []; + if (typeof processLabels === 'string') { + processLabelTexts = [processLabels]; + } else if (processLabels && processLabels.text) { + processLabelTexts = [processLabels.text]; + } else if (Array.isArray(processLabels) && processLabels.length > 0) { + // 处理多选字段,获取所有选项的值 + processLabelTexts = processLabels.map(item => { + if (typeof item === 'string') { + return item; + } else if (item && item.text) { + return item.text; + } else if (item && item.value) { + return item.value; + } + return String(item); + }).filter(text => text && text.trim()); + } + + // 处理节点名称 + let nodeNameText = ''; + if (typeof nodeName === 'string') { + nodeNameText = nodeName; + } else if (nodeName.text) { + nodeNameText = nodeName.text; + } + + // 处理流程顺序 + let orderValue = 0; + if (typeof processOrder === 'number') { + orderValue = processOrder; + } else if (typeof processOrder === 'string') { + orderValue = parseInt(processOrder) || 0; + } else if (processOrder && processOrder.value !== undefined) { + orderValue = processOrder.value; + } + + if (processLabelTexts.length === 0 || !nodeNameText) continue; + + // 检查是否匹配当前选择的标签 - 修复匹配逻辑 + let isMatched = false; + const matchedLabels: string[] = []; + + for (const [labelKey, labelValue] of Object.entries(currentSelectedLabels)) { + if (!labelValue) continue; + + const valuesToCheck = Array.isArray(labelValue) ? labelValue : [labelValue]; + + for (const value of valuesToCheck) { + // 检查用户选择的值是否在流程配置的任何一个标签选项中 + const isValueMatched = processLabelTexts.some(processLabelText => { + // 直接匹配 + if (processLabelText === value) { + return true; + } + // 包含匹配(用于处理可能的格式差异) + if (processLabelText.includes(value)) { + return true; + } + return false; + }); + + if (isValueMatched) { + isMatched = true; + matchedLabels.push(`${labelKey}: ${value}`); + console.log(`匹配成功: ${labelKey} = ${value}`); + } else { + console.log(`匹配失败: ${labelKey} = ${value}, 流程配置标签: [${processLabelTexts.join(', ')}]`); + } + } + } + + // 处理休息日配置 - 完全从表格字段获取 + let weekendDays: number[] = []; + const weekendDaysField = fields[WEEKEND_DAYS_FIELD_ID]; // 获取休息日配置 + if (weekendDaysField) { + console.log('原始休息日字段数据:', weekendDaysField); + + if (Array.isArray(weekendDaysField)) { + // 多选字段返回数组,每个元素可能是选项对象 + weekendDays = weekendDaysField.map(item => { + // 处理选项对象 {id: "xxx", text: "0"} 或直接的值 + if (item && typeof item === 'object') { + // 如果是选项对象,取text或id字段 + const value = item.text || item.id || item.value; + if (typeof value === 'string') { + const parsed = parseInt(value); + return !isNaN(parsed) && parsed >= 0 && parsed <= 6 ? parsed : null; + } else if (typeof value === 'number') { + return value >= 0 && value <= 6 ? value : null; + } + } else if (typeof item === 'string') { + const parsed = parseInt(item); + return !isNaN(parsed) && parsed >= 0 && parsed <= 6 ? parsed : null; + } else if (typeof item === 'number') { + return item >= 0 && item <= 6 ? item : null; + } + return null; + }).filter(day => day !== null) as number[]; + } else if (typeof weekendDaysField === 'string') { + // 如果是字符串,尝试解析 + try { + const parsed = JSON.parse(weekendDaysField); + if (Array.isArray(parsed)) { + weekendDays = parsed.filter(day => typeof day === 'number' && day >= 0 && day <= 6); + } + } catch { + // 如果解析失败,尝试按逗号分割 + const parts = weekendDaysField.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n) && n >= 0 && n <= 6); + if (parts.length > 0) { + weekendDays = parts; + } + } + } else if (typeof weekendDaysField === 'number' && weekendDaysField >= 0 && weekendDaysField <= 6) { + weekendDays = [weekendDaysField]; + } + + console.log('解析后的休息日配置:', weekendDays); + } + // 如果表格中没有配置休息日或配置为空,则该节点没有固定休息日 + // 这样就完全依赖表格数据,不会有任何硬编码的默认值 + + // 处理不参与计算日期(自定义跳过日期) + let excludedDates: string[] = []; + const excludedDatesField = fields[EXCLUDED_DATES_FIELD_ID]; + if (excludedDatesField) { + console.log('原始不参与计算日期字段数据:', excludedDatesField); + if (Array.isArray(excludedDatesField)) { + excludedDates = excludedDatesField.map(item => { + if (item && typeof item === 'object') { + const val = item.text || item.name || item.value || item.id || ''; + return String(val).trim(); + } + return String(item).trim(); + }).filter(d => !!d); + } else if (typeof excludedDatesField === 'string') { + excludedDates = excludedDatesField.split(/[\,\s]+/).map(s => s.trim()).filter(Boolean); + } else if (excludedDatesField && typeof excludedDatesField === 'object' && excludedDatesField.text) { + excludedDates = [String(excludedDatesField.text).trim()].filter(Boolean); + } + console.log('解析后的不参与计算日期:', excludedDates); + } + + if (isMatched) { + // 获取起始日期调整规则 + const startDateRule = fields[START_DATE_RULE_FIELD_ID]; + // 获取JSON格式的日期调整规则 + const dateAdjustmentRule = fields[DATE_ADJUSTMENT_RULE_FIELD_ID]; + + matchedProcessNodes.push({ + id: record.id, + nodeName: nodeNameText, + processLabels: processLabelTexts.join(', '), + matchedLabels: matchedLabels, + processOrder: orderValue, // 添加流程顺序 + weekendDays: weekendDays, // 添加休息日配置 + excludedDates: excludedDates, // 添加自定义跳过日期 + startDateRule: startDateRule, // 添加起始日期调整规则 + dateAdjustmentRule: dateAdjustmentRule // 添加JSON格式日期调整规则 + }); + } + } + + // 按流程顺序排序 + matchedProcessNodes.sort((a, b) => a.processOrder - b.processOrder); + + console.log('按顺序排列的流程节点:', matchedProcessNodes); + + // 2. 优化:预先获取所有时效数据并建立索引 + const timelineRecordsResult = await timelineTable.getRecords({ + pageSize: 5000 + }); + + const timelineRecords = timelineRecordsResult.records || []; + + // 优化2:预处理时效数据,建立节点名称到记录的映射 + const timelineIndexByNode = new Map(); + + for (const timelineRecord of timelineRecords) { + const timelineFields = timelineRecord.fields; + const timelineNodeName = timelineFields[TIMELINE_NODE_FIELD_ID]; + + // 处理时效表中的节点名称 + let timelineNodeNames: string[] = []; + + if (typeof timelineNodeName === 'string') { + timelineNodeNames = timelineNodeName.split(',').map(name => name.trim()); + } else if (Array.isArray(timelineNodeName)) { + timelineNodeNames = timelineNodeName.map(item => { + if (typeof item === 'string') { + return item.trim(); + } else if (item && item.text) { + return item.text.trim(); + } + return ''; + }).filter(name => name); + } else if (timelineNodeName && timelineNodeName.text) { + timelineNodeNames = timelineNodeName.text.split(',').map(name => name.trim()); + } + + // 为每个节点名称建立索引 + for (const nodeName of timelineNodeNames) { + const normalizedName = nodeName.toLowerCase(); + if (!timelineIndexByNode.has(normalizedName)) { + timelineIndexByNode.set(normalizedName, []); + } + timelineIndexByNode.get(normalizedName)!.push({ + record: timelineRecord, + fields: timelineFields + }); + } + } + + console.log('时效数据索引构建完成,节点数量:', timelineIndexByNode.size); + + const results: any[] = []; + + // 3. 按顺序为每个匹配的流程节点查找时效数据并计算累积时间 + let cumulativeStartTime = currentStartTime ? new Date(currentStartTime) : new Date(); // 累积开始时间 + + for (let i = 0; i < matchedProcessNodes.length; i++) { + const processNode = matchedProcessNodes[i]; + let timelineValue = null; + let matchedTimelineRecord = null; + + // 优化3:使用索引快速查找候选记录 + const normalizedProcessName = processNode.nodeName.trim().toLowerCase(); + const candidateRecords = timelineIndexByNode.get(normalizedProcessName) || []; + + console.log(`节点 ${processNode.nodeName} 找到 ${candidateRecords.length} 个候选时效记录`); + + // 在候选记录中进行标签匹配 + let matchedCandidates = []; // 收集所有匹配的候选记录 + + for (const candidate of candidateRecords) { + const { record: timelineRecord, fields: timelineFields } = candidate; + + // 优化4:使用预构建的字段映射进行标签匹配 + let isLabelsMatched = true; + + // 检查时效表中每个有值的标签是否都包含在业务标签中 + for (const [labelKey, timelineFieldId] of Object.entries(timelineLabelFields)) { + const timelineLabelValue = timelineFields[timelineFieldId]; + + // 处理时效表中的标签值 + let timelineLabelTexts: string[] = []; + + if (typeof timelineLabelValue === 'string' && timelineLabelValue.trim()) { + timelineLabelTexts = [timelineLabelValue.trim()]; + } else if (timelineLabelValue && timelineLabelValue.text && timelineLabelValue.text.trim()) { + timelineLabelTexts = [timelineLabelValue.text.trim()]; + } else if (Array.isArray(timelineLabelValue) && timelineLabelValue.length > 0) { + // 多选字段,获取所有值 + timelineLabelTexts = timelineLabelValue.map(item => { + if (typeof item === 'string') return item.trim(); + if (item && item.text) return item.text.trim(); + return ''; + }).filter(text => text); + } + + // 如果时效表中该标签有值,则检查该标签的所有值是否都包含在业务标签中 + if (timelineLabelTexts.length > 0) { + let allValuesMatched = true; + + // 优化5:使用Set的has方法进行快速查找 + for (const timelineText of timelineLabelTexts) { + if (!businessLabelValues.has(timelineText)) { + allValuesMatched = false; + console.log(`时效表标签 ${labelKey} 的值 "${timelineText}" 不在业务选择的标签中`); + break; + } + } + + if (!allValuesMatched) { + console.log(`时效表标签 ${labelKey} 的值 [${timelineLabelTexts.join(', ')}] 不完全包含在业务选择的标签中`); + isLabelsMatched = false; + break; + } else { + console.log(`时效表标签 ${labelKey} 的值 [${timelineLabelTexts.join(', ')}] 完全匹配成功`); + } + } + // 如果时效表中该标签为空,则跳过检查(空标签不影响匹配) + } + + // 只有标签匹配成功才获取时效数据 + if (isLabelsMatched) { + // 找到匹配的节点,获取时效值 + const timelineValueField = timelineFields[TIMELINE_FIELD_ID]; + // 获取关系字段值 + const relationshipField = timelineFields[relationshipFieldId]; + // 获取时效计算方式字段值 + const calculationMethodField = timelineFields[calculationMethodFieldId]; + + if (timelineValueField !== null && timelineValueField !== undefined) { + let candidateTimelineValue = 0; + + // 解析时效值 + if (typeof timelineValueField === 'number') { + candidateTimelineValue = timelineValueField; + } else if (typeof timelineValueField === 'string') { + const parsedValue = parseFloat(timelineValueField); + candidateTimelineValue = isNaN(parsedValue) ? 0 : parsedValue; + } else if (timelineValueField && typeof timelineValueField === 'object' && timelineValueField.value !== undefined) { + candidateTimelineValue = timelineValueField.value; + } + + // 获取计算方式 + let calculationMethod = '外部'; // 默认值 + if (calculationMethodField) { + if (typeof calculationMethodField === 'string') { + calculationMethod = calculationMethodField; + } else if (calculationMethodField && typeof calculationMethodField === 'object' && calculationMethodField.text) { + calculationMethod = calculationMethodField.text; + } + } + + // 根据计算方式转换时效值(小时转天) + let convertedTimelineValue = 0; + if (calculationMethod === '内部') { + convertedTimelineValue = Math.round((candidateTimelineValue / 9) * 1000) / 1000; // 精确到3位小数 + console.log(`内部计算方式: ${candidateTimelineValue}小时 → ${convertedTimelineValue.toFixed(3)}天`); + } else { + convertedTimelineValue = Math.round((candidateTimelineValue / 24) * 1000) / 1000; // 精确到3位小数 + console.log(`外部计算方式: ${candidateTimelineValue}小时 → ${convertedTimelineValue.toFixed(3)}天`); + } + + // 获取关系类型 + let relationshipType = '默认'; + if (relationshipField) { + if (typeof relationshipField === 'string') { + relationshipType = relationshipField; + } else if (relationshipField && typeof relationshipField === 'object' && relationshipField.text) { + relationshipType = relationshipField.text; + } + } + + // 调试时效记录对象结构 + console.log('时效记录对象结构:', timelineRecord); + console.log('记录ID字段:', { + id: timelineRecord.id, + recordId: timelineRecord.recordId, + _id: timelineRecord._id, + record_id: timelineRecord.record_id + }); + + matchedCandidates.push({ + record: timelineRecord, + timelineValue: convertedTimelineValue, // 使用转换后的值 + relationshipType: relationshipType, + calculationMethod: calculationMethod, // 记录计算方式 + originalHours: candidateTimelineValue // 保留原始小时值用于日志 + }); + + // 移除单条记录的详细日志输出,避免日志冗余 + // console.log(`节点 ${processNode.nodeName} 找到匹配记录:`, { + // 记录ID: timelineRecord.id || timelineRecord.recordId || timelineRecord._id || timelineRecord.record_id, + // 原始时效值小时: candidateTimelineValue, + // 转换后天数: convertedTimelineValue, + // 关系类型: relationshipType, + // 时效计算方式: calculationMethod + // }); + } + } + } + + // 在 matchedCandidates 处理之前定义 processingRule + let processingRule = '默认'; // 默认值 + + // 根据关系字段和时效计算方式决定如何处理多个匹配的记录 + if (matchedCandidates.length > 0) { + // 检查所有匹配记录的关系类型和计算方式 + const relationshipTypes = [...new Set(matchedCandidates.map(c => c.relationshipType))]; + const calculationMethods = [...new Set(matchedCandidates.map(c => c.calculationMethod))]; + + if (relationshipTypes.length > 1) { + console.warn(`节点 ${processNode.nodeName} 存在多种关系类型:`, relationshipTypes); + } + if (calculationMethods.length > 1) { + console.warn(`节点 ${processNode.nodeName} 存在多种时效计算方式:`, calculationMethods); + } + + // 使用第一个匹配记录的关系类型和计算方式作为处理方式 + const primaryRelationshipType = matchedCandidates[0].relationshipType; + const primaryCalculationMethod = matchedCandidates[0].calculationMethod; + + // 添加调试日志 + console.log(`节点 ${processNode.nodeName} 调试信息:`); + console.log('primaryCalculationMethod:', primaryCalculationMethod); + console.log('primaryRelationshipType:', primaryRelationshipType); + console.log('所有关系类型:', relationshipTypes); + console.log('所有计算方式:', calculationMethods); + + let finalTimelineValue = 0; + + // 使用关系字段决定处理方式(累加值、最大值、默认) + processingRule = primaryRelationshipType || '默认'; + + console.log('processingRule:', processingRule); + + if (processingRule === '累加值') { + finalTimelineValue = matchedCandidates.reduce((sum, candidate) => sum + candidate.timelineValue, 0); + const totalOriginalHours = matchedCandidates.reduce((sum, candidate) => sum + candidate.originalHours, 0); + + console.log(`节点 ${processNode.nodeName} 累加值处理 - 找到 ${matchedCandidates.length} 条匹配记录:`); + matchedCandidates.forEach((candidate, index) => { + console.log(` 记录${index + 1}: ID=${candidate.record.id || candidate.record.recordId}, 时效=${candidate.originalHours}小时(${candidate.timelineValue}天), 计算方式=${candidate.calculationMethod}`); + }); + console.log(`累加结果: 总计${totalOriginalHours}小时 → ${finalTimelineValue}天`); + + matchedTimelineRecord = matchedCandidates[0].record; + } else if (processingRule === '最大值') { + finalTimelineValue = Math.max(...matchedCandidates.map(c => c.timelineValue)); + const maxCandidate = matchedCandidates.find(c => c.timelineValue === finalTimelineValue); + + console.log(`节点 ${processNode.nodeName} 最大值处理 - 找到 ${matchedCandidates.length} 条匹配记录:`); + matchedCandidates.forEach((candidate, index) => { + console.log(` 记录${index + 1}: ID=${candidate.record.id || candidate.record.recordId}, 时效=${candidate.originalHours}小时(${candidate.timelineValue}天), 计算方式=${candidate.calculationMethod}`); + }); + console.log(`最大值结果: ${maxCandidate?.originalHours}小时(${maxCandidate?.calculationMethod}) → ${finalTimelineValue}天`); + + matchedTimelineRecord = maxCandidate?.record || matchedCandidates[0].record; + } else { + finalTimelineValue = matchedCandidates[0].timelineValue; + + console.log(`节点 ${processNode.nodeName} 默认处理 - 找到 ${matchedCandidates.length} 条匹配记录:`); + matchedCandidates.forEach((candidate, index) => { + console.log(` 记录${index + 1}: ID=${candidate.record.id || candidate.record.recordId}, 时效=${candidate.originalHours}小时(${candidate.timelineValue}天), 计算方式=${candidate.calculationMethod}`); + }); + console.log(`默认结果: 使用第一条记录 ${matchedCandidates[0].originalHours}小时(${matchedCandidates[0].calculationMethod}) → ${finalTimelineValue}天`); + + matchedTimelineRecord = matchedCandidates[0].record; + } + + timelineValue = finalTimelineValue; + // matchedTimelineRecord 已在上面的处理逻辑中设置 + } + + + + // 计算当前节点的开始和完成时间(使用工作日计算) + const calculateTimeline = (startDate: Date, timelineValue: number, calculationMethod: string = '外部') => { + // 根据计算方式调整开始时间 + const adjustedStartDate = adjustToNextWorkingHour(startDate, calculationMethod, processNode.weekendDays, processNode.excludedDates || []); + + let endDate: Date; + if (calculationMethod === '内部') { + // 使用内部工作时间计算 + endDate = addInternalBusinessTime(adjustedStartDate, timelineValue, processNode.weekendDays, processNode.excludedDates || []); + } else { + // 使用原有的24小时制计算 + endDate = addBusinessDaysWithHolidays(adjustedStartDate, timelineValue, processNode.weekendDays, processNode.excludedDates || []); + } + + return { + startDate: formatDate(adjustedStartDate, 'STORAGE_FORMAT'), + endDate: formatDate(endDate, 'STORAGE_FORMAT') + }; + }; + + let nodeStartTime = new Date(cumulativeStartTime); + + // 应用起始日期调整规则 + if (processNode.startDateRule) { + let ruleJson = ''; + if (typeof processNode.startDateRule === 'string') { + ruleJson = processNode.startDateRule; + } else if (processNode.startDateRule && processNode.startDateRule.text) { + ruleJson = processNode.startDateRule.text; + } + + if (ruleJson.trim()) { + nodeStartTime = adjustStartDateByRule(nodeStartTime, ruleJson); + console.log(`节点 ${processNode.nodeName} 应用起始日期调整规则后的开始时间:`, formatDate(nodeStartTime)); + } + } + + // 应用JSON格式日期调整规则 + let ruleDescription = ''; + if (processNode.dateAdjustmentRule) { + console.log('原始dateAdjustmentRule:', processNode.dateAdjustmentRule); + let ruleText = ''; + if (typeof processNode.dateAdjustmentRule === 'string') { + ruleText = processNode.dateAdjustmentRule; + } else if (Array.isArray(processNode.dateAdjustmentRule)) { + // 处理富文本数组格式 + ruleText = processNode.dateAdjustmentRule + .filter(item => item.type === 'text') + .map(item => item.text) + .join(''); + } else if (processNode.dateAdjustmentRule && processNode.dateAdjustmentRule.text) { + ruleText = processNode.dateAdjustmentRule.text; + } + + console.log('解析后的ruleText:', ruleText); + console.log('调整前的nodeStartTime:', formatDate(nodeStartTime)); + + if (ruleText && ruleText.trim() !== '') { + const result = adjustStartDateByJsonRule(nodeStartTime, ruleText); + nodeStartTime = result.adjustedDate; + ruleDescription = result.description || ''; + console.log(`节点 ${processNode.nodeName} 应用JSON日期调整规则后的开始时间:`, formatDate(nodeStartTime)); + } + } + + // 获取当前节点的计算方式 + let nodeCalculationMethod = '外部'; // 默认值 + if (matchedCandidates.length > 0) { + nodeCalculationMethod = matchedCandidates[0].calculationMethod; + } else if (matchedTimelineRecord) { + const calculationMethodField = matchedTimelineRecord.fields[calculationMethodFieldId]; + if (calculationMethodField) { + if (typeof calculationMethodField === 'string') { + nodeCalculationMethod = calculationMethodField; + } else if (calculationMethodField && typeof calculationMethodField === 'object' && calculationMethodField.text) { + nodeCalculationMethod = calculationMethodField.text; + } + } + } + + const timelineResult = timelineValue ? + calculateTimeline(nodeStartTime, timelineValue, nodeCalculationMethod) : + { + startDate: formatDate(nodeStartTime, 'STORAGE_FORMAT'), + endDate: '未找到时效数据' + }; + + let nodeEndTime: Date; + if (timelineValue) { + const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, processNode.weekendDays, processNode.excludedDates || []); + if (nodeCalculationMethod === '内部') { + nodeEndTime = addInternalBusinessTime(adjustedStartTime, timelineValue, processNode.weekendDays, processNode.excludedDates || []); + } else { + nodeEndTime = addBusinessDaysWithHolidays(adjustedStartTime, timelineValue, processNode.weekendDays, processNode.excludedDates || []); + } + } else { + nodeEndTime = new Date(nodeStartTime); + } + + // 计算跳过的天数 + const skippedWeekends = calculateSkippedWeekends(nodeStartTime, nodeEndTime, processNode.weekendDays); + const actualDays = calculateActualDays(timelineResult.startDate, timelineResult.endDate); + + // 计算时间范围内实际跳过的自定义日期 + const excludedDatesInRange = calculateExcludedDatesInRange(nodeStartTime, nodeEndTime, processNode.excludedDates || []); + + results.push({ + processOrder: processNode.processOrder, + nodeName: processNode.nodeName, + matchedLabels: processNode.matchedLabels, + timelineValue: timelineValue, + estimatedStart: timelineResult.startDate, + estimatedEnd: timelineResult.endDate, + timelineRecordId: matchedTimelineRecord ? + (matchedTimelineRecord.id || matchedTimelineRecord.recordId || matchedTimelineRecord._id || matchedTimelineRecord.record_id) : null, + // 新增:保存所有匹配记录的信息(用于累加情况) + allMatchedRecords: matchedCandidates.length > 1 ? matchedCandidates.map(candidate => ({ + recordId: candidate.record.id || candidate.record.recordId || candidate.record._id || candidate.record.record_id, + timelineValue: candidate.timelineValue, + originalHours: candidate.originalHours, + calculationMethod: candidate.calculationMethod, + relationshipType: candidate.relationshipType + })) : null, + // 新增:标识是否为累加处理 + isAccumulated: processingRule === '累加值' && matchedCandidates.length > 1, + weekendDaysConfig: processNode.weekendDays, // 新增:保存休息日配置用于显示 + excludedDates: processNode.excludedDates || [], // 新增:保存不参与计算日期用于显示与快照 + // 新增:保存时间范围内实际跳过的日期 + actualExcludedDates: excludedDatesInRange.dates, + actualExcludedDatesCount: excludedDatesInRange.count, + calculationMethod: nodeCalculationMethod, // 新增:保存计算方式 + ruleDescription: ruleDescription, // 新增:保存规则描述 + skippedWeekends: skippedWeekends, + actualDays: actualDays, + // 新增:保存调整规则用于重新计算 + startDateRule: processNode.startDateRule, + dateAdjustmentRule: processNode.dateAdjustmentRule, + adjustmentDescription: ruleDescription // 新增:保存调整规则描述 + }); + + // 更新累积时间:当前节点的完成时间成为下一个节点的开始时间 + if (timelineValue) { + cumulativeStartTime = new Date(nodeEndTime); + } + + console.log(`节点 ${processNode.nodeName} (顺序: ${processNode.processOrder}):`, { + 开始时间: formatDate(nodeStartTime), + 完成时间: formatDate(nodeEndTime), + 时效天数: timelineValue, + 计算方式: nodeCalculationMethod + }); + } + + setTimelineResults(results); + if (showUI) { + setTimelineVisible(true); + } + + console.log('按流程顺序计算的时效结果:', results); + + return results; // 返回计算结果 + + } catch (error) { + console.error('计算时效失败:', error); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'error', + message: '计算时效失败,请检查表格配置' + }); + } + throw error; // 重新抛出错误 + } finally { + setTimelineLoading(false); + } + }; + + // 复合调整处理函数:根据缓冲期和交期余量状态决定调整方式 + const handleComplexAdjustment = (nodeIndex: number, adjustment: number) => { + // 计算当前状态 + const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); + const baseBuferDays = baseBufferDays; + + // 智能缓冲期计算逻辑 + let dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments); + + // 只有在缓冲期为0时才检查最终限制:最后节点预计完成时间是否已达到客户期望日期 + let hasReachedFinalLimit = false; + if (dynamicBufferDays === 0 && expectedDate && timelineResults.length > 0) { + // 获取有效的最后流程完成日期 + let effectiveLastProcess = null; + let lastCompletionDate = null; + + for (let i = timelineResults.length - 1; i >= 0; i--) { + const process = timelineResults[i]; + const processDate = new Date(process.estimatedEnd); + + if (!isNaN(processDate.getTime()) && process.estimatedEnd !== '时效值为0') { + effectiveLastProcess = process; + lastCompletionDate = processDate; + break; + } + } + + if (!effectiveLastProcess) { + effectiveLastProcess = timelineResults[timelineResults.length - 1]; + lastCompletionDate = new Date(effectiveLastProcess.estimatedEnd); + } + + // 计算最后流程完成日期(包含调整) + const adjustedCompletionDate = new Date(lastCompletionDate); + adjustedCompletionDate.setDate(adjustedCompletionDate.getDate() + completionDateAdjustment); + + // 检查是否已达到客户期望日期 + const timeDiffToExpected = expectedDate.getTime() - adjustedCompletionDate.getTime(); + const daysToExpected = Math.ceil(timeDiffToExpected / (1000 * 60 * 60 * 24)); + hasReachedFinalLimit = daysToExpected <= 0; + } + + // 如果已达到最终限制且是正向调整,则禁止操作 + if (hasReachedFinalLimit && adjustment > 0) { + return; + } + + // 调整逻辑: + // 1. 优先使用缓冲期 + // 2. 缓冲期为0时,通过调整节点时效值实现连锁传递 + if (adjustment > 0) { + // 正数调整:需要扣减 + if (dynamicBufferDays > 0) { + // 1. 缓冲期 > 0,优先扣减缓冲期(通过调整节点时间) + const updatedResults = handleTimelineAdjustment(nodeIndex, adjustment); + // 如果调整失败,不进行后续操作 + if (!updatedResults) return; + } else { + // 2. 缓冲期 = 0,通过调整节点时效值实现连锁传递 + const updatedResults = handleTimelineAdjustment(nodeIndex, adjustment); + // 如果调整失败,不进行后续操作 + if (!updatedResults) return; + } + } else if (adjustment < 0) { + // 负数调整:需要恢复 + if (completionDateAdjustment > 0) { + // 1. 优先恢复最后流程完成日期的调整 + const restoreAmount = Math.min(-adjustment, completionDateAdjustment); + setCompletionDateAdjustment(completionDateAdjustment - restoreAmount); + const remainingRestore = -adjustment - restoreAmount; + + if (remainingRestore > 0) { + // 2. 如果还有剩余恢复量,恢复缓冲期(通过调整节点时间) + const updatedResults = handleTimelineAdjustment(nodeIndex, -remainingRestore); + // 如果调整失败,不进行后续操作 + if (!updatedResults) return; + } + } else if (totalAdjustments > 0) { + // 直接恢复缓冲期 + const updatedResults = handleTimelineAdjustment(nodeIndex, adjustment); + // 如果调整失败,不进行后续操作 + if (!updatedResults) return; + } + } + }; + + // 调整时效值的函数 + const handleTimelineAdjustment = (nodeIndex: number, adjustment: number) => { + const newAdjustments = { ...timelineAdjustments }; + const currentAdjustment = newAdjustments[nodeIndex] || 0; + const newAdjustment = currentAdjustment + adjustment; + + // 防止调整后的时效值小于0(优先使用原始时效值,其次使用上次重算后的值) + const baseValue = (typeof timelineResults[nodeIndex]?.timelineValue === 'number') + ? timelineResults[nodeIndex]!.timelineValue + : (typeof timelineResults[nodeIndex]?.adjustedTimelineValue === 'number') + ? timelineResults[nodeIndex]!.adjustedTimelineValue + : 0; + if (baseValue + newAdjustment < 0) { + return null; + } + + // 检查当前调整的节点是否为周转周期节点 + const currentNodeName = timelineResults[nodeIndex]?.nodeName; + const isTurnoverNode = currentNodeName === '周转周期'; + + // 如果调整的是周转周期节点,直接返回(禁用手动调整) + if (isTurnoverNode) { + return null; + } + + newAdjustments[nodeIndex] = newAdjustment; + + // 查找周转周期节点的索引 + const turnoverNodeIndex = timelineResults.findIndex(result => result.nodeName === '周转周期'); + + // 如果存在周转周期节点,进行反向调整 + if (turnoverNodeIndex !== -1 && turnoverNodeIndex !== nodeIndex) { + const turnoverCurrentAdjustment = newAdjustments[turnoverNodeIndex] || 0; + const turnoverBaseValue = (typeof timelineResults[turnoverNodeIndex]?.timelineValue === 'number') + ? timelineResults[turnoverNodeIndex]!.timelineValue + : (typeof timelineResults[turnoverNodeIndex]?.adjustedTimelineValue === 'number') + ? timelineResults[turnoverNodeIndex]!.adjustedTimelineValue + : 0; + + // 计算周转周期的反向调整值 + const turnoverNewAdjustment = turnoverCurrentAdjustment - adjustment; + + // 确保周转周期调整后的值不小于0 + if (turnoverBaseValue + turnoverNewAdjustment >= 0) { + newAdjustments[turnoverNodeIndex] = turnoverNewAdjustment; + } + } + + setTimelineAdjustments(newAdjustments); + + // 使用智能重算逻辑,只重算被调整的节点及其后续节点 + recalculateTimeline(newAdjustments); + return timelineResults; + }; + + // 获取重新计算后的时间线结果(不更新状态) + const getRecalculatedTimeline = (adjustments: {[key: number]: number}) => { + const updatedResults = [...timelineResults]; + let cumulativeStartTime = startTime ? new Date(startTime) : new Date(); // 从起始时间开始 + + for (let i = 0; i < updatedResults.length; i++) { + const result = updatedResults[i]; + const baseTimelineValue = (typeof result.timelineValue === 'number') + ? result.timelineValue + : (typeof result.adjustedTimelineValue === 'number') + ? result.adjustedTimelineValue + : 0; + const adjustment = adjustments[i] || 0; + const adjustedTimelineValue = baseTimelineValue + adjustment; + + // 计算当前节点的开始时间 + let nodeStartTime = new Date(cumulativeStartTime); + + // 获取节点的计算方式、周末天数和排除日期 + const nodeCalculationMethod = result.calculationMethod || 'external'; + const nodeWeekendDays = result.weekendDays || []; + const nodeExcludedDates = result.excludedDates || []; + + // 获取调整规则描述 + const ruleDescription = result.ruleDescription || ''; + + // 计算节点的结束时间 + let nodeEndTime; + if (adjustedTimelineValue > 0) { + const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates); + if (nodeCalculationMethod === 'internal') { + nodeEndTime = addInternalBusinessTime(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates); + } else { + nodeEndTime = addBusinessDaysWithHolidays(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates); + } + } else { + nodeEndTime = new Date(nodeStartTime); + } + + // 计算跳过的天数 + const adjustedStartTime = adjustedTimelineValue > 0 ? adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates) : nodeStartTime; + const skippedWeekends = calculateSkippedWeekends(adjustedStartTime, nodeEndTime, nodeWeekendDays); + const estimatedStartStr = formatDate(adjustedStartTime); + const estimatedEndStr = adjustedTimelineValue > 0 ? formatDate(nodeEndTime) : '时效值为0'; + const actualDays = calculateActualDays(estimatedStartStr, estimatedEndStr); + + // 计算时间范围内实际跳过的自定义日期 + const excludedDatesInRange = calculateExcludedDatesInRange(adjustedStartTime, nodeEndTime, nodeExcludedDates); + + // 更新结果 + updatedResults[i] = { + ...result, + adjustedTimelineValue: adjustedTimelineValue, + estimatedStart: estimatedStartStr, + estimatedEnd: estimatedEndStr, + adjustment: adjustment, + calculationMethod: nodeCalculationMethod, // 保持计算方式 + skippedWeekends: skippedWeekends, + actualDays: actualDays, + // 更新时间范围内实际跳过的日期 + actualExcludedDates: excludedDatesInRange.dates, + actualExcludedDatesCount: excludedDatesInRange.count, + adjustmentDescription: result.adjustmentDescription, // 保持调整规则描述 + ruleDescription: ruleDescription // 添加更新后的规则描述 + }; + + // 更新累积时间:当前节点的完成时间成为下一个节点的开始时间 + if (adjustedTimelineValue > 0) { + cumulativeStartTime = new Date(nodeEndTime); + } + } + + return updatedResults; + }; + + // 重新计算时间线的函数 + const recalculateTimeline = (adjustments: {[key: number]: number}, forceRecalculateAll: boolean = false) => { + const updatedResults = [...timelineResults]; + + // 找到第一个被调整的节点索引 + const adjustedIndices = Object.keys(adjustments).map(k => parseInt(k)).filter(i => adjustments[i] !== 0); + + // 找到有实际完成时间的节点 + const actualCompletionIndices = Object.keys(actualCompletionDates) + .map(k => parseInt(k)) + .filter(i => actualCompletionDates[i] !== null && actualCompletionDates[i] !== undefined); + + // 如果没有调整且不是强制重算,但有实际完成时间,需要重新计算 + if (adjustedIndices.length === 0 && !forceRecalculateAll && actualCompletionIndices.length === 0) { + return; // 没有调整也没有实际完成时间,直接返回 + } + + // 确定第一个需要重新计算的节点索引 + let firstAdjustedIndex: number; + + if (forceRecalculateAll) { + firstAdjustedIndex = 0; + } else if (actualCompletionIndices.length > 0) { + // 如果有实际完成时间,从最早有实际完成时间的节点的下一个节点开始重新计算 + const earliestActualCompletionIndex = Math.min(...actualCompletionIndices); + const earliestAdjustmentIndex = adjustedIndices.length > 0 ? Math.min(...adjustedIndices) : Infinity; + firstAdjustedIndex = Math.min(earliestActualCompletionIndex + 1, earliestAdjustmentIndex); + console.log(`检测到实际完成时间,从节点 ${firstAdjustedIndex} 开始重新计算`); + } else { + firstAdjustedIndex = adjustedIndices.length > 0 ? Math.min(...adjustedIndices) : 0; + } + + // 确保索引不超出范围 + firstAdjustedIndex = Math.max(0, Math.min(firstAdjustedIndex, updatedResults.length - 1)); + + // 确定累积开始时间 + let cumulativeStartTime: Date; + if (firstAdjustedIndex === 0) { + // 如果调整的是第一个节点,从起始时间开始 + cumulativeStartTime = startTime ? new Date(startTime) : new Date(); + } else { + // 如果调整的不是第一个节点,从前一个节点的结束时间开始 + const previousResult = updatedResults[firstAdjustedIndex - 1]; + const previousIndex = firstAdjustedIndex - 1; + + // 检查前一个节点是否有实际完成时间 + if (actualCompletionDates[previousIndex]) { + // 使用实际完成时间作为下一个节点的开始时间 + cumulativeStartTime = new Date(actualCompletionDates[previousIndex]!); + console.log(`节点 ${previousIndex} 使用实际完成时间: ${formatDate(cumulativeStartTime)}`); + } else { + // 使用预计完成时间 + cumulativeStartTime = new Date(previousResult.estimatedEnd); + } + } + + // 只重新计算从第一个调整节点开始的后续节点 + for (let i = firstAdjustedIndex; i < updatedResults.length; i++) { + const result = updatedResults[i]; + const baseTimelineValue = (typeof result.timelineValue === 'number') + ? result.timelineValue + : (typeof result.adjustedTimelineValue === 'number') + ? result.adjustedTimelineValue + : 0; + const adjustment = adjustments[i] || 0; + const adjustedTimelineValue = baseTimelineValue + adjustment; + + // 计算当前节点的开始时间 + let nodeStartTime = new Date(cumulativeStartTime); + + // 重新应用起始日期调整规则 + if (result.startDateRule) { + let ruleJson = ''; + if (typeof result.startDateRule === 'string') { + ruleJson = result.startDateRule; + } else if (result.startDateRule && result.startDateRule.text) { + ruleJson = result.startDateRule.text; + } + + if (ruleJson.trim()) { + nodeStartTime = adjustStartDateByRule(nodeStartTime, ruleJson); + } + } + + // 重新应用JSON格式日期调整规则 + let ruleDescription = result.ruleDescription; // 保持原有描述作为默认值 + if (result.dateAdjustmentRule) { + let ruleText = ''; + if (typeof result.dateAdjustmentRule === 'string') { + ruleText = result.dateAdjustmentRule; + } else if (Array.isArray(result.dateAdjustmentRule)) { + ruleText = result.dateAdjustmentRule + .filter(item => item.type === 'text') + .map(item => item.text) + .join(''); + } else if (result.dateAdjustmentRule && result.dateAdjustmentRule.text) { + ruleText = result.dateAdjustmentRule.text; + } + + if (ruleText && ruleText.trim() !== '') { + const adjustmentResult = adjustStartDateByJsonRule(nodeStartTime, ruleText); + nodeStartTime = adjustmentResult.adjustedDate; + // 更新规则描述 + if (adjustmentResult.description) { + ruleDescription = adjustmentResult.description; + } + } + } + + const nodeWeekendDays = result.weekendDaysConfig || []; // 使用节点特定的休息日配置 + const nodeCalculationMethod = result.calculationMethod || '外部'; // 获取节点的计算方式 + + let nodeEndTime: Date; + const nodeExcludedDates = Array.isArray(result.excludedDates) ? result.excludedDates : []; + if (adjustedTimelineValue > 0) { + const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates); + if (nodeCalculationMethod === '内部') { + nodeEndTime = addInternalBusinessTime(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates); + } else { + nodeEndTime = addBusinessDaysWithHolidays(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates); + } + } else { + nodeEndTime = new Date(nodeStartTime); + } + + // 计算跳过的天数 + const adjustedStartTime = adjustedTimelineValue > 0 ? adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates) : nodeStartTime; + const skippedWeekends = calculateSkippedWeekends(adjustedStartTime, nodeEndTime, nodeWeekendDays); + const estimatedStartStr = formatDate(adjustedStartTime); + const estimatedEndStr = adjustedTimelineValue > 0 ? formatDate(nodeEndTime) : '时效值为0'; + const actualDays = calculateActualDays(estimatedStartStr, estimatedEndStr); + + // 计算时间范围内实际跳过的自定义日期 + const excludedDatesInRange = calculateExcludedDatesInRange(adjustedStartTime, nodeEndTime, nodeExcludedDates); + + // 更新结果 + updatedResults[i] = { + ...result, + adjustedTimelineValue: adjustedTimelineValue, + estimatedStart: estimatedStartStr, + estimatedEnd: estimatedEndStr, + adjustment: adjustment, + calculationMethod: nodeCalculationMethod, // 保持计算方式 + skippedWeekends: skippedWeekends, + actualDays: actualDays, + // 更新时间范围内实际跳过的日期 + actualExcludedDates: excludedDatesInRange.dates, + actualExcludedDatesCount: excludedDatesInRange.count, + adjustmentDescription: result.adjustmentDescription, // 保持调整规则描述 + ruleDescription: ruleDescription // 添加更新后的规则描述 + }; + + // 更新累积时间:优先使用当前节点的实际完成时间,否则使用预计完成时间 + if (adjustedTimelineValue > 0) { + if (actualCompletionDates[i]) { + // 如果当前节点有实际完成时间,使用实际完成时间 + cumulativeStartTime = new Date(actualCompletionDates[i]!); + } else { + // 否则使用预计完成时间 + cumulativeStartTime = new Date(nodeEndTime); + } + } + } + + setTimelineResults(updatedResults); + }; + + // 添加快照还原状态标志 + const [isRestoringSnapshot, setIsRestoringSnapshot] = useState(false); + + // 当起始时间变更时,重新以最新起始时间为基准重算全流程 + useEffect(() => { + if (timelineResults.length > 0 && !isRestoringSnapshot) { + recalculateTimeline(timelineAdjustments, true); // 强制重算所有节点 + } + }, [startTime, isRestoringSnapshot]); + + // 重置调整的函数 + const resetTimelineAdjustments = () => { + setTimelineAdjustments({}); + setDeliveryMarginDeductions(0); // 同时重置交期余量扣减 + setCompletionDateAdjustment(0); // 重置最后流程完成日期调整 + setActualCompletionDates({}); // 重置实际完成日期 + recalculateTimeline({}, true); // 强制重算所有节点 + }; + + // 已移除未使用的 getTimelineLabelFieldId 辅助函数 + + // 写入货期记录表的函数 + const writeToDeliveryRecordTable = async ( + timelineResults: any[], + processRecordIds: string[], + timelineAdjustments: {[key: number]: number} = {}, + batchData?: { + selectedRecords: string[], + recordDetails: any[], + labels?: Record, + expectedDate?: any, + startTime?: any + } + ) => { + try { + console.log('=== 开始写入货期记录表 ==='); + console.log('当前模式:', mode); + console.log('timelineResults数量:', timelineResults?.length || 0); + console.log('processRecordIds数量:', processRecordIds?.length || 0); + console.log('timelineAdjustments:', timelineAdjustments); + console.log('timelineResults详情:', timelineResults); + console.log('processRecordIds详情:', processRecordIds); + + // 获取货期记录表 + console.log('正在获取货期记录表...'); + const deliveryRecordTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID); + console.log('成功获取货期记录表'); + + // 检查字段是否存在 + console.log('正在检查和获取所有必需字段...'); + const fieldsToCheck = [ + DELIVERY_FOREIGN_ID_FIELD_ID, + DELIVERY_LABELS_FIELD_ID, + DELIVERY_STYLE_FIELD_ID, + DELIVERY_COLOR_FIELD_ID, + DELIVERY_CREATE_TIME_FIELD_ID, + DELIVERY_EXPECTED_DATE_FIELD_ID, + DELIVERY_CUSTOMER_EXPECTED_DATE_FIELD_ID, + DELIVERY_NODE_DETAILS_FIELD_ID, + DELIVERY_ADJUSTMENT_INFO_FIELD_ID, // 添加货期调整信息字段 + DELIVERY_START_TIME_FIELD_ID // 新增:起始时间字段 + ]; + console.log('需要检查的字段ID列表:', fieldsToCheck); + + // 获取各个字段 + console.log('正在获取各个字段对象...'); + const foreignIdField = await deliveryRecordTable.getField(DELIVERY_FOREIGN_ID_FIELD_ID); + const labelsField = await deliveryRecordTable.getField(DELIVERY_LABELS_FIELD_ID); + const styleField = await deliveryRecordTable.getField(DELIVERY_STYLE_FIELD_ID); + const colorField = await deliveryRecordTable.getField(DELIVERY_COLOR_FIELD_ID); + const text2Field = await deliveryRecordTable.getField(DELIVERY_TEXT2_FIELD_ID); + const createTimeField = await deliveryRecordTable.getField(DELIVERY_CREATE_TIME_FIELD_ID); + const expectedDateField = await deliveryRecordTable.getField(DELIVERY_EXPECTED_DATE_FIELD_ID); + const nodeDetailsField = await deliveryRecordTable.getField(DELIVERY_NODE_DETAILS_FIELD_ID); + const customerExpectedDateField = await deliveryRecordTable.getField(DELIVERY_CUSTOMER_EXPECTED_DATE_FIELD_ID); + const adjustmentInfoField = await deliveryRecordTable.getField(DELIVERY_ADJUSTMENT_INFO_FIELD_ID); + const versionField = await deliveryRecordTable.getField(DELIVERY_VERSION_FIELD_ID); + const startTimeField = await deliveryRecordTable.getField(DELIVERY_START_TIME_FIELD_ID); + const snapshotField = await deliveryRecordTable.getField(DELIVERY_SNAPSHOT_JSON_FIELD_ID); + console.log('成功获取所有字段对象'); + + // 检查标签汇总字段的类型 + console.log('标签汇总字段信息:', { + id: labelsField.id, + name: labelsField.name, + type: labelsField.type, + property: labelsField.property + }); + + // 获取foreign_id:调整模式严格使用快照数据,生成模式优先使用选择记录 + console.log('=== 开始获取foreign_id ==='); + let foreignId = ''; + + // 使用传递的数据或全局状态 + const currentSelectedRecords = batchData?.selectedRecords || selectedRecords; + const currentRecordDetails = batchData?.recordDetails || recordDetails; + + if (mode === 'adjust') { + // 调整模式:严格使用快照回填的foreign_id,即使为空也不回退 + foreignId = currentForeignId; + console.log('调整模式:严格使用快照恢复的foreign_id:', foreignId); + } else if (currentSelectedRecords.length > 0) { + // 生成模式:从选择记录获取 + console.log('生成模式:从选择记录获取foreign_id'); + console.log('selectedRecords[0]:', currentSelectedRecords[0]); + + if (batchData) { + // 批量模式:直接从传递的数据中获取 + const firstRecord = currentRecordDetails[0]; + if (firstRecord && firstRecord.fields && firstRecord.fields['fldpvBfeC0']) { + const fieldValue = firstRecord.fields['fldpvBfeC0']; + foreignId = extractText(fieldValue); + console.log('批量模式:从传递数据获取foreign_id:', foreignId); + } + } else { + // 生成模式:从数据库获取 + const table = await bitable.base.getTable(TABLE_ID); + const firstRecord = await table.getRecordById(currentSelectedRecords[0]); + console.log('获取到的记录:', firstRecord); + const fieldValue = firstRecord.fields['fldpvBfeC0']; + console.log('fldpvBfeC0字段值:', fieldValue); + + foreignId = extractText(fieldValue); + } + } + + // 生成模式的回退逻辑:记录详情 + if (mode !== 'adjust' && !foreignId && currentRecordDetails.length > 0) { + const first = currentRecordDetails[0]; + const val = first.fields['fldpvBfeC0']; + foreignId = extractText(val); + } + // 生成模式的最后回退:快照状态 + if (mode !== 'adjust' && !foreignId && currentForeignId) { + foreignId = currentForeignId; + } + + // 获取款式与颜色:调整模式优先使用快照数据,生成模式优先使用记录详情 + let style = ''; + let color = ''; + + if (mode === 'adjust') { + // 调整模式:严格使用快照回填的数据,即使为空也不回退 + style = currentStyleText; + color = currentColorText; + console.log('调整模式:严格使用快照恢复的款式:', style, '颜色:', color); + } else { + // 生成模式:优先使用记录详情 + if (currentRecordDetails.length > 0) { + const first = currentRecordDetails[0]; + style = extractText(first.fields['fld6Uw95kt']) || currentStyleText || ''; + color = extractText(first.fields['flde85ni4O']) || currentColorText || ''; + } else { + // 回退:使用快照回填的状态 + style = currentStyleText || ''; + color = currentColorText || ''; + // 若仍为空且有选择记录,仅做一次读取 + if ((!style || !color) && currentSelectedRecords.length > 0) { + const table = await bitable.base.getTable(TABLE_ID); + const firstRecord = await table.getRecordById(currentSelectedRecords[0]); + style = style || extractText(firstRecord.fields['fld6Uw95kt']); + color = color || extractText(firstRecord.fields['flde85ni4O']); + } + } + } + + // 获取文本2:调整模式优先使用快照数据;生成模式在批量模式下填写 + let text2 = ''; + if (mode === 'adjust') { + // 调整模式:严格使用快照回填的数据,即使为空也不回退 + text2 = currentText2; // 直接使用快照值,不使用 || '' 的回退逻辑 + console.log('调整模式:严格使用快照恢复的文本2:', text2); + } else { + if (batchData && currentRecordDetails.length > 0) { + const first = currentRecordDetails[0]; + if (first?.fields?.['fldG6LZnmU']) { + text2 = extractText(first.fields['fldG6LZnmU']); + } + // 兜底:若批量数据中显式传递了text2,则使用之 + if (!text2 && (batchData as any).text2) { + try { text2 = extractText((batchData as any).text2); } catch { text2 = (batchData as any).text2; } + } + console.log('生成模式(批量):文本2来自批量数据:', text2); + } else { + // 非批量的生成模式:保持为空 + text2 = ''; + console.log('生成模式:文本2字段保持为空'); + } + } + + // 获取标签汇总:批量模式优先使用传入的labels + const selectedLabelValues = batchData?.labels + ? Object.values(batchData.labels).flat().filter(Boolean) + : Object.values(selectedLabels).flat().filter(Boolean); + + // 获取预计交付日期(交期余量的日期版本:最后流程完成日期 + 基础缓冲期) + let expectedDeliveryDate = null; + if (timelineResults.length > 0) { + // 从后往前查找第一个有效的流程完成日期(与交期余量计算逻辑一致) + let effectiveLastProcess = null; + for (let i = timelineResults.length - 1; i >= 0; i--) { + const process = timelineResults[i]; + if (process.estimatedEnd && + !process.estimatedEnd.includes('未找到') && + !process.estimatedEnd.includes('时效值为0') && + process.estimatedEnd !== 'Invalid Date') { + effectiveLastProcess = process; + break; + } + } + + if (effectiveLastProcess) { + try { + const lastCompletionDate = new Date(effectiveLastProcess.estimatedEnd); + + // 计算动态缓冲期:基础缓冲期 - 节点总调整量,最小为0天 + const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); + const baseBuferDays = baseBufferDays; + const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments); + + // 加上动态缓冲期 + const deliveryDate = new Date(lastCompletionDate); + deliveryDate.setDate(deliveryDate.getDate() + dynamicBufferDays); + expectedDeliveryDate = deliveryDate.getTime(); + } catch (error) { + console.error('转换预计交付日期失败:', error); + } + } + } + + // 获取客户期望日期:批量模式优先使用传入的expectedDate + let customerExpectedDate = null; + if (batchData?.expectedDate) { + try { + customerExpectedDate = new Date(batchData.expectedDate).getTime(); + } catch { /* 忽略转换错误,保持null */ } + } else if (expectedDate) { + customerExpectedDate = expectedDate.getTime(); // 转换为时间戳 + } + + // 创建当前时间戳 + const currentTime = new Date().getTime(); + + // 计算版本号(数字)并格式化货期调整信息 + let versionNumber = 1; + try { + if (mode === 'adjust' && currentVersionNumber !== null) { + // 调整模式:优先使用快照version +1 + versionNumber = currentVersionNumber + 1; + } else if (foreignId) { + const existing = await deliveryRecordTable.getRecords({ + pageSize: 5000, + filter: { + conjunction: 'and', + conditions: [{ + fieldId: DELIVERY_FOREIGN_ID_FIELD_ID, + operator: 'is', + value: [foreignId] + }] + } + }); + const count = existing.records?.length || 0; + versionNumber = count + 1; + } + } catch (e) { + console.warn('计算版本号失败:', e); + } + + let adjustmentInfo = `版本:V${versionNumber}`; + if (Object.keys(timelineAdjustments).length > 0) { + const adjustmentTexts = Object.entries(timelineAdjustments).map(([nodeIndex, adjustment]) => { + const nodeName = timelineResults[parseInt(nodeIndex)]?.nodeName || `节点${parseInt(nodeIndex) + 1}`; + return `${nodeName}: ${adjustment > 0 ? '+' : ''}${adjustment.toFixed(1)} 天`; + }); + adjustmentInfo += `\n当前调整:\n${adjustmentTexts.join('\n')}`; + } + + // 在创建Cell之前进行数据校验(移除冗余日志) + + // ===== 构建完整快照(保持与流程数据表写入时的一致内容)并写入到货期记录表 ===== + const expectedDateTimestamp = (batchData?.expectedDate || expectedDate) + ? (batchData?.expectedDate || expectedDate).getTime() + : null; + const expectedDateString = (batchData?.expectedDate || expectedDate) + ? format((batchData?.expectedDate || expectedDate), DATE_FORMATS.STORAGE_FORMAT) + : null; + const currentStartTime = batchData?.startTime || startTime; + const currentSelectedLabels = batchData?.labels || selectedLabels; + + // 与快照字段保持相同的命名 + const styleText = style || ''; + const colorText = color || ''; + + const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); + const baseBuferDays = baseBufferDays; + const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments); + + // 检查是否达到最终限制 + let hasReachedFinalLimit = false; + const currentExpectedDate = batchData?.expectedDate || expectedDate; + if (dynamicBufferDays === 0 && currentExpectedDate && timelineResults.length > 0) { + const lastNode = timelineResults[timelineResults.length - 1]; + if (lastNode && lastNode.estimatedEnd && !lastNode.estimatedEnd.includes('未找到')) { + try { + const lastCompletionDate = new Date(lastNode.estimatedEnd); + const daysToExpected = Math.ceil((currentExpectedDate.getTime() - lastCompletionDate.getTime()) / (1000 * 60 * 60 * 24)); + hasReachedFinalLimit = daysToExpected <= 0; + } catch (error) { + console.warn('计算最终限制状态失败:', error); + } + } + } + + const globalSnapshot = { + version: versionNumber, + foreignId, + styleText, + colorText, + text2, + mode, + selectedLabels: currentSelectedLabels, + expectedDateTimestamp, + expectedDateString, + startTimestamp: currentStartTime ? currentStartTime.getTime() : undefined, + startString: currentStartTime ? formatDate(currentStartTime, 'STORAGE_FORMAT') : undefined, + timelineAdjustments, + generationModeState: { + currentForeignId, + currentStyleText, + currentColorText, + currentText2, + currentVersionNumber: versionNumber, + recordDetails: recordDetails || [], + hasSelectedLabels: Object.values(selectedLabels).some(value => { + return Array.isArray(value) ? value.length > 0 : Boolean(value); + }), + labelSelectionComplete: Object.keys(selectedLabels).length > 0 + }, + bufferManagement: { + baseDays: baseBuferDays, + totalAdjustments, + dynamicBufferDays, + hasReachedFinalLimit + }, + chainAdjustmentSystem: { + enabled: true, + lastCalculationTime: new Date().getTime(), + adjustmentHistory: timelineAdjustments + }, + timelineCalculationState: { + calculationTimestamp: new Date().getTime(), + totalNodes: timelineResults.length, + hasValidResults: timelineResults.length > 0, + lastCalculationMode: mode + }, + totalNodes: timelineResults.length, + isGlobalSnapshot: true + }; + + // 选择用于快照的最后一个有效节点(与流程写入时的节点快照结构一致) + let selectedIndex = -1; + for (let i = timelineResults.length - 1; i >= 0; i--) { + const r = timelineResults[i]; + if (r.estimatedEnd && !r.estimatedEnd.includes('未找到') && r.estimatedEnd.trim() !== '') { + selectedIndex = i; + break; + } + } + if (selectedIndex === -1) selectedIndex = 0; // 无有效结束时间时兜底为第一个 + + const selectedResult = timelineResults[selectedIndex]; + let nodeStartTs = null as number | null; + let nodeEndTs = null as number | null; + if (selectedResult.estimatedStart && !selectedResult.estimatedStart.includes('未找到')) { + try { nodeStartTs = new Date(selectedResult.estimatedStart).getTime(); } catch {} + } + if (selectedResult.estimatedEnd && !selectedResult.estimatedEnd.includes('未找到')) { + try { nodeEndTs = new Date(selectedResult.estimatedEnd).getTime(); } catch {} + } + + const nodeSnapshot = { + processOrder: selectedResult.processOrder, + nodeName: selectedResult.nodeName, + matchedLabels: selectedResult.matchedLabels, + timelineValue: selectedResult.timelineValue, + estimatedStart: selectedResult.estimatedStart, + estimatedEnd: selectedResult.estimatedEnd, + estimatedStartTimestamp: nodeStartTs, + estimatedEndTimestamp: nodeEndTs, + timelineRecordId: selectedResult.timelineRecordId, + allMatchedRecords: selectedResult.allMatchedRecords, + isAccumulated: selectedResult.isAccumulated, + weekendDaysConfig: selectedResult.weekendDaysConfig, + excludedDates: selectedResult.excludedDates, + actualExcludedDates: selectedResult.actualExcludedDates, + actualExcludedDatesCount: selectedResult.actualExcludedDatesCount, + calculationMethod: selectedResult.calculationMethod, + ruleDescription: selectedResult.ruleDescription, + skippedWeekends: selectedResult.skippedWeekends, + actualDays: selectedResult.actualDays, + adjustedTimelineValue: selectedResult.adjustedTimelineValue, + adjustment: selectedResult.adjustment, + adjustmentDescription: selectedResult.adjustmentDescription, + startDateRule: selectedResult.startDateRule, + dateAdjustmentRule: selectedResult.dateAdjustmentRule, + nodeCalculationState: { + hasValidTimelineValue: selectedResult.timelineValue > 0, + hasValidStartTime: Boolean(nodeStartTs), + hasValidEndTime: Boolean(nodeEndTs), + calculationTimestamp: new Date().getTime(), + originalTimelineValue: selectedResult.timelineValue, + finalAdjustedValue: selectedResult.adjustedTimelineValue || selectedResult.timelineValue + }, + chainAdjustmentNode: { + nodeIndex: selectedIndex, + hasAdjustment: selectedResult.adjustment !== undefined && selectedResult.adjustment !== 0, + adjustmentValue: selectedResult.adjustment || 0, + isChainSource: selectedResult.adjustment !== undefined && selectedResult.adjustment !== 0, + affectedByChain: selectedIndex > 0 + }, + isNodeSnapshot: true + }; + + const completeSnapshot = { + ...globalSnapshot, + ...nodeSnapshot, + timelineResults: timelineResults, + currentNodeIndex: selectedIndex, + currentNodeName: selectedResult.nodeName, + isCompleteSnapshot: true, + snapshotType: 'complete' + }; + const snapshotJson = JSON.stringify(completeSnapshot); + + // 使用createCell方法创建各个字段的Cell + const foreignIdCell = await foreignIdField.createCell(foreignId); + const labelsCell = selectedLabelValues.length > 0 ? await labelsField.createCell(selectedLabelValues) : null; + const styleCell = await styleField.createCell(style); + const colorCell = await colorField.createCell(color); + const text2Cell = await text2Field.createCell(text2); + const createTimeCell = await createTimeField.createCell(currentTime); + + // 调试日志:检查startTime参数 + console.log('批量模式 - startTime参数:', startTime); + console.log('批量模式 - startTime类型:', typeof startTime); + console.log('批量模式 - currentTime:', currentTime, '对应日期:', new Date(currentTime).toLocaleString()); + + const startTimestamp = batchData?.startTime + ? new Date(batchData.startTime).getTime() + : (startTime ? startTime.getTime() : currentTime); + console.log('批量模式 - 最终使用的startTimestamp:', startTimestamp, '对应日期:', new Date(startTimestamp).toLocaleString()); + + const startTimeCell = await startTimeField.createCell(startTimestamp); + const snapshotCell = await snapshotField.createCell(snapshotJson); + const expectedDateCell = expectedDeliveryDate ? await expectedDateField.createCell(expectedDeliveryDate) : null; + const customerExpectedDateCell = customerExpectedDate ? await customerExpectedDateField.createCell(customerExpectedDate) : null; + // 对于关联记录字段,确保传入的是记录ID数组 + const nodeDetailsCell = processRecordIds.length > 0 ? + await nodeDetailsField.createCell({ recordIds: processRecordIds }) : null; + // 创建货期调整信息Cell + const adjustmentInfoCell = adjustmentInfo ? await adjustmentInfoField.createCell(adjustmentInfo) : null; + // 创建版本号Cell(数字) + const versionCell = await versionField.createCell(versionNumber); + + // 组合所有Cell到一个记录中 + const recordCells = [foreignIdCell, styleCell, colorCell, text2Cell, createTimeCell, startTimeCell, versionCell, snapshotCell]; + + // 只有当数据存在时才添加对应的Cell + if (labelsCell) recordCells.push(labelsCell); + if (expectedDateCell) recordCells.push(expectedDateCell); + if (customerExpectedDateCell) recordCells.push(customerExpectedDateCell); + if (nodeDetailsCell) recordCells.push(nodeDetailsCell); + if (adjustmentInfoCell) recordCells.push(adjustmentInfoCell); + + // 添加记录到货期记录表 + const addedRecord = await deliveryRecordTable.addRecord(recordCells); + + return addedRecord; + } catch (error) { + console.error('写入货期记录表详细错误:', { + error: error, + message: error.message, + stack: error.stack, + recordCellsLength: typeof recordCells !== 'undefined' ? recordCells.length : 'recordCells未定义' + }); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'error', + message: '写入货期记录表失败: ' + (error as Error).message + }); + } + throw error; + } + }; + + // 写入流程数据表的函数 + const writeToProcessDataTable = async (timelineResults: any[], batchData?: { + selectedRecords: string[], + recordDetails: any[], + labels?: Record, + expectedDate?: any, + startTime?: any + }): Promise => { + try { + console.log('=== 开始写入流程数据表 ==='); + console.log('当前模式:', mode); + console.log('timelineResults数量:', timelineResults?.length || 0); + console.log('timelineResults详情:', timelineResults); + + // 获取流程数据表和流程配置表 + console.log('正在获取流程数据表和流程配置表...'); + const processDataTable = await bitable.base.getTable(PROCESS_DATA_TABLE_ID); + const processConfigTable = await bitable.base.getTable(PROCESS_CONFIG_TABLE_ID); + console.log('成功获取数据表'); + + // 获取所有需要的字段 + console.log('正在获取所有必需字段...'); + const foreignIdField = await processDataTable.getField(FOREIGN_ID_FIELD_ID); + const processNameField = await processDataTable.getField(PROCESS_NAME_FIELD_ID); + const processOrderField = await processDataTable.getField(PROCESS_ORDER_FIELD_ID_DATA); + const startDateField = await processDataTable.getField(ESTIMATED_START_DATE_FIELD_ID); + const endDateField = await processDataTable.getField(ESTIMATED_END_DATE_FIELD_ID); + const versionField = await processDataTable.getField(PROCESS_VERSION_FIELD_ID); + const timelinessField = await processDataTable.getField(PROCESS_TIMELINESS_FIELD_ID); + console.log('成功获取所有字段'); + + // 获取foreign_id - 支持批量模式直接传递数据 + console.log('=== 开始获取foreign_id ==='); + let foreignId = null; + + // 使用传递的数据或全局状态 + const currentSelectedRecords = batchData?.selectedRecords || selectedRecords; + const currentRecordDetails = batchData?.recordDetails || recordDetails; + + console.log('selectedRecords数量:', currentSelectedRecords?.length || 0); + console.log('recordDetails数量:', currentRecordDetails?.length || 0); + console.log('selectedRecords:', currentSelectedRecords); + console.log('recordDetails:', currentRecordDetails); + + if (currentSelectedRecords.length > 0 && currentRecordDetails.length > 0) { + // 从第一个选择的记录的详情中获取fldpvBfeC0字段的值 + const firstRecord = currentRecordDetails[0]; + if (firstRecord && firstRecord.fields && firstRecord.fields['fldpvBfeC0']) { + const fieldValue = firstRecord.fields['fldpvBfeC0']; + foreignId = extractText(fieldValue); + console.log('从fldpvBfeC0字段获取到的foreign_id:', foreignId); + } else { + console.warn('未在记录详情中找到fldpvBfeC0字段'); + console.log('第一个记录的字段:', firstRecord?.fields); + } + } + + // 快照回填:在调整模式通过快照还原时使用当前foreign_id状态 + if (!foreignId && currentForeignId) { + foreignId = currentForeignId; + console.log('使用快照恢复的foreign_id:', foreignId); + } + + if (!foreignId) { + console.warn('未找到foreign_id,跳过写入流程数据表'); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'warning', + message: '未找到foreign_id字段,无法写入流程数据表' + }); + } + return []; + } + + // 先删除该foreign_id的所有现有记录 + console.log('=== 开始删除现有记录 ==='); + console.log('使用的foreign_id:', foreignId); + try { + console.log('正在查询现有记录...'); + const existingRecords = await processDataTable.getRecords({ + pageSize: 5000, + filter: { + conjunction: 'And', + conditions: [{ + fieldId: FOREIGN_ID_FIELD_ID, + operator: 'is', + value: [actualForeignId] + }] + } + }); + + console.log('查询到现有记录数量:', existingRecords.records?.length || 0); + + if (existingRecords.records && existingRecords.records.length > 0) { + const recordIdsToDelete = existingRecords.records.map(record => record.id); + console.log('准备删除的记录ID:', recordIdsToDelete); + await processDataTable.deleteRecords(recordIdsToDelete); + console.log(`成功删除 ${recordIdsToDelete.length} 条现有记录`); + + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'info', + message: `已删除 ${recordIdsToDelete.length} 条现有流程数据` + }); + } + } else { + console.log('没有找到需要删除的现有记录'); + } + } catch (deleteError) { + console.error('删除现有记录时出错:', deleteError); + // 继续执行,不中断流程 + } + + // 构建页面快照JSON(确保可一模一样还原) + // 计算版本号:数字。默认按 foreign_id 在货期记录表的记录数量 + 1 + let versionNumber = 1; + try { + const deliveryRecordTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID); + if (mode === 'adjust' && currentVersionNumber !== null) { + // 调整模式:优先使用快照version +1,避免公式字段读取错误 + versionNumber = currentVersionNumber + 1; + } else if (foreignId) { + const existing = await deliveryRecordTable.getRecords({ + pageSize: 5000, + filter: { + conjunction: 'and', + conditions: [{ + fieldId: DELIVERY_FOREIGN_ID_FIELD_ID, + operator: 'is', + value: [foreignId] + }] + } + }); + const count = existing.records?.length || 0; + versionNumber = count + 1; + } + } catch (e) { + console.warn('计算版本号失败:', e); + } + + + // 使用createCell方法准备要写入的记录数据 + const recordCellsToAdd = []; + + for (let index = 0; index < timelineResults.length; index++) { + const result = timelineResults[index]; + console.log('处理节点数据:', result); + + // 检查是否有有效的预计完成时间(只检查结束时间) + const hasValidEndTime = ( + result.estimatedEnd && + !result.estimatedEnd.includes('未找到') && + result.estimatedEnd.trim() !== '' + ); + + // 如果没有有效的预计完成时间,跳过这个节点 + if (!hasValidEndTime) { + console.log(`跳过节点 "${result.nodeName}" - 未找到有效的预计完成时间`); + continue; + } + + // 转换日期格式为时间戳(毫秒) + let startTimestamp = null; + let endTimestamp = null; + + if (result.estimatedStart && !result.estimatedStart.includes('未找到')) { + try { + startTimestamp = new Date(result.estimatedStart).getTime(); + } catch (error) { + console.error(`转换开始时间失败:`, error); + } + } + + if (result.estimatedEnd && !result.estimatedEnd.includes('未找到')) { + try { + endTimestamp = new Date(result.estimatedEnd).getTime(); + } catch (error) { + console.error(`转换结束时间失败:`, error); + } + } + + + // 使用createCell方法创建每个字段的Cell + const foreignIdCell = await foreignIdField.createCell(foreignId); + // 直接使用节点名称创建单元格 + const processNameCell = await processNameField.createCell(result.nodeName); + const processOrderCell = await processOrderField.createCell(result.processOrder); + const startDateCell = startTimestamp ? await startDateField.createCell(startTimestamp) : null; + const endDateCell = endTimestamp ? await endDateField.createCell(endTimestamp) : null; + + // 组合所有Cell到一个记录中 + const versionCell = await versionField.createCell(versionNumber); + const timelinessCell = (typeof result.timelineValue === 'number') + ? await timelinessField.createCell(result.timelineValue) + : null; + const recordCells = [ + foreignIdCell, + processNameCell, + processOrderCell, + versionCell + ]; + + // 只有当时间戳存在时才添加日期Cell + if (startDateCell) recordCells.push(startDateCell); + if (endDateCell) recordCells.push(endDateCell); + if (timelinessCell) recordCells.push(timelinessCell); + + console.log(`准备写入的Cell记录 - ${result.nodeName}:`, recordCells); + recordCellsToAdd.push(recordCells); + } + + console.log('所有准备写入的Cell记录:', recordCellsToAdd); + + // 在添加记录的部分,收集记录ID + const addedRecordIds: string[] = []; + + if (recordCellsToAdd.length > 0) { + try { + // 使用addRecords进行批量写入 + const addedRecords = await processDataTable.addRecords(recordCellsToAdd); + // 直接使用返回值,不需要map操作 + addedRecordIds.push(...addedRecords); + console.log(`批量写入成功,记录ID:`, addedRecords); + } catch (error) { + console.error('批量写入失败,尝试逐条写入:', error); + // 如果批量写入失败,回退到逐条写入 + for (const recordCells of recordCellsToAdd) { + const addedRecord = await processDataTable.addRecord(recordCells); + addedRecordIds.push(addedRecord); + console.log('成功添加记录:', addedRecord); + } + } + + console.log(`成功写入 ${addedRecordIds.length} 条流程数据`); + + // 显示成功提示 + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'success', + message: `成功写入 ${addedRecordIds.length} 条流程数据到流程数据表` + }); + } + + return addedRecordIds; // 返回记录ID列表 + } else { + console.warn('没有有效的记录可以写入'); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'warning', + message: '没有有效的流程数据可以写入 - 所有节点都缺少时效数据' + }); + } + return []; + } + + } catch (error) { + console.error('写入流程数据表失败:', error); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'error', + message: `写入流程数据表失败: ${(error as Error).message}` + }); + } + return []; + } + }; + + // 批量处理函数 + // 批量模式:模拟生成模式的操作(简化版本) + const simulateGenerationModeForBatch = async ( + recordId: string, + extractedLabels: {[key: string]: string | string[]}, + extractedStartTime: Date | null, + extractedExpectedDate: Date | null, + style: any, + color: any, + foreignId: any, + text2: any + ) => { + console.log('=== 批量模式:模拟生成模式操作 ==='); + console.log('recordId:', recordId); + console.log('extractedLabels:', extractedLabels); + console.log('extractedStartTime:', extractedStartTime); + console.log('extractedExpectedDate:', extractedExpectedDate); + + // 1. 构造虚拟的 selectedRecords 和 recordDetails + const virtualSelectedRecords = [recordId]; + const virtualRecordDetails = [{ + id: recordId, + fields: { + 'fldpvBfeC0': foreignId, // 外键ID字段 + 'fld6Uw95kt': style, // 样式字段 + 'flde85ni4O': color, // 颜色字段 + 'fldG6LZnmU': text2 // 文本字段2 + } + }]; + + try { + // 不再设置全局状态,直接通过overrideData传递数据 + console.log('批量模式:直接调用计算逻辑,传递起始时间:', extractedStartTime); + + // 3. 直接调用时效计算,通过overrideData传递所有必要数据 + const calculatedResults = await handleCalculateTimeline(true, { + selectedRecords: virtualSelectedRecords, + recordDetails: virtualRecordDetails, + selectedLabels: extractedLabels, + expectedDate: extractedExpectedDate, + startTime: extractedStartTime // 直接传递起始时间,避免异步状态更新问题 + }, false); // showUI: false,批量模式下不显示时间轴UI + + console.log('批量模式:时效计算完成,结果:', calculatedResults); + + // 批量模式:更新UI显示最后处理记录的时间设置 + console.log('批量模式:更新UI显示时间设置'); + setExpectedDate(extractedExpectedDate); + setStartTime(extractedStartTime); + + // 批量模式:自动保存逻辑 + console.log('批量模式:开始自动保存流程'); + + // 检查计算结果 + if (!calculatedResults || calculatedResults.length === 0) { + console.error('批量模式:时效计算结果为空,无法保存'); + throw new Error('时效计算结果为空,请检查流程配置和标签匹配'); + } + + // 自动触发保存 + console.log('批量模式:开始自动保存数据'); + await saveTimelineData({ + timelineResults: calculatedResults, // 使用直接返回的结果 + selectedRecords: virtualSelectedRecords, + recordDetails: virtualRecordDetails, + timelineAdjustments: {}, // 批量模式下没有手动调整 + labels: extractedLabels, + expectedDate: extractedExpectedDate, + startTime: extractedStartTime + }); + + console.log('批量模式:自动保存完成'); + + } catch (error) { + console.error('批量模式:处理失败', error); + throw error; // 重新抛出错误让上层处理 + } + }; + + // 保存时效数据的核心逻辑(从确定并保存按钮提取) + const saveTimelineData = async (batchData?: { + timelineResults?: any[], + selectedRecords?: string[], + recordDetails?: any[], + timelineAdjustments?: any, + labels?: Record, // 添加标签汇总 + expectedDate?: any, // 添加期望日期 + startTime?: any // 添加起始时间 + }) => { + try { + // 使用传入的数据或全局状态 + const currentTimelineResults = batchData?.timelineResults || timelineResults; + const currentTimelineAdjustments = batchData?.timelineAdjustments || timelineAdjustments; + + if (currentTimelineResults.length > 0) { + // 构造批量数据参数 + const batchDataForWriting = batchData ? { + selectedRecords: batchData.selectedRecords || [], + recordDetails: batchData.recordDetails || [], + labels: batchData.labels, // 传递标签汇总 + expectedDate: batchData.expectedDate, // 传递期望日期 + startTime: batchData.startTime // 传递起始时间 + } : undefined; + + // 写入流程数据表 + const processRecordIds = await writeToProcessDataTable(currentTimelineResults, batchDataForWriting); + + // 写入货期记录表 + await writeToDeliveryRecordTable(currentTimelineResults, processRecordIds, currentTimelineAdjustments, batchDataForWriting); + + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'success', + message: Object.keys(currentTimelineAdjustments).length > 0 + ? '已保存调整后的时效数据到流程数据表和货期记录表' + : '已保存计算的时效数据到流程数据表和货期记录表' + }); + } + + return true; // 保存成功 + } + return false; // 没有数据需要保存 + } catch (error) { + console.error('保存数据时出错:', error); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'error', + message: '保存数据失败,请重试' + }); + } + throw error; // 重新抛出错误 + } + }; + + const handleBatchProcess = async (rowCount: number) => { + // 切换到批量处理流程时重置全局变量 + resetGlobalState(); + // 检查是否选择了表 + if (!selectedBatchTableId) { + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'warning', + message: '请先选择要处理的数据表' + }); + } + return; + } + + setBatchProcessing(true); + setBatchProgress({ current: 0, total: rowCount }); + + try { + // 使用选择的表而不是硬编码的表ID + const batchTable = await bitable.base.getTable(selectedBatchTableId); + + // 获取表中的记录(限制数量) + let records = []; + if (selectedBatchViewId) { + // 如果选择了视图,从视图中获取记录ID列表,然后获取记录 + const view = await batchTable.getViewById(selectedBatchViewId); + const recordIdList = await view.getVisibleRecordIdList(); + const limitedRecordIds = recordIdList.slice(0, rowCount).filter(id => id !== undefined); + + // 批量获取记录 + for (const recordId of limitedRecordIds) { + try { + const record = await batchTable.getRecordById(recordId); + // 将recordId添加到记录对象中,确保记录对象包含正确的ID + record.recordId = recordId; + records.push(record); + } catch (error) { + console.warn(`Failed to get record ${recordId}:`, error); + } + } + } else { + // 如果没有选择视图,从表中获取记录 + const recordsResult = await batchTable.getRecords({ + pageSize: rowCount + }); + records = recordsResult.records || []; + } + const actualCount = Math.min(records.length, rowCount); + + if (actualCount === 0) { + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'warning', + message: selectedBatchViewId ? '选择的视图中没有找到记录' : '选择的表中没有找到记录' + }); + } + return; + } + + setBatchProgress({ current: 0, total: actualCount }); + + // 获取新表的字段映射 + const fieldMetaList = await batchTable.getFieldMetaList(); + const fieldMapping: {[key: string]: string} = {}; + const optionMapping: {[fieldId: string]: {[optionId: string]: string}} = {}; + + // 建立字段映射和选项映射 + for (const fieldMeta of fieldMetaList) { + if (fieldMeta.name.match(/^标签([1-9]|10)$/)) { + fieldMapping[fieldMeta.name] = fieldMeta.id; + + // 如果是选择字段,建立选项ID到名称的映射 + if (fieldMeta.property && fieldMeta.property.dataType && fieldMeta.property.dataType.property && fieldMeta.property.dataType.property.options) { + optionMapping[fieldMeta.id] = {}; + for (const option of fieldMeta.property.dataType.property.options) { + if (option.id && option.name) { + optionMapping[fieldMeta.id][option.id] = option.name; + } + } + } + } else if (fieldMeta.name === '起始日期') { + fieldMapping['起始日期'] = fieldMeta.id; + } else if (fieldMeta.name === '客户期望日期') { + fieldMapping['客户期望日期'] = fieldMeta.id; + } else if (fieldMeta.id === 'fldGSdZgMI') { + // 根据字段ID直接映射起始时间字段 + fieldMapping['起始时间'] = fieldMeta.id; + } else if (fieldMeta.id === 'fldczh2nty') { + // 款式字段映射 + fieldMapping['款式'] = fieldMeta.id; + } else if (fieldMeta.id === 'fldk5fVYvW') { + // 颜色字段映射 + fieldMapping['颜色'] = fieldMeta.id; + } else if (fieldMeta.id === 'fldkKZecSv') { + // foreign_id字段映射 + fieldMapping['foreign_id'] = fieldMeta.id; + } else if (fieldMeta.name === 'oms看板record_id' || fieldMeta.id === 'fldlaYgpYO') { + // 文本2字段映射 - 支持有空格和无空格的字段名称,以及直接ID匹配 + fieldMapping['文本2'] = fieldMeta.id; + } + } + + console.log('字段映射:', fieldMapping); + console.log('选项映射:', optionMapping); + console.log('所有字段元数据:', fieldMetaList.map(f => ({ id: f.id, name: f.name }))); + + // 特别检查文本2字段 + const text2Fields = fieldMetaList.filter(f => f.name.includes('文本') || f.id === 'fldlaYgpYO'); + console.log('包含"文本"的字段或fldlaYgpYO:', text2Fields.map(f => ({ id: f.id, name: f.name }))); + + let successCount = 0; + let errorCount = 0; + const errors: string[] = []; + + // 跟踪最后成功处理的记录的时间信息 + let lastSuccessfulStartTime: Date | null = null; + let lastSuccessfulExpectedDate: Date | null = null; + + // 逐条处理记录 + for (let i = 0; i < actualCount; i++) { + const record = records[i]; + setBatchProgress({ current: i + 1, total: actualCount }); + + try { + // 获取记录ID - 现在应该能正确获取到recordId + const recordId = record.recordId || record.id || `record_${i + 1}`; + console.log(`处理第 ${i + 1} 条记录:`, recordId); + console.log(`记录对象结构:`, record); + + // 从记录中提取标签数据 + const extractedLabels: {[key: string]: string | string[]} = {}; + const recordFields = record.fields; + + // 提取标签1-10 + for (let labelNum = 1; labelNum <= 10; labelNum++) { + const labelKey = `标签${labelNum}`; + const fieldId = fieldMapping[labelKey]; + + if (fieldId && recordFields[fieldId]) { + const fieldValue = recordFields[fieldId]; + + // 处理不同类型的字段值 + if (Array.isArray(fieldValue)) { + // 多选字段或选项ID数组 + const values = fieldValue.map((item: any) => { + if (typeof item === 'string') { + // 如果是选项ID,从选项映射中查找对应的名称 + if (optionMapping[fieldId] && optionMapping[fieldId][item]) { + return optionMapping[fieldId][item]; + } + return item; + } + if (item && item.text) return item.text; + if (item && item.name) return item.name; + return ''; + }).filter(v => v); + + if (values.length > 0) { + // 处理可能包含逗号分隔的复合标签值,并与生成模式保持一致 + const expandedValues: string[] = []; + values.forEach(value => { + if (typeof value === 'string' && value.includes(',')) { + // 拆分逗号分隔的值 + const splitValues = value.split(',').map(v => v.trim()).filter(v => v !== ''); + expandedValues.push(...splitValues); + } else { + expandedValues.push(value); + } + }); + + // 与生成模式保持一致:单值返回字符串,多值返回数组 + // 特殊处理:标签7始终返回数组结构 + if (labelKey === '标签7') { + extractedLabels[labelKey] = expandedValues; + } else { + extractedLabels[labelKey] = expandedValues.length === 1 ? expandedValues[0] : expandedValues; + } + } + } else if (typeof fieldValue === 'string') { + // 单行文本或选项ID + let finalValue: string; + if (optionMapping[fieldId] && optionMapping[fieldId][fieldValue]) { + finalValue = optionMapping[fieldId][fieldValue]; + } else { + finalValue = fieldValue; + } + + // 特殊处理:标签7始终返回数组结构 + if (labelKey === '标签7') { + extractedLabels[labelKey] = [finalValue]; + } else { + extractedLabels[labelKey] = finalValue; + } + } else if (fieldValue && fieldValue.text) { + // 富文本等 + const textValue = fieldValue.text; + // 特殊处理:标签7始终返回数组结构 + if (labelKey === '标签7') { + extractedLabels[labelKey] = [textValue]; + } else { + extractedLabels[labelKey] = textValue; + } + } else if (fieldValue && typeof fieldValue === 'object') { + // 处理公式字段和其他复杂对象 + if (fieldValue.value !== undefined) { + // 公式字段通常有value属性 + if (Array.isArray(fieldValue.value)) { + const values = fieldValue.value.map((item: any) => { + if (typeof item === 'string') return item; + if (item && item.text) return item.text; + if (item && item.name) return item.name; // 处理选项的name属性 + return ''; + }).filter(v => v); + + if (values.length > 0) { + // 与生成模式保持一致:单值返回字符串,多值返回数组 + // 特殊处理:标签7始终返回数组结构 + if (labelKey === '标签7') { + extractedLabels[labelKey] = values; + } else { + extractedLabels[labelKey] = values.length === 1 ? values[0] : values; + } + } + } else if (typeof fieldValue.value === 'string') { + // 特殊处理:标签7始终返回数组结构 + if (labelKey === '标签7') { + extractedLabels[labelKey] = [fieldValue.value]; + } else { + extractedLabels[labelKey] = fieldValue.value; + } + } else if (fieldValue.value && fieldValue.value.name) { + // 单选字段的选项对象 + const nameValue = fieldValue.value.name; + // 特殊处理:标签7始终返回数组结构 + if (labelKey === '标签7') { + extractedLabels[labelKey] = [nameValue]; + } else { + extractedLabels[labelKey] = nameValue; + } + } + } else if (fieldValue.displayValue !== undefined) { + // 有些字段使用displayValue + if (Array.isArray(fieldValue.displayValue)) { + const values = fieldValue.displayValue.map((item: any) => { + if (typeof item === 'string') return item; + if (item && item.text) return item.text; + if (item && item.name) return item.name; // 处理选项的name属性 + return ''; + }).filter(v => v); + + if (values.length > 0) { + // 与生成模式保持一致:单值返回字符串,多值返回数组 + // 特殊处理:标签7始终返回数组结构 + if (labelKey === '标签7') { + extractedLabels[labelKey] = values; + } else { + extractedLabels[labelKey] = values.length === 1 ? values[0] : values; + } + } + } else if (typeof fieldValue.displayValue === 'string') { + // 特殊处理:标签7始终返回数组结构 + if (labelKey === '标签7') { + extractedLabels[labelKey] = [fieldValue.displayValue]; + } else { + extractedLabels[labelKey] = fieldValue.displayValue; + } + } else if (fieldValue.displayValue && fieldValue.displayValue.name) { + // 单选字段的选项对象 + const nameValue = fieldValue.displayValue.name; + // 特殊处理:标签7始终返回数组结构 + if (labelKey === '标签7') { + extractedLabels[labelKey] = [nameValue]; + } else { + extractedLabels[labelKey] = nameValue; + } + } + } else if (fieldValue.name) { + // 直接的选项对象,有name属性 + const nameValue = fieldValue.name; + // 特殊处理:标签7始终返回数组结构 + if (labelKey === '标签7') { + extractedLabels[labelKey] = [nameValue]; + } else { + extractedLabels[labelKey] = nameValue; + } + } + } + } + } + + // 提取起始日期 + let extractedStartTime: Date | null = null; + const startDateFieldId = fieldMapping['起始日期'] || fieldMapping['起始时间']; + if (startDateFieldId && recordFields[startDateFieldId]) { + const startDateValue = recordFields[startDateFieldId]; + if (typeof startDateValue === 'number') { + extractedStartTime = new Date(startDateValue); + } else if (typeof startDateValue === 'string') { + extractedStartTime = new Date(startDateValue); + } else if (Array.isArray(startDateValue) && startDateValue.length > 0) { + // 处理数组格式的时间戳,如 [1757260800000] + const timestamp = startDateValue[0]; + if (typeof timestamp === 'number') { + extractedStartTime = new Date(timestamp); + } + } + } + + // 提取客户期望日期 + let extractedExpectedDate: Date | null = null; + const expectedDateFieldId = fieldMapping['客户期望日期']; + if (expectedDateFieldId && recordFields[expectedDateFieldId]) { + const expectedDateValue = recordFields[expectedDateFieldId]; + if (typeof expectedDateValue === 'number') { + extractedExpectedDate = new Date(expectedDateValue); + } else if (typeof expectedDateValue === 'string') { + extractedExpectedDate = new Date(expectedDateValue); + } else if (Array.isArray(expectedDateValue) && expectedDateValue.length > 0) { + // 处理数组格式的时间戳,如 [1757260800000] + const timestamp = expectedDateValue[0]; + if (typeof timestamp === 'number') { + extractedExpectedDate = new Date(timestamp); + } + } + } + + // 统一的字段提取函数(复用生成模式的逻辑) + // 将提取的文本转换为富文本格式 + const createRichTextFormat = (text: string) => { + return text ? [{ type: "text", text: text }] : null; + }; + + // 提取款式、颜色、foreign_id字段 + let extractedStyle: any = null; + let extractedColor: any = null; + let extractedForeignId: any = null; + + // 提取款式字段 + const styleFieldId = fieldMapping['款式']; + if (styleFieldId && recordFields[styleFieldId]) { + const styleText = extractText(recordFields[styleFieldId]); + extractedStyle = createRichTextFormat(styleText); + } + + // 提取颜色字段 + const colorFieldId = fieldMapping['颜色']; + if (colorFieldId && recordFields[colorFieldId]) { + const colorText = extractText(recordFields[colorFieldId]); + extractedColor = createRichTextFormat(colorText); + } + + // 提取foreign_id字段 + const foreignIdFieldId = fieldMapping['foreign_id']; + if (foreignIdFieldId && recordFields[foreignIdFieldId]) { + const foreignIdText = extractText(recordFields[foreignIdFieldId]); + extractedForeignId = createRichTextFormat(foreignIdText); + } + + // 提取文本2字段 + let extractedText2: any = null; + const text2FieldId = fieldMapping['文本2']; + console.log('文本2字段映射:', { fieldName: '文本2', fieldId: text2FieldId }); + console.log('记录字段keys:', Object.keys(recordFields)); + console.log('fldlaYgpYO字段值:', recordFields['fldlaYgpYO']); + + if (text2FieldId && recordFields[text2FieldId]) { + extractedText2 = extractText(recordFields[text2FieldId]); + console.log('提取后的extractedText2:', extractedText2); + } else { + console.log('文本2字段未找到或无数据:', { + fieldId: text2FieldId, + hasField: !!recordFields[text2FieldId], + fieldValue: recordFields[text2FieldId] + }); + } + + console.log(`记录 ${i + 1} 提取的数据:`, { + labels: extractedLabels, + startTime: extractedStartTime, + expectedDate: extractedExpectedDate, + style: extractedStyle, + color: extractedColor, + foreignId: extractedForeignId, + text2: extractedText2 + }); + + console.log(`记录 ${i + 1} 字段映射:`, fieldMapping); + console.log(`记录 ${i + 1} 记录字段:`, Object.keys(recordFields)); + + // 如果没有提取到任何标签,跳过这条记录 + if (Object.keys(extractedLabels).length === 0) { + console.warn(`记录 ${i + 1} 没有找到任何标签数据,跳过处理`); + errorCount++; + errors.push(`记录 ${i + 1}: 没有找到标签数据`); + continue; + } + + // 使用模拟生成模式的方式进行时效计算和写入 + await simulateGenerationModeForBatch( + recordId, + extractedLabels, + extractedStartTime, + extractedExpectedDate, + extractedStyle, + extractedColor, + extractedForeignId, + extractedText2 + ); + + successCount++; + + // 记录最后成功处理的记录的时间信息 + lastSuccessfulStartTime = extractedStartTime; + lastSuccessfulExpectedDate = extractedExpectedDate; + + // 添加延迟避免API限制 + if (i < actualCount - 1) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + + } catch (error) { + console.error(`处理记录 ${i + 1} 失败:`, error); + errorCount++; + errors.push(`记录 ${i + 1}: ${(error as Error).message}`); + } + } + + // 显示处理结果 + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: successCount > 0 ? 'success' : 'error', + message: `批量处理完成!成功: ${successCount} 条,失败: ${errorCount} 条` + }); + } + + // 如果有成功处理的记录,更新UI时间显示为最后一条成功记录的时间 + if (successCount > 0 && lastSuccessfulStartTime && lastSuccessfulExpectedDate) { + console.log('批量处理完成:更新UI时间显示'); + console.log('最后成功记录的起始时间:', lastSuccessfulStartTime); + console.log('最后成功记录的期望日期:', lastSuccessfulExpectedDate); + + setStartTime(lastSuccessfulStartTime); + setExpectedDate(lastSuccessfulExpectedDate); + } + + if (errors.length > 0) { + console.error('批量处理错误详情:', errors); + } + + } catch (error) { + console.error('批量处理失败:', error); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'error', + message: `批量处理失败: ${(error as Error).message}` + }); + } + } finally { + setBatchProcessing(false); + setBatchProgress({ current: 0, total: 0 }); + } + }; + + // 为批量处理优化的时效计算函数 + const calculateTimelineForBatch = async ( + labels: {[key: string]: string | string[]}, + startTime: Date | null, + expectedDate: Date | null, + sourceRecordId: string, + style?: any, + color?: any, + foreignId?: any, + text2?: any + ) => { + try { + // 批量模式:输出当前数据结构,便于与生成模式对比 + try { + console.group('=== 批量模式:计算时效 - 当前数据结构 ==='); + console.log('sourceRecordId:', sourceRecordId); + console.log('labels:', labels); + console.log('startTime:', startTime); + console.log('expectedDate:', expectedDate); + console.log('style:', style); + console.log('color:', color); + console.log('foreignId:', foreignId); + console.log('text2:', text2); + console.groupEnd(); + } catch (logErr) { + console.warn('批量模式结构化日志输出失败:', logErr); + } + // 1. 获取流程配置表数据 + const processConfigTable = await bitable.base.getTable(PROCESS_CONFIG_TABLE_ID); + const processConfigRecords = await processConfigTable.getRecords({ + pageSize: 5000 + }); + + const processNodes = processConfigRecords.records || []; + + // 2. 构建业务选择的标签值集合 + const businessLabelValues = new Set(); + console.log('批量模式 - 开始处理标签数据:', labels); + + for (const [labelKey, selectedValue] of Object.entries(labels)) { + console.log(`批量模式 - 处理标签 ${labelKey}:`, selectedValue, '类型:', typeof selectedValue); + + if (selectedValue) { + const values = Array.isArray(selectedValue) ? selectedValue : [selectedValue]; + values.forEach((value, index) => { + console.log(`批量模式 - ${labelKey} 值${index}:`, value, '类型:', typeof value); + + if (typeof value === 'string' && value.trim()) { + // 处理可能包含逗号分隔的复合标签值 + const splitValues = value.split(',').map(v => v.trim()).filter(v => v !== ''); + console.log(`批量模式 - ${labelKey} 分割后的值:`, splitValues); + + splitValues.forEach(splitValue => { + businessLabelValues.add(splitValue); + console.log(`批量模式 - 添加到业务标签集合: "${splitValue}"`); + }); + } + }); + } + } + + console.log('批量模式 - 最终业务标签值集合:', Array.from(businessLabelValues)); + + // 3. 匹配流程节点 + const matchedProcessNodes: any[] = []; + console.log('批量模式 - 开始匹配流程节点,总节点数:', processNodes.length); + + for (const processNode of processNodes) { + const processFields = processNode.fields; + const nodeName = processFields[NODE_NAME_FIELD_ID]; + const processLabels = processFields[PROCESS_LABEL_FIELD_ID]; + const processOrder = processFields[PROCESS_ORDER_FIELD_ID]; + const weekendDays = processFields[WEEKEND_DAYS_FIELD_ID] || []; + const excludedDates = processFields[EXCLUDED_DATES_FIELD_ID] || []; + const startDateRule = processFields[START_DATE_RULE_FIELD_ID]; + const dateAdjustmentRule = processFields[DATE_ADJUSTMENT_RULE_FIELD_ID]; + + // 处理节点名称 + let nodeNameText = ''; + if (typeof nodeName === 'string') { + nodeNameText = nodeName; + } else if (nodeName && nodeName.text) { + nodeNameText = nodeName.text; + } + + if (!nodeNameText) continue; + + // 处理流程标签数据,与生成模式保持一致 + const processLabelTexts: string[] = []; + if (processLabels && Array.isArray(processLabels)) { + for (const labelItem of processLabels) { + const labelText = extractText(labelItem); + if (labelText && labelText.trim()) { + processLabelTexts.push(labelText.trim()); + } + } + } + + // 与生成模式保持一致:如果流程标签为空,跳过该节点 + if (processLabelTexts.length === 0) { + console.log(`批量模式 - 流程节点 "${nodeNameText}" 标签为空,跳过`); + continue; + } + + console.log(`批量模式 - 检查流程节点 "${nodeNameText}":`, { + 流程标签: processLabelTexts, + 流程顺序: processOrder + }); + + // 处理流程标签匹配 + let isMatched = false; + const matchedLabels: string[] = []; + + // 改为OR逻辑:只要有一个标签匹配就认为节点匹配 + for (const processLabelText of processLabelTexts) { + console.log(`批量模式 - 检查流程标签: "${processLabelText}"`); + + // 对流程标签也进行分割处理,以支持复合标签值 + const processLabelValues = processLabelText.split(',').map(v => v.trim()).filter(v => v !== ''); + console.log(`批量模式 - 流程标签分割后: [${processLabelValues.join(', ')}]`); + + // 检查分割后的每个流程标签值是否在业务标签集合中 + for (const processLabelValue of processLabelValues) { + if (businessLabelValues.has(processLabelValue)) { + matchedLabels.push(processLabelValue); + isMatched = true; + console.log(`批量模式 - 流程节点 "${nodeNameText}" 匹配成功,匹配的标签: "${processLabelValue}"`); + break; // 找到一个匹配就足够了 + } else { + console.log(`批量模式 - 流程标签值 "${processLabelValue}" 未匹配`); + } + } + + if (isMatched) break; // 已经匹配,不需要继续检查其他标签 + } + + if (isMatched) { + matchedProcessNodes.push({ + nodeName: nodeNameText, + processOrder: typeof processOrder === 'number' ? processOrder : parseInt(processOrder) || 0, + matchedLabels, + weekendDays: Array.isArray(weekendDays) ? weekendDays : [], + excludedDates: Array.isArray(excludedDates) ? excludedDates : [], + startDateRule, + dateAdjustmentRule + }); + console.log(`批量模式 - 添加匹配的流程节点: "${nodeNameText}"`); + } else { + console.log(`批量模式 - 流程节点 "${nodeNameText}" 未匹配`); + } + } + + // 按流程顺序排序 + matchedProcessNodes.sort((a, b) => a.processOrder - b.processOrder); + + if (matchedProcessNodes.length === 0) { + throw new Error('没有匹配的流程节点'); + } + + // 4. 获取时效数据并计算时间线 + const timelineTable = await bitable.base.getTable(TIMELINE_TABLE_ID); + const timelineRecords = await timelineTable.getRecords({ + pageSize: 5000 + }); + + // 获取时效表的标签字段映射(只需要获取一次) + const timelineFieldMetaList = await timelineTable.getFieldMetaList(); + const timelineLabelFields: {[key: string]: string} = {}; + + // 查找关系字段和计算方式字段 + let relationshipFieldId = ''; + let calculationMethodFieldId = ''; + + for (const fieldMeta of timelineFieldMetaList) { + const match = fieldMeta.name.match(/^标签([1-9]|10)$/); + if (match) { + const labelKey = `标签${match[1]}`; + timelineLabelFields[labelKey] = fieldMeta.id; + } + // 查找关系字段 + if (fieldMeta.name === '关系' || fieldMeta.id === 'fldaIDAhab') { + relationshipFieldId = fieldMeta.id; + } + // 查找时效计算方式字段 + if (fieldMeta.name === '时效计算方式' || fieldMeta.id === 'fldxfLZNUu') { + calculationMethodFieldId = fieldMeta.id; + } + } + + // 构建时效数据索引 + const timelineIndexByNode = new Map(); + + for (const timelineRecord of timelineRecords.records || []) { + const timelineFields = timelineRecord.fields; + const timelineNodeName = timelineFields[TIMELINE_NODE_FIELD_ID]; + + let timelineNodeNames: string[] = []; + + if (typeof timelineNodeName === 'string') { + timelineNodeNames = timelineNodeName.split(',').map(name => name.trim()); + } else if (Array.isArray(timelineNodeName)) { + timelineNodeNames = timelineNodeName.map(item => { + if (typeof item === 'string') { + return item.trim(); + } else if (item && item.text) { + return item.text.trim(); + } + return ''; + }).filter(name => name); + } else if (timelineNodeName && timelineNodeName.text) { + timelineNodeNames = timelineNodeName.text.split(',').map(name => name.trim()); + } + + for (const nodeName of timelineNodeNames) { + const normalizedName = nodeName.toLowerCase(); + if (!timelineIndexByNode.has(normalizedName)) { + timelineIndexByNode.set(normalizedName, []); + } + timelineIndexByNode.get(normalizedName)!.push({ + record: timelineRecord, + fields: timelineFields + }); + } + } + + // 5. 计算每个节点的时效(遵循生成模式的匹配逻辑) + const results: any[] = []; + let cumulativeStartTime = startTime || new Date(); + + for (const processNode of matchedProcessNodes) { + console.log(`批量模式 - 开始处理流程节点: "${processNode.nodeName}"`); + + // 根据流程节点名称,在时效表中查找匹配的记录(与生成模式一致) + const normalizedProcessName = processNode.nodeName.trim().toLowerCase(); + const candidateRecords = timelineIndexByNode.get(normalizedProcessName) || []; + + console.log(`批量模式 - 节点 "${processNode.nodeName}" 找到 ${candidateRecords.length} 个候选时效记录`); + + let timelineValue = null; + let matchedTimelineRecord = null; + + // 收集所有匹配的候选记录(与生成模式保持一致) + const matchedCandidates: any[] = []; + + // 在候选记录中进行标签匹配,收集所有匹配的记录 + for (const candidate of candidateRecords) { + const { record: timelineRecord, fields: timelineFields } = candidate; + + // 进行完整的标签匹配逻辑(与生成模式保持一致) + let isLabelsMatched = true; + + // 检查时效表中每个有值的标签是否都包含在业务标签中 + for (const [labelKey, timelineFieldId] of Object.entries(timelineLabelFields)) { + const timelineLabelValue = timelineFields[timelineFieldId]; + + // 处理时效表中的标签值 + let timelineLabelTexts: string[] = []; + + if (typeof timelineLabelValue === 'string' && timelineLabelValue.trim()) { + timelineLabelTexts = [timelineLabelValue.trim()]; + } else if (timelineLabelValue && timelineLabelValue.text && timelineLabelValue.text.trim()) { + timelineLabelTexts = [timelineLabelValue.text.trim()]; + } else if (Array.isArray(timelineLabelValue) && timelineLabelValue.length > 0) { + // 多选字段,获取所有值 + timelineLabelTexts = timelineLabelValue.map(item => { + if (typeof item === 'string') return item.trim(); + if (item && item.text) return item.text.trim(); + return ''; + }).filter(text => text); + } + + // 如果时效表中该标签有值,则检查该标签的所有值是否都包含在业务标签中 + if (timelineLabelTexts.length > 0) { + let allValuesMatched = true; + + for (const timelineText of timelineLabelTexts) { + if (!businessLabelValues.has(timelineText)) { + allValuesMatched = false; + console.log(`批量模式 - 时效表标签 ${labelKey} 的值 "${timelineText}" 不在业务选择的标签中`); + break; + } + } + + if (!allValuesMatched) { + console.log(`批量模式 - 时效表标签 ${labelKey} 的值 [${timelineLabelTexts.join(', ')}] 不完全包含在业务选择的标签中`); + isLabelsMatched = false; + break; + } else { + console.log(`批量模式 - 时效表标签 ${labelKey} 的值 [${timelineLabelTexts.join(', ')}] 完全匹配成功`); + } + } + // 如果时效表中该标签为空,则跳过检查(空标签不影响匹配) + } + + if (isLabelsMatched) { + const timelineValueField = timelineFields[TIMELINE_FIELD_ID]; + const calculationMethodField = timelineFields[calculationMethodFieldId]; + const relationshipField = timelineFields[relationshipFieldId]; + + if (timelineValueField !== null && timelineValueField !== undefined) { + let candidateTimelineValue = 0; + + // 解析时效值(与生成模式保持一致) + if (typeof timelineValueField === 'number') { + candidateTimelineValue = timelineValueField; + } else if (typeof timelineValueField === 'string') { + const parsedValue = parseFloat(timelineValueField); + candidateTimelineValue = isNaN(parsedValue) ? 0 : parsedValue; + } else if (timelineValueField && typeof timelineValueField === 'object' && timelineValueField.value !== undefined) { + candidateTimelineValue = timelineValueField.value; + } + + // 获取计算方式 + let calculationMethod = '外部'; // 默认值 + if (calculationMethodField) { + if (typeof calculationMethodField === 'string') { + calculationMethod = calculationMethodField; + } else if (calculationMethodField && typeof calculationMethodField === 'object' && calculationMethodField.text) { + calculationMethod = calculationMethodField.text; + } + } + + // 获取关系类型 + let relationshipType = '默认'; + if (relationshipField) { + if (typeof relationshipField === 'string') { + relationshipType = relationshipField; + } else if (relationshipField && typeof relationshipField === 'object' && relationshipField.text) { + relationshipType = relationshipField.text; + } + } + + // 根据计算方式转换时效值(小时转天) + let convertedTimelineValue = 0; + if (calculationMethod === '内部') { + convertedTimelineValue = Math.round((candidateTimelineValue / 9) * 1000) / 1000; + } else { + convertedTimelineValue = Math.round((candidateTimelineValue / 24) * 1000) / 1000; + } + + // 添加到匹配候选列表 + matchedCandidates.push({ + record: timelineRecord, + timelineValue: convertedTimelineValue, + originalHours: candidateTimelineValue, + calculationMethod: calculationMethod, + relationshipType: relationshipType + }); + } + } + } + + // 处理匹配的候选记录(与生成模式保持一致) + let finalTimelineValue = 0; + let processingRule = '默认'; + + if (matchedCandidates.length > 0) { + // 获取主要关系类型(用于决定处理方式) + const relationshipTypes = matchedCandidates.map(c => c.relationshipType); + const primaryRelationshipType = relationshipTypes.find(type => type === '累加值' || type === '最大值') || '默认'; + + // 使用关系字段决定处理方式(累加值、最大值、默认) + processingRule = primaryRelationshipType || '默认'; + + console.log('批量模式 - processingRule:', processingRule); + + if (processingRule === '累加值') { + finalTimelineValue = matchedCandidates.reduce((sum, candidate) => sum + candidate.timelineValue, 0); + const totalOriginalHours = matchedCandidates.reduce((sum, candidate) => sum + candidate.originalHours, 0); + + console.log(`批量模式 - 节点 ${processNode.nodeName} 累加值处理 - 找到 ${matchedCandidates.length} 条匹配记录:`); + matchedCandidates.forEach((candidate, index) => { + console.log(` 记录${index + 1}: ID=${candidate.record.id || candidate.record.recordId}, 时效=${candidate.originalHours}小时(${candidate.timelineValue}天), 计算方式=${candidate.calculationMethod}`); + }); + console.log(`累加结果: 总计${totalOriginalHours}小时 → ${finalTimelineValue}天`); + + matchedTimelineRecord = matchedCandidates[0].record; + } else if (processingRule === '最大值') { + finalTimelineValue = Math.max(...matchedCandidates.map(c => c.timelineValue)); + const maxCandidate = matchedCandidates.find(c => c.timelineValue === finalTimelineValue); + + console.log(`批量模式 - 节点 ${processNode.nodeName} 最大值处理 - 找到 ${matchedCandidates.length} 条匹配记录:`); + matchedCandidates.forEach((candidate, index) => { + console.log(` 记录${index + 1}: ID=${candidate.record.id || candidate.record.recordId}, 时效=${candidate.originalHours}小时(${candidate.timelineValue}天), 计算方式=${candidate.calculationMethod}`); + }); + console.log(`最大值结果: ${maxCandidate?.originalHours}小时(${maxCandidate?.calculationMethod}) → ${finalTimelineValue}天`); + + matchedTimelineRecord = maxCandidate?.record || matchedCandidates[0].record; + } else { + finalTimelineValue = matchedCandidates[0].timelineValue; + + console.log(`批量模式 - 节点 ${processNode.nodeName} 默认处理 - 找到 ${matchedCandidates.length} 条匹配记录:`); + matchedCandidates.forEach((candidate, index) => { + console.log(` 记录${index + 1}: ID=${candidate.record.id || candidate.record.recordId}, 时效=${candidate.originalHours}小时(${candidate.timelineValue}天), 计算方式=${candidate.calculationMethod}`); + }); + console.log(`默认结果: 使用第一条记录 ${matchedCandidates[0].originalHours}小时(${matchedCandidates[0].calculationMethod}) → ${finalTimelineValue}天`); + + matchedTimelineRecord = matchedCandidates[0].record; + } + + timelineValue = finalTimelineValue; + } + + // 计算节点时间(与生成模式保持一致) + let nodeStartTime = new Date(cumulativeStartTime); + let nodeEndTime = new Date(nodeStartTime); + let nodeCalculationMethod = '外部'; // 默认计算方式 + + // 如果有匹配的候选记录,使用其计算方式 + if (matchedCandidates.length > 0) { + if (processingRule === '累加值') { + nodeCalculationMethod = matchedCandidates[0].calculationMethod; + } else if (processingRule === '最大值') { + const maxCandidate = matchedCandidates.find(c => c.timelineValue === finalTimelineValue); + nodeCalculationMethod = maxCandidate?.calculationMethod || matchedCandidates[0].calculationMethod; + } else { + nodeCalculationMethod = matchedCandidates[0].calculationMethod; + } + } + + if (timelineValue && timelineValue > 0) { + // 根据计算方式调整开始时间 + const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, processNode.weekendDays, processNode.excludedDates || []); + + let endDate: Date; + if (nodeCalculationMethod === '内部') { + // 使用内部工作时间计算 + endDate = addInternalBusinessTime(adjustedStartTime, timelineValue, processNode.weekendDays, processNode.excludedDates || []); + } else { + // 使用原有的24小时制计算 + endDate = addBusinessDaysWithHolidays(adjustedStartTime, timelineValue, processNode.weekendDays, processNode.excludedDates || []); + } + + nodeStartTime = adjustedStartTime; + nodeEndTime = endDate; + } + + results.push({ + processOrder: processNode.processOrder, + nodeName: processNode.nodeName, + matchedLabels: processNode.matchedLabels, + timelineValue: timelineValue, + estimatedStart: formatDate(nodeStartTime), // 使用默认格式,与生成模式一致 + estimatedEnd: formatDate(nodeEndTime), // 使用默认格式,与生成模式一致 + timelineRecordId: matchedTimelineRecord ? (matchedTimelineRecord.id || matchedTimelineRecord.recordId) : null, + calculationMethod: nodeCalculationMethod, // 使用实际的计算方式 + weekendDaysConfig: processNode.weekendDays, + skippedWeekends: 0, + actualDays: calculateActualDays(formatDate(nodeStartTime), formatDate(nodeEndTime)), // 使用一致的格式 + startDateRule: processNode.startDateRule, + dateAdjustmentRule: processNode.dateAdjustmentRule, + // 新增:保存所有匹配记录的信息(用于累加情况) + allMatchedRecords: matchedCandidates.length > 1 ? matchedCandidates.map(candidate => ({ + recordId: candidate.record.id || candidate.record.recordId || candidate.record._id || candidate.record.record_id, + timelineValue: candidate.timelineValue, + originalHours: candidate.originalHours, + calculationMethod: candidate.calculationMethod, + relationshipType: candidate.relationshipType + })) : null, + // 新增:标识是否为累加处理 + isAccumulated: processingRule === '累加值' && matchedCandidates.length > 1 + }); + + // 更新累积时间 + if (timelineValue && timelineValue > 0) { + cumulativeStartTime = new Date(nodeEndTime); + } + } + + console.log('批量计算结果:', results); + + // 6. 写入数据到货期记录表和流程数据表 + if (results.length > 0) { + console.log('=== 批量模式:开始写入数据 ==='); + console.log('批量写入 - sourceRecordId:', sourceRecordId); + console.log('批量写入 - labels:', labels); + console.log('批量写入 - expectedDate:', expectedDate); + console.log('批量写入 - style:', style); + console.log('批量写入 - color:', color); + console.log('批量写入 - foreignId:', foreignId); + console.log('批量写入 - text2:', text2); + console.log('批量写入 - startTime:', startTime); + + try { + // 为标准写入函数准备数据:构造临时的数据结构 + console.log('批量写入 - 设置临时状态以适配标准写入函数'); + + // 构造临时的recordDetails,包含当前记录的字段信息 + const tempRecordDetails = [{ + id: sourceRecordId, + fields: { + 'fldpvBfeC0': foreignId, // 外键ID字段 + 'fld6Uw95kt': style, // 样式字段 + 'flde85ni4O': color, // 颜色字段 + 'fldG6LZnmU': text2 // 文本字段2 + } + }]; + + // 构造批量数据对象(包含必要的业务字段) + const batchDataForWriting = { + selectedRecords: [sourceRecordId], + recordDetails: tempRecordDetails, + labels, // 标签汇总(对象) + expectedDate, // 客户期望日期(Date) + startTime // 起始时间(Date) + }; + + console.log('批量写入 - 临时数据构造完成,开始调用标准写入函数'); + console.log('批量写入 - batchDataForWriting:', batchDataForWriting); + + // 写入流程数据表 - 使用标准函数,传递批量数据 + console.log('批量写入 - 调用标准writeToProcessDataTable函数'); + const processRecordIds = await writeToProcessDataTable(results, batchDataForWriting); + console.log('批量写入 - 流程数据表写入完成,记录ID:', processRecordIds); + + // 写入货期记录表 - 使用标准函数,传递批量数据和空的调整信息 + console.log('批量写入 - 调用标准writeToDeliveryRecordTable函数'); + await writeToDeliveryRecordTable(results, processRecordIds, {}, batchDataForWriting); + console.log('批量写入 - 货期记录表写入完成'); + + // 批量写入成功后,设置全局状态以便快照保存 + console.log('批量写入 - 设置全局状态以便快照保存'); + setSelectedRecords([sourceRecordId]); + setRecordDetails(tempRecordDetails); + setSelectedLabels(labels); + setCurrentStyleText(extractText(style)); + setCurrentColorText(extractText(color)); + setCurrentText2(extractText(text2)); + setCurrentForeignId(extractText(foreignId)); + setExpectedDate(expectedDate); + setStartTime(startTime); + + } catch (error) { + console.error('批量写入失败:', error); + throw error; + } + + console.log('=== 批量模式:数据写入完成 ==='); + } + + } catch (error) { + console.error('批量时效计算失败:', error); + throw error; + } + }; + + + + // 执行定价数据查询 + const executeQuery = async (packId: string, packType: string) => { + setQueryLoading(true); + try { + // 使用 apiService 中的函数 + const data = await executePricingQuery(packId, packType, selectedLabels); + setQueryResults(data); + + // 处理品类属性填充到标签8的逻辑保持不变 + if (data && data.length > 0) { + const label8Options = labelOptions['标签8'] || []; + const matchingCategoryValues: string[] = []; + + data.forEach((record: any) => { + if (record.品类属性 && record.品类属性.trim() !== '') { + const categoryValue = record.品类属性.trim(); + const matchingOption = label8Options.find(option => + option.value === categoryValue || option.label === categoryValue + ); + + if (matchingOption && !matchingCategoryValues.includes(categoryValue)) { + matchingCategoryValues.push(categoryValue); + } + } + }); + + if (matchingCategoryValues.length > 0) { + setSelectedLabels(prev => ({ + ...prev, + '标签8': matchingCategoryValues + })); + + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'success', + message: `已将 ${matchingCategoryValues.length} 个品类属性填充到标签8: ${matchingCategoryValues.join(', ')}` + }); + } + } + } + + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'success', + message: '查询成功' + }); + } + + } catch (error: any) { + console.error('数据库查询出错:', error); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'error', + message: `数据库查询失败: ${error.message}` + }); + } + } finally { + setQueryLoading(false); + } + }; + + // 执行二次工艺查询 + const executeSecondaryProcessQueryLocal = async (packId: string, packType: string) => { + setSecondaryProcessLoading(true); + try { + const data = await executeSecondaryProcessQuery(packId, packType); + setSecondaryProcessResults(data); + + // 处理二次工艺填充到标签7的逻辑 + if (data && data.length > 0) { + const label7Options = labelOptions['标签7'] || []; + const matchingProcessValues: string[] = []; + + data.forEach((record: any) => { + if (record.costs_type && record.costs_type.trim() !== '') { + const processValue = record.costs_type.trim(); + const matchingOption = label7Options.find(option => + option.value === processValue || option.label === processValue + ); + + if (matchingOption && !matchingProcessValues.includes(processValue)) { + matchingProcessValues.push(processValue); + } + } + }); + + if (matchingProcessValues.length > 0) { + setSelectedLabels(prev => ({ + ...prev, + '标签7': matchingProcessValues + })); + + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'success', + message: `已将 ${matchingProcessValues.length} 个二次工艺填充到标签7: ${matchingProcessValues.join(', ')}` + }); + } + } + } + + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'success', + message: '二次工艺查询成功' + }); + } + } catch (error: any) { + console.error('二次工艺查询出错:', error); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'error', + message: `二次工艺查询失败: ${error.message}` + }); + } + } finally { + setSecondaryProcessLoading(false); + } + }; + + // 执行定价详情查询 + const executePricingDetailsQueryLocal = async (packId: string) => { + setPricingDetailsLoading(true); + try { + const data = await executePricingDetailsQuery(packId); + setPricingDetailsResults(data); + + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'success', + message: '定价详情查询成功' + }); + } + } catch (error: any) { + console.error('定价详情查询出错:', error); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'error', + message: `定价详情查询失败: ${error.message}` + }); + } + } finally { + setPricingDetailsLoading(false); + } + }; + + // 处理数据库查询 + const handleQueryDatabase = async (record: any) => { + // 从记录字段中提取 packId 和 packType + let packId = ''; + let packType = ''; + + // 提取 pack_id (fldpvBfeC0) + if (record.fields.fldpvBfeC0 && Array.isArray(record.fields.fldpvBfeC0) && record.fields.fldpvBfeC0.length > 0) { + packId = record.fields.fldpvBfeC0[0].text; + } + + // 提取 pack_type (fldSAF9qXe) + if (record.fields.fldSAF9qXe && record.fields.fldSAF9qXe.text) { + packType = record.fields.fldSAF9qXe.text; + } + + if (!packId || !packType) { + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'error', + message: '缺少必要的查询参数 (pack_id 或 pack_type)' + }); + } + return; + } + + await executeQuery(packId, packType); + }; + + // 处理二次工艺查询 + const handleSecondaryProcessQuery = async (record: any) => { + // 从记录字段中提取 packId 和 packType + let packId = ''; + let packType = ''; + + // 提取 pack_id (fldpvBfeC0) + if (record.fields.fldpvBfeC0 && Array.isArray(record.fields.fldpvBfeC0) && record.fields.fldpvBfeC0.length > 0) { + packId = record.fields.fldpvBfeC0[0].text; + } + + // 提取 pack_type (fldSAF9qXe) + if (record.fields.fldSAF9qXe && record.fields.fldSAF9qXe.text) { + packType = record.fields.fldSAF9qXe.text; + } + + if (!packId || !packType) { + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'error', + message: '缺少必要的查询参数 (pack_id 或 pack_type)' + }); + } + return; + } + + await executeSecondaryProcessQueryLocal(packId, packType); + }; + + // 处理定价详情查询 + const handlePricingDetailsQuery = async (record: any) => { + // 从记录字段中提取 packId + let packId = ''; + + // 提取 pack_id (fldpvBfeC0) + if (record.fields.fldpvBfeC0 && Array.isArray(record.fields.fldpvBfeC0) && record.fields.fldpvBfeC0.length > 0) { + packId = record.fields.fldpvBfeC0[0].text; + } + + if (!packId) { + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'error', + message: '缺少必要的查询参数 (pack_id)' + }); + } + return; + } + + await executePricingDetailsQueryLocal(packId); + }; + + + + // 获取记录详情的函数 + const fetchRecordDetails = async (recordIdList: string[]) => { + + try { + const table = await bitable.base.getTable(TABLE_ID); + + // 并行获取所有记录详情 + const recordPromises = recordIdList.map(recordId => + table.getRecordById(recordId) + ); + + const records = await Promise.all(recordPromises); + + const recordValList = records.map((record, index) => { + console.log(`记录 ${recordIdList[index]} 的详情:`, record); + console.log(`记录 ${recordIdList[index]} 的fldpvBfeC0字段:`, record.fields['fldpvBfeC0']); + return { + id: recordIdList[index], + fields: record.fields + }; + }); + + setRecordDetails(recordValList); + } catch (error) { + console.error('获取记录详情失败:', error); + } + }; + + // 选择记录 + const handleSelectRecords = async () => { + // 切换版单数据时重置全局变量,清空旧结果与回填状态 + resetGlobalState(); + setLoading(true); + // 清空标签选择 + setSelectedLabels({}); + setExpectedDate(null); + + try { + // 修改这里:使用正确的 API + const recordIdList = await bitable.ui.selectRecordIdList(TABLE_ID, VIEW_ID); + + setSelectedRecords(recordIdList); + + // 获取记录的详细信息 + if (recordIdList.length > 0) { + // 并行获取记录详情和字段元数据 + const table = await bitable.base.getTable(TABLE_ID); + const [recordPromises, fieldMetaList] = await Promise.all([ + Promise.all(recordIdList.map(recordId => table.getRecordById(recordId))), + table.getFieldMetaList() + ]); + + const recordValList = recordPromises.map((record, index) => ({ + id: recordIdList[index], + fields: record.fields + })); + + if (recordValList.length > 0) { + const firstRecord = recordValList[0]; + const extractedLabels: {[key: string]: string} = {}; + + // 建立字段名到字段ID的映射 + const fieldNameToId: {[key: string]: string} = {}; + for (const fieldMeta of fieldMetaList) { + fieldNameToId[fieldMeta.name] = fieldMeta.id; + } + + // 提取标签值的辅助函数 + const extractFieldValue = (fieldName: string) => { + const fieldId = fieldNameToId[fieldName]; + if (fieldId && firstRecord.fields[fieldId]) { + const fieldValue = firstRecord.fields[fieldId]; + + // 优先处理数组格式(公式字段) + if (Array.isArray(fieldValue) && fieldValue.length > 0) { + const firstItem = fieldValue[0]; + if (typeof firstItem === 'string') { + return firstItem; + } else if (firstItem && (firstItem.text || firstItem.name)) { + return firstItem.text || firstItem.name; + } + } + // 处理对象格式(普通字段) + else if (typeof fieldValue === 'object' && fieldValue !== null) { + if (fieldValue.text) { + return fieldValue.text; + } else if (fieldValue.name) { + return fieldValue.name; + } + } + // 处理字符串格式 + else if (typeof fieldValue === 'string') { + return fieldValue.trim(); + } + } + return ''; + }; + + // 直接通过字段ID提取fld6Uw95kt的值 + const getFieldValueById = (fieldId: string) => { + if (fieldId && firstRecord.fields[fieldId]) { + const fieldValue = firstRecord.fields[fieldId]; + + // 优先处理数组格式(公式字段) + if (Array.isArray(fieldValue) && fieldValue.length > 0) { + const firstItem = fieldValue[0]; + if (typeof firstItem === 'string') { + return firstItem; + } else if (firstItem && (firstItem.text || firstItem.name)) { + return firstItem.text || firstItem.name; + } + } + // 处理对象格式(普通字段) + else if (typeof fieldValue === 'object' && fieldValue !== null) { + if (fieldValue.text) { + return fieldValue.text; + } else if (fieldValue.name) { + return fieldValue.name; + } + } + // 处理字符串格式 + else if (typeof fieldValue === 'string') { + return fieldValue.trim(); + } + } + return ''; + }; + + // 提取fld6Uw95kt字段的值 + const mainRecordDisplayValue = getFieldValueById('fld6Uw95kt') || firstRecord.id; + + // 将这个值存储到recordDetails中,以便在UI中使用 + const updatedRecordValList = recordValList.map((record, index) => ({ + ...record, + displayValue: index === 0 ? mainRecordDisplayValue : record.id + })); + + setRecordDetails(updatedRecordValList); + + // 提取各个标签的值 + const label2Value = extractFieldValue('品类名称'); + const label3Value = extractFieldValue('大类名称'); + const label4Value = extractFieldValue('中类名称'); + const label5Value = extractFieldValue('小类名称'); + const label6Value = extractFieldValue('工艺难易度'); + + // 设置提取到的标签值 + const newSelectedLabels: {[key: string]: string | string[]} = {}; + if (label2Value) newSelectedLabels['标签2'] = label2Value; + if (label3Value) newSelectedLabels['标签3'] = label3Value; + if (label4Value) newSelectedLabels['标签4'] = label4Value; + if (label5Value) newSelectedLabels['标签5'] = label5Value; + if (label6Value) newSelectedLabels['标签6'] = label6Value; + + // 添加标签10的自动填充 + newSelectedLabels['标签10'] = ['复版', '开货版不打版']; + + // 保留用户手动选择的标签1、7、8、9 + setSelectedLabels(prev => ({ + ...prev, + ...newSelectedLabels + })); + + console.log('自动提取的标签值:', newSelectedLabels); + + // 显示提取结果的提示 + if (Object.keys(newSelectedLabels).length > 0 && bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'success', + message: `已自动提取 ${Object.keys(newSelectedLabels).length} 个标签值` + }); + } + + // 自动执行所有三个查询 - 对第一条记录顺序执行查询 + // 顺序执行查询,避免 loading 状态冲突 + try { + await handleQueryDatabase(recordValList[0]); + await handleSecondaryProcessQuery(recordValList[0]); + await handlePricingDetailsQuery(recordValList[0]); + } catch (queryError) { + console.error('自动查询出错:', queryError); + } + } + } else { + setRecordDetails([]); + } + } catch (error) { + console.error('选择记录时出错:', error); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ + toastType: 'error', + message: '选择记录时出错,请重试' + }); + } + } finally { + setLoading(false); + } + }; + + // 清空选中的记录 + const handleClearRecords = () => { + setSelectedRecords([]); + setRecordDetails([]); + setQueryResults([]); + setSecondaryProcessResults([]); + setPricingDetailsResults([]); + // 同时清空标签选择 + setSelectedLabels({}); + setExpectedDate(null); + }; + + // 定价数据表格列定义 + const columns = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + }, + { + title: 'Pack ID', + dataIndex: 'pack_id', + key: 'pack_id', + }, + { + title: 'Pack Type', + dataIndex: 'pack_type', + key: 'pack_type', + }, + { + title: '物料编码', + dataIndex: 'material_code', + key: 'material_code', + }, + { + title: '一级分类', + dataIndex: 'category1_name', + key: 'category1_name', + }, + { + title: '二级分类', + dataIndex: 'category2_name', + key: 'category2_name', + }, + { + title: '三级分类', + dataIndex: 'category3_name', + key: 'category3_name', + }, + { + title: '品类属性', + dataIndex: '品类属性', + key: '品类属性', + }, + ]; + + // 二次工艺表格列定义 + const secondaryProcessColumns = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + }, + { + title: 'Foreign ID', + dataIndex: 'foreign_id', + key: 'foreign_id', + }, + { + title: 'Pack Type', + dataIndex: 'pack_type', + key: 'pack_type', + }, + { + title: '项目', + dataIndex: 'costs_item', + key: 'costs_item', + }, + { + title: '二次工艺', + dataIndex: 'costs_type', + key: 'costs_type', + }, + { + title: '备注', + dataIndex: 'remarks', + key: 'remarks', + }, + ]; + + // 定价详情表格列定义 + const pricingDetailsColumns = [ + { + title: 'Pack ID', + dataIndex: 'pack_id', + key: 'pack_id', + }, + { + title: '倍率', + dataIndex: '倍率', + key: '倍率', + }, + { + title: '加工费', + dataIndex: '加工费', + key: '加工费', + }, + { + title: '总价', + dataIndex: '总价', + key: '总价', + }, + ]; + + return ( +
+ {/* 入口选择弹窗 */} + setModeSelectionVisible(false)} + maskClosable={false} + > +
+ chooseMode('generate')} + > + 生成流程日期 + 基于业务数据计算并生成节点时间线 +
+ +
+
+ chooseMode('adjust')} + > + 调整流程日期 + 读取货期记录,精确还原时间线 +
+ +
+
+ chooseMode('batch')} + > + 批量生成 + 批量生成多条流程记录 +
+ +
+
+
+
+ + {mode === 'generate' && ( +
+ + 数据查询工具 + + 基于业务数据计算并生成节点时间线 +
+ )} + {mode === 'batch' && ( +
+ + 批量生成工具 + + 批量生成多条流程记录,提高工作效率 +
+ )} + {mode === 'adjust' && ( +
+ + 调整流程日期 + + 读取货期记录,精确还原时间线 +
+ )} + {/* 功能入口切换与调整入口 */} + {mode !== null && ( +
+ +