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 && (
+
+
+
+
+ )}
+
+ {/* 时效计算结果模态框 */}
+
{
+ setTimelineVisible(false);
+ setTimelineAdjustments({}); // 关闭时重置调整
+ setDeliveryMarginDeductions(0); // 关闭时重置交期余量扣减
+ setCompletionDateAdjustment(0); // 关闭时重置最后流程完成日期调整
+ }}
+ footer={
+
+
+ 缓冲期(天):
+ {
+ const n = Number(val);
+ setBaseBufferDays(Number.isFinite(n) && n >= 0 ? n : 0);
+ }}
+ style={{ width: 90 }}
+ />
+
+
+
+
+ }
+ width={900}
+ >
+
+ {timelineResults.length === 0 ? (
+
+ 未找到匹配的时效数据
+
+ ) : (
+
+
+ 起始时间
+ {
+ setStartTime(date);
+ }}
+ type="dateTime"
+ format="yyyy-MM-dd HH:mm"
+ />
+
+ {timelineResults.map((result, index) => {
+ const adjustment = timelineAdjustments[index] || 0;
+ const baseValue = (typeof result.timelineValue === 'number')
+ ? result.timelineValue
+ : (typeof result.adjustedTimelineValue === 'number')
+ ? result.adjustedTimelineValue
+ : 0;
+ const adjustedValue = baseValue + adjustment;
+
+ // 检查是否为周转周期节点
+ const isTurnoverNode = result.nodeName === '周转周期';
+
+ // 检查是否存在周转周期为零的情况
+ const hasTurnoverNodeWithZero = timelineResults.some((r, i) => {
+ if (r.nodeName === '周转周期') {
+ const adj = timelineAdjustments[i] || 0;
+ const baseVal = (typeof r.timelineValue === 'number')
+ ? r.timelineValue
+ : (typeof r.adjustedTimelineValue === 'number')
+ ? r.adjustedTimelineValue
+ : 0;
+ return (baseVal + adj) === 0;
+ }
+ return false;
+ });
+
+ // 当前节点是否为零值的周转周期节点
+ const isCurrentTurnoverZero = isTurnoverNode && adjustedValue === 0;
+
+ // 计算动态缓冲期,用于限制节点增加操作
+ const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0);
+ const baseBuferDays = baseBufferDays;
+ const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments);
+
+ // 新的复合限制逻辑:
+ // 1. 如果缓冲期 > 0,允许操作
+ // 2. 如果缓冲期 = 0,进一步判断交期余量
+ // 3. 如果缓冲期 = 0 且交期余量 <= 0,禁止操作
+ let hasNegativeBuffer = false;
+
+ // 计算交期余量(仅用于显示,不用于限制)
+ let deliveryMargin = 0;
+ if (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 deliveryDate = new Date(adjustedCompletionDate);
+ deliveryDate.setDate(deliveryDate.getDate() + dynamicBufferDays);
+
+ if (expectedDate) {
+ // 有客户期望日期:交期余量 = 客户期望日期 - (最后流程完成 + 缓冲期)
+ const timeDiff = expectedDate.getTime() - deliveryDate.getTime();
+ const baseDeliveryMargin = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
+ deliveryMargin = baseDeliveryMargin - deliveryMarginDeductions;
+ } else {
+ // 无客户期望日期:交期余量 = (最后流程完成 + 缓冲期) - 今天
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const timeDiff = deliveryDate.getTime() - today.getTime();
+ const baseDeliveryMargin = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
+ deliveryMargin = baseDeliveryMargin - deliveryMarginDeductions;
+ }
+ }
+
+ // 执行复合限制判断 - 只有在缓冲期为0时才检查最终限制
+ let canIncrease = false;
+ if (dynamicBufferDays > 0) {
+ // 1. 缓冲期 > 0,允许操作
+ canIncrease = true;
+ } else {
+ // 2. 缓冲期 = 0,检查最终限制:最后节点预计完成时间是否已达到客户期望日期
+ if (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));
+ canIncrease = daysToExpected > 0; // 只有当还有剩余天数时才允许调整
+ } else {
+ // 无客户期望日期时,理论上可以无限调整
+ canIncrease = true;
+ }
+ }
+ hasNegativeBuffer = !canIncrease;
+
+ return (
+
+
+
+ {result.processOrder && `${result.processOrder}. `}{result.nodeName}
+ {result.isAccumulated && result.allMatchedRecords ? (
+
+ (累加 {result.allMatchedRecords.length} 条记录: {result.allMatchedRecords.map(record => record.recordId).join(', ')})
+
+ ) : result.timelineRecordId ? (
+
+ ({result.timelineRecordId})
+
+ ) : (
+
+ (无匹配记录)
+
+ )}
+
+
+
+ 实际完成:
+ {
+ setActualCompletionDates(prev => ({
+ ...prev,
+ [index]: date
+ }));
+
+ // 自动设置调整量:按工作日规则(考虑内部/外部、休息日、跳过日期)
+ try {
+ const currentResult = timelineResults[index];
+ const startStr = currentResult?.estimatedStart;
+ if (startStr && date) {
+ const startDate = new Date(startStr);
+ if (!isNaN(startDate.getTime())) {
+ const calcRaw = currentResult?.calculationMethod || '外部';
+ const calcMethod = (calcRaw === '内部' || calcRaw === 'internal') ? '内部' : '外部';
+ const weekendDays = currentResult?.weekendDaysConfig || currentResult?.weekendDays || [];
+ const excludedDates = Array.isArray(currentResult?.excludedDates) ? currentResult!.excludedDates : [];
+
+ // 与正向计算一致:先对起始时间应用工作时间调整
+ const adjustedStart = adjustToNextWorkingHour(startDate, calcMethod, weekendDays, excludedDates);
+ const targetDate = new Date(date);
+
+ // 使用二分搜索反推工作日数(按0.5天粒度),使得正向计算的结束时间尽量贴近目标日期
+ const dayMs = 1000 * 60 * 60 * 24;
+ const approxNatural = Math.max(0, (targetDate.getTime() - adjustedStart.getTime()) / dayMs);
+ const endFor = (bd: number): Date => {
+ if (bd <= 0) return new Date(adjustedStart);
+ return calcMethod === '内部'
+ ? addInternalBusinessTime(new Date(adjustedStart), bd, weekendDays, excludedDates)
+ : addBusinessDaysWithHolidays(new Date(adjustedStart), bd, weekendDays, excludedDates);
+ };
+
+ let lo = 0;
+ let hi = Math.max(1, approxNatural + 50);
+ for (let it = 0; it < 40; it++) {
+ const mid = (lo + hi) / 2;
+ const end = endFor(mid);
+ if (end.getTime() < targetDate.getTime()) {
+ lo = mid;
+ } else {
+ hi = mid;
+ }
+ }
+ const desiredBusinessDays = Math.round(hi * 2) / 2; // 与UI保持0.5粒度一致
+
+ // 目标调整量 = 目标工作日数 - 基准时效值
+ const baseValue = (typeof currentResult.timelineValue === 'number')
+ ? currentResult.timelineValue
+ : (typeof currentResult.adjustedTimelineValue === 'number')
+ ? currentResult.adjustedTimelineValue
+ : 0;
+
+ const desiredAdjustmentAbs = desiredBusinessDays - baseValue;
+ const currentAdj = timelineAdjustments[index] || 0;
+ const deltaToApply = desiredAdjustmentAbs - currentAdj;
+ if (deltaToApply !== 0) {
+ const updated = handleTimelineAdjustment(index, deltaToApply);
+ if (!updated) {
+ // 如果该节点不允许调整,仍然重算以联动后续节点
+ setTimeout(() => {
+ recalculateTimeline(timelineAdjustments, false);
+ }, 0);
+ return;
+ }
+ }
+ }
+ }
+ } catch (e) {
+ console.warn('自动调整量计算失败:', e);
+ }
+
+ // 无论如何都重新计算时间线以联动交期余量
+ setTimeout(() => {
+ recalculateTimeline(timelineAdjustments, false);
+ }, 0);
+ }}
+ style={{ width: '140px' }}
+ format="yyyy-MM-dd"
+ />
+
+
+
+
+
+
时效值调整:
+
+
+
+
+
0 ? '#52c41a' : '#ff4d4f',
+ fontWeight: 'bold',
+ fontSize: '13px'
+ }}>
+ {adjustedValue.toFixed(1)} 工作日
+
+ {adjustment !== 0 && (
+
+ 原值: {baseValue.toFixed(1)}
+
+ 调整: {adjustment > 0 ? '+' : ''}{adjustment.toFixed(1)}
+
+ )}
+
+
+
+
+
+
+
+
预计开始:
+
{formatDate(result.estimatedStart)}
+
+ {getDayOfWeek(result.estimatedStart)}
+
+
+
+
+
预计完成:
+
+ {result.estimatedEnd === '时效值为0' ? result.estimatedEnd : formatDate(result.estimatedEnd)}
+
+
+ {getDayOfWeek(result.estimatedEnd)}
+
+
+
+
+
实际跨度:
+
+ {calculateActualDays(result.estimatedStart, result.estimatedEnd)} 自然日
+
+
+ 含 {adjustedValue.toFixed(1)} 工作日
+
+
+
+
+
计算方式:
+
+ {result.calculationMethod || '外部'}
+ {result.ruleDescription && (
+
+ ({result.ruleDescription})
+
+ )}
+ 0 ? `休息日配置:${result.weekendDaysConfig.map(day => ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][day]).join(', ')}` : '休息日配置:无固定休息日'}${result.calculationMethod === '内部' ? '\n工作时间:9:00-18:00 (9小时制)' : ''}${(Array.isArray(result.actualExcludedDates) && result.actualExcludedDates.length > 0) ? `\n\n跳过的具体日期:\n${result.actualExcludedDates.join('\n')}` : ''}`}
+ >
+ ?
+
+
+
+ 跳过日期:{(result.actualExcludedDatesCount && result.actualExcludedDatesCount > 0) ? `${result.actualExcludedDatesCount} 天` : '无'}
+
+
+
+
+ );
+ })}
+
+ {/* 计算公式展示卡片 */}
+ {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);
+
+ // 检查日期是否有效且不是"时效值为0"的情况
+ 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 totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0);
+
+ // 计算动态缓冲期:基础14天 - 节点总调整量,最小为0天
+ const baseBuferDays = baseBufferDays;
+ const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments);
+
+ // 计算最后完成日期 + 动态缓冲期(考虑最后流程完成日期的调整)
+ const adjustedCompletionDate = new Date(lastCompletionDate);
+ adjustedCompletionDate.setDate(adjustedCompletionDate.getDate() + completionDateAdjustment);
+
+ const deliveryDate = new Date(adjustedCompletionDate);
+ deliveryDate.setDate(deliveryDate.getDate() + dynamicBufferDays);
+
+ // 计算交期余量
+ // 如果客户期望日期不为空:交期余量 = 客户期望日期 - (最后流程完成 + 缓冲期)
+ // 如果客户期望日期为空:交期余量 = (最后流程完成 + 缓冲期)
+ let timeDiff, baseDaysDiff, finalDaysDiff;
+ if (expectedDate) {
+ // 有客户期望日期:计算期望日期与交付日期的差值
+ timeDiff = expectedDate.getTime() - deliveryDate.getTime();
+ baseDaysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
+ } else {
+ // 无客户期望日期:直接显示交付日期(最后流程完成 + 缓冲期)
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ timeDiff = deliveryDate.getTime() - today.getTime();
+ baseDaysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
+ }
+
+ // 计算最终交期余量:基础余量 - 扣减天数
+ finalDaysDiff = baseDaysDiff - deliveryMarginDeductions;
+
return (
<>
{expectedDate && (