2025-11-11 17:39:18 +08:00

6725 lines
298 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
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<string[]>([]);
const [recordDetails, setRecordDetails] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [queryResults, setQueryResults] = useState<any[]>([]);
const [queryLoading, setQueryLoading] = useState(false);
const [secondaryProcessResults, setSecondaryProcessResults] = useState<any[]>([]);
const [secondaryProcessLoading, setSecondaryProcessLoading] = useState(false);
const [pricingDetailsResults, setPricingDetailsResults] = useState<any[]>([]);
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<Date | null>(null);
// 起始时间状态(从货期记录表获取,新记录则使用当前时间)
const [startTime, setStartTime] = useState<Date | null>(null);
// 预览相关状态(已移除未使用的 previewLoading 状态)
// 时效计算相关状态
const [timelineVisible, setTimelineVisible] = useState(false);
const [timelineLoading, setTimelineLoading] = useState(false);
const [timelineResults, setTimelineResults] = useState<any[]>([]);
const [timelineAdjustments, setTimelineAdjustments] = useState<{[key: number]: number}>({});
// 交期余量扣减状态:记录从交期余量中扣减的天数
const [deliveryMarginDeductions, setDeliveryMarginDeductions] = useState<number>(0);
// 最后流程完成日期调整状态:记录最后流程完成日期增加的天数
const [completionDateAdjustment, setCompletionDateAdjustment] = useState<number>(0);
// 实际完成日期状态:记录每个节点的实际完成日期
const [actualCompletionDates, setActualCompletionDates] = useState<{[key: number]: Date | null}>({});
// 基础缓冲期天数可配置用于计算动态缓冲期默认14天
const [baseBufferDays, setBaseBufferDays] = useState<number>(14);
// 快照回填来源foreign_id、款式、颜色、文本2
const [currentForeignId, setCurrentForeignId] = useState<string | null>(null);
const [currentStyleText, setCurrentStyleText] = useState<string>('');
const [currentColorText, setCurrentColorText] = useState<string>('');
const [currentText2, setCurrentText2] = useState<string>('');
const [currentVersionNumber, setCurrentVersionNumber] = useState<number | null>(null);
// 功能入口模式与调整相关状态
const [mode, setMode] = useState<'generate' | 'adjust' | null>(null);
const [modeSelectionVisible, setModeSelectionVisible] = useState(true);
const [adjustLoading, setAdjustLoading] = useState(false);
// 删除未使用的 deliveryRecords 状态
const [selectedDeliveryRecordId, setSelectedDeliveryRecordId] = useState<string>('');
// 批量处理相关状态
const [batchProcessing, setBatchProcessing] = useState(false);
const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0 });
const [batchRowCount, setBatchRowCount] = useState<number>(10); // 默认处理10行
// 批量处理当前记录信息(用于保存时传递正确的数据)
const [currentBatchRecord, setCurrentBatchRecord] = useState<{
selectedRecords: string[],
recordDetails: any[],
labels: Record<string, any>,
expectedDate: any,
startTime: any
} | null>(null);
// 批量处理表和视图选择状态
const [selectedBatchTableId, setSelectedBatchTableId] = useState<string>('');
const [selectedBatchViewId, setSelectedBatchViewId] = useState<string>('');
const [availableTables, setAvailableTables] = useState<Array<{id: string, name: string}>>([]);
const [availableViews, setAvailableViews] = useState<Array<{id: string, name: string}>>([]);
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 PROCESS_STYLE_FIELD_ID = 'fld8xVqHJW'; // 款式字段(流程数据表)
const PROCESS_COLOR_FIELD_ID = 'fld3F1zGYe'; // 颜色字段(流程数据表)
// 货期记录表相关常量
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<string>();
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<string, any[]>();
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<string, any>,
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<string, any>,
expectedDate?: any,
startTime?: any
}): Promise<string[]> => {
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);
const processStyleField = await processDataTable.getField(PROCESS_STYLE_FIELD_ID);
const processColorField = await processDataTable.getField(PROCESS_COLOR_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 [];
}
// 获取款式与颜色:与货期记录写入逻辑保持一致
let style = '';
let color = '';
if (mode === 'adjust') {
// 调整模式:严格使用快照回填的数据
style = currentStyleText;
color = currentColorText;
console.log('调整模式:流程数据款式/颜色来自快照', { style, color });
} else {
// 生成模式:优先使用记录详情
if (currentRecordDetails.length > 0) {
const first = currentRecordDetails[0];
try { style = extractText(first.fields['fld6Uw95kt']); } catch { style = first?.fields?.['fld6Uw95kt'] || ''; }
try { color = extractText(first.fields['flde85ni4O']); } catch { color = first?.fields?.['flde85ni4O'] || ''; }
if (!style) style = currentStyleText || '';
if (!color) color = currentColorText || '';
} else {
// 回退:使用快照回填的状态
style = currentStyleText || '';
color = currentColorText || '';
// 若仍为空且有选择记录,仅做一次读取
if ((!style || !color) && currentSelectedRecords.length > 0) {
try {
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']);
} catch (e) {
console.warn('读取源表款式/颜色失败,保持现有值', e);
}
}
}
}
// 先删除该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;
const styleCell = await processStyleField.createCell(style);
const colorCell = await processColorField.createCell(color);
// 组合所有Cell到一个记录中
const versionCell = await versionField.createCell(versionNumber);
const timelinessCell = (typeof result.timelineValue === 'number')
? await timelinessField.createCell(result.timelineValue)
: null;
const recordCells = [
foreignIdCell,
processNameCell,
processOrderCell,
styleCell,
colorCell,
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<string, any>, // 添加标签汇总
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<string>();
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<string, any[]>();
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 (
<div style={{ padding: '20px' }}>
{/* 入口选择弹窗 */}
<Modal
title="请选择功能入口"
visible={modeSelectionVisible}
footer={null}
onCancel={() => setModeSelectionVisible(false)}
maskClosable={false}
>
<div style={{ display: 'flex', gap: 24, justifyContent: 'center', padding: '12px 8px 8px', flexWrap: 'wrap' }}>
<Card
style={{
width: 280,
cursor: 'pointer',
borderRadius: 12,
boxShadow: '0 8px 24px rgba(0,0,0,0.06)',
border: '1px solid #e5e7eb',
background: 'linear-gradient(180deg, #fff, #f9fbff)'
}}
onClick={() => chooseMode('generate')}
>
<Title heading={3} style={{ marginBottom: 8 }}></Title>
<Text type='tertiary'>线</Text>
<div style={{ marginTop: 16 }}>
<Button type='primary' theme='solid' size='large' style={{ width: '100%' }} onClick={() => chooseMode('generate')}></Button>
</div>
</Card>
<Card
style={{
width: 280,
cursor: 'pointer',
borderRadius: 12,
boxShadow: '0 8px 24px rgba(0,0,0,0.06)',
border: '1px solid #e5e7eb',
background: 'linear-gradient(180deg, #fff, #f9fbff)'
}}
onClick={() => chooseMode('adjust')}
>
<Title heading={3} style={{ marginBottom: 8 }}></Title>
<Text type='tertiary'>线</Text>
<div style={{ marginTop: 16 }}>
<Button type='primary' theme='solid' size='large' style={{ width: '100%' }} onClick={() => chooseMode('adjust')}></Button>
</div>
</Card>
<Card
style={{
width: 280,
cursor: 'pointer',
borderRadius: 12,
boxShadow: '0 8px 24px rgba(0,0,0,0.06)',
border: '1px solid #e5e7eb',
background: 'linear-gradient(180deg, #fff, #f9fbff)'
}}
onClick={() => chooseMode('batch')}
>
<Title heading={3} style={{ marginBottom: 8 }}></Title>
<Text type='tertiary'></Text>
<div style={{ marginTop: 16 }}>
<Button type='primary' theme='solid' size='large' style={{ width: '100%' }} onClick={() => chooseMode('batch')}></Button>
</div>
</Card>
</div>
</Modal>
{mode === 'generate' && (
<div style={{ marginBottom: '24px' }}>
<Title heading={2} style={{
background: 'linear-gradient(135deg, #3b82f6, #1d4ed8)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
marginBottom: '8px'
}}>
</Title>
<Text type="tertiary" style={{ fontSize: '14px' }}>线</Text>
</div>
)}
{mode === 'batch' && (
<div style={{ marginBottom: '24px' }}>
<Title heading={2} style={{
background: 'linear-gradient(135deg, #10b981, #059669)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
marginBottom: '8px'
}}>
</Title>
<Text type="tertiary" style={{ fontSize: '14px' }}></Text>
</div>
)}
{mode === 'adjust' && (
<div style={{ marginBottom: '24px' }}>
<Title heading={2} style={{
background: 'linear-gradient(135deg, #f59e0b, #d97706)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
marginBottom: '8px'
}}>
</Title>
<Text type="tertiary" style={{ fontSize: '14px' }}>线</Text>
</div>
)}
{/* 功能入口切换与调整入口 */}
{mode !== null && (
<div style={{ margin: '18px 0 14px' }}>
<Space spacing={16} align='center'>
<Select value={mode} onChange={(v) => setMode(v as any)}
optionList={[
{ value: 'generate', label: '生成流程日期' },
{ value: 'adjust', label: '调整流程日期' },
{ value: 'batch', label: '批量生成' }
]} />
{mode === 'adjust' && (
<Space spacing={12} align='center'>
<Button type="primary" onClick={async () => {
try {
const selection = await bitable.base.getSelection();
const recordId = selection?.recordId || '';
const tableId = selection?.tableId || '';
if (!recordId) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 'warning', message: '请先在数据表中选中一条货期记录' });
}
return;
}
if (tableId && tableId !== DELIVERY_RECORD_TABLE_ID) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 'warning', message: '请在货期记录表中选择记录' });
}
return;
}
setSelectedDeliveryRecordId(recordId);
await loadProcessDataFromDeliveryRecord(recordId);
} catch (e) {
console.error('读取当前选中记录失败:', e);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 'error', message: '读取当前选中记录失败' });
}
}
}}></Button>
</Space>
)}
{/* 批量处理功能 - 只在批量模式下显示 */}
</Space>
</div>
)}
{/* 时效计算结果模态框 */}
<Modal
title="预计开始和完成时间"
visible={timelineVisible}
onCancel={() => {
setTimelineVisible(false);
setTimelineAdjustments({}); // 关闭时重置调整
setDeliveryMarginDeductions(0); // 关闭时重置交期余量扣减
setCompletionDateAdjustment(0); // 关闭时重置最后流程完成日期调整
}}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space align="center">
<Text>()</Text>
<InputNumber
min={0}
step={1}
value={baseBufferDays}
onChange={(val) => {
const n = Number(val);
setBaseBufferDays(Number.isFinite(n) && n >= 0 ? n : 0);
}}
style={{ width: 90 }}
/>
<Button onClick={resetTimelineAdjustments}>
</Button>
</Space>
<Button
type="primary"
onClick={async () => {
try {
// 如果有当前批量记录信息,使用批量数据进行保存
if (currentBatchRecord) {
console.log('使用批量记录数据进行保存:', currentBatchRecord);
await saveTimelineData({
timelineResults: timelineResults,
selectedRecords: currentBatchRecord.selectedRecords,
recordDetails: currentBatchRecord.recordDetails,
timelineAdjustments: timelineAdjustments,
labels: currentBatchRecord.labels,
expectedDate: currentBatchRecord.expectedDate,
startTime: currentBatchRecord.startTime
});
} else {
// 普通模式保存
await saveTimelineData();
}
} catch (error) {
// 错误已在saveTimelineData中处理
}
setTimelineVisible(false);
// 清除批量记录信息
setCurrentBatchRecord(null);
}}
>
</Button>
</div>
}
width={900}
>
<div style={{ maxHeight: '80vh', overflowY: 'auto' }}>
{timelineResults.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px' }}>
<Text type="tertiary"></Text>
</div>
) : (
<div>
<div style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '12px' }}>
<Text strong></Text>
<DatePicker
style={{ width: 280 }}
placeholder="请选择起始时间"
value={startTime}
onChange={(date) => {
setStartTime(date);
}}
type="dateTime"
format="yyyy-MM-dd HH:mm"
/>
</div>
{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 (
<Card key={index} style={{ marginBottom: '8px', padding: '12px', position: 'relative' }}>
<div style={{ position: 'absolute', top: '12px', left: '12px', zIndex: 1, display: 'flex', alignItems: 'center', gap: '16px' }}>
<Title heading={6} style={{ margin: 0, color: '#1890ff', fontSize: '18px' }}>
{result.processOrder && `${result.processOrder}. `}{result.nodeName}
{result.isAccumulated && result.allMatchedRecords ? (
<span style={{
marginLeft: '8px',
fontSize: '12px',
color: '#000000',
fontWeight: 'normal'
}}>
( {result.allMatchedRecords.length} : {result.allMatchedRecords.map(record => record.recordId).join(', ')})
</span>
) : result.timelineRecordId ? (
<span style={{
marginLeft: '8px',
fontSize: '12px',
color: '#000000',
fontWeight: 'normal'
}}>
({result.timelineRecordId})
</span>
) : (
<span style={{
marginLeft: '8px',
fontSize: '12px',
color: '#ff4d4f',
fontWeight: 'normal'
}}>
()
</span>
)}
</Title>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Text style={{ fontSize: '14px', color: '#666', whiteSpace: 'nowrap' }}></Text>
<DatePicker
size="small"
type="dateTime"
format="yyyy-MM-dd HH:mm"
defaultPickerValue={(() => {
try {
const expectedEndStr = timelineResults?.[index]?.estimatedEnd;
if (expectedEndStr && typeof expectedEndStr === 'string' && !expectedEndStr.includes('未找到')) {
const expectedEnd = new Date(expectedEndStr);
if (!isNaN(expectedEnd.getTime())) {
const dv = new Date(expectedEnd);
dv.setSeconds(0, 0);
return dv;
}
}
} catch {}
return undefined;
})()}
placeholder="选择日期时间"
style={{ width: '200px' }}
value={actualCompletionDates[index] || null}
onChange={(date) => {
// 自动填充默认时分:仅在用户未指定具体时分时套用预计完成的时分
let nextDate = date ? new Date(date) : null;
try {
if (nextDate) {
const expectedEndStr = timelineResults?.[index]?.estimatedEnd;
if (expectedEndStr && typeof expectedEndStr === 'string' && !expectedEndStr.includes('未找到')) {
const expectedEnd = new Date(expectedEndStr);
if (!isNaN(expectedEnd.getTime())) {
const h = nextDate.getHours();
const m = nextDate.getMinutes();
const s = nextDate.getSeconds();
// 当用户未选择具体时间默认为00:00:00应用预计完成的时分
if (h === 0 && m === 0 && s === 0) {
nextDate.setHours(expectedEnd.getHours(), expectedEnd.getMinutes(), 0, 0);
}
}
}
}
} catch {}
setActualCompletionDates(prev => ({
...prev,
[index]: nextDate
}));
// 自动设置调整量:按工作日规则(考虑内部/外部、休息日、跳过日期)
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);
}}
/>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr', gap: '12px', marginTop: '40px' }}>
<div>
<Text strong style={{ display: 'block', marginBottom: '2px', fontSize: '15px' }}></Text>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
{/* 注释掉 -0.5 功能按钮 */}
{false && (
<Button
size="small"
onClick={() => handleComplexAdjustment(index, -0.5)}
disabled={adjustedValue <= 0 || isCurrentTurnoverZero}
style={{ minWidth: '28px', height: '24px', fontSize: '13px' }}
title={isCurrentTurnoverZero ? '周转周期为零,无法调整' : ''}
>
-0.5
</Button>
)}
{/* 注释掉 -1 功能按钮 */}
{false && (
<Button
size="small"
onClick={() => handleComplexAdjustment(index, -1)}
disabled={adjustedValue <= 0 || isCurrentTurnoverZero}
style={{ minWidth: '24px', height: '24px', fontSize: '13px' }}
title={isCurrentTurnoverZero ? '周转周期为零,无法调整' : ''}
>
-1
</Button>
)}
<div style={{
minWidth: '70px',
textAlign: 'center',
padding: '2px 6px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
backgroundColor: adjustment !== 0 ? '#fff7e6' : '#f5f5f5',
fontSize: '13px'
}}>
<Text style={{
color: adjustedValue > 0 ? '#52c41a' : '#ff4d4f',
fontWeight: 'bold',
fontSize: '13px'
}}>
{adjustedValue.toFixed(1)}
</Text>
{adjustment !== 0 && (
<div style={{ fontSize: '10px', color: '#666', lineHeight: '1.2' }}>
: {baseValue.toFixed(1)}
<br />
: {adjustment > 0 ? '+' : ''}{adjustment.toFixed(1)}
</div>
)}
</div>
{/* 注释掉 +1 功能按钮 */}
{false && (
<Button
size="small"
onClick={() => handleComplexAdjustment(index, 1)}
disabled={isCurrentTurnoverZero || (hasTurnoverNodeWithZero && !isTurnoverNode) || (isTurnoverNode && !isCurrentTurnoverZero) || hasNegativeBuffer}
style={{ minWidth: '24px', height: '24px', fontSize: '13px' }}
title={
isCurrentTurnoverZero ? '周转周期为零,无法调整' :
(hasTurnoverNodeWithZero && !isTurnoverNode) ? '周转周期为零,其他节点无法增加' :
(isTurnoverNode && !isCurrentTurnoverZero) ? '周转周期节点会自动调整,无法手动修改' :
hasNegativeBuffer ? `缓冲期已耗尽且交期余量不足,不允许增加时效` : ''
}
>
+1
</Button>
)}
{/* 注释掉 +0.5 功能按钮 */}
{false && (
<Button
size="small"
onClick={() => handleComplexAdjustment(index, 0.5)}
disabled={isCurrentTurnoverZero || (hasTurnoverNodeWithZero && !isTurnoverNode) || (isTurnoverNode && !isCurrentTurnoverZero) || hasNegativeBuffer}
style={{ minWidth: '28px', height: '24px', fontSize: '13px' }}
title={
isCurrentTurnoverZero ? '周转周期为零,无法调整' :
(hasTurnoverNodeWithZero && !isTurnoverNode) ? '周转周期为零,其他节点无法增加' :
(isTurnoverNode && !isCurrentTurnoverZero) ? '周转周期节点会自动调整,无法手动修改' :
hasNegativeBuffer ? `缓冲期已耗尽且交期余量不足,不允许增加时效` : ''
}
>
+0.5
</Button>
)}
</div>
</div>
<div>
<Text strong style={{ display: 'block', marginBottom: '2px', fontSize: '15px' }}></Text>
<Text style={{ fontSize: '14px' }}>{formatDate(result.estimatedStart)}</Text>
<div style={{ fontSize: '12px', color: '#666', marginTop: '1px' }}>
<Text>{getDayOfWeek(result.estimatedStart)}</Text>
</div>
</div>
<div>
<Text strong style={{ display: 'block', marginBottom: '2px', fontSize: '15px' }}></Text>
<Text style={{
color: result.estimatedEnd.includes('未找到') || result.estimatedEnd.includes('时效值为0') ? '#ff4d4f' : '#52c41a',
fontSize: '14px'
}}>
{result.estimatedEnd === '时效值为0' ? result.estimatedEnd : formatDate(result.estimatedEnd)}
</Text>
<div style={{ fontSize: '12px', color: '#666', marginTop: '1px' }}>
<Text>{getDayOfWeek(result.estimatedEnd)}</Text>
</div>
</div>
<div>
<Text strong style={{ display: 'block', marginBottom: '2px', fontSize: '15px' }}></Text>
<Text style={{ color: '#1890ff', fontSize: '14px' }}>
{calculateActualDays(result.estimatedStart, result.estimatedEnd)}
</Text>
<div style={{ fontSize: '12px', color: '#666', marginTop: '1px' }}>
<Text> {adjustedValue.toFixed(1)} </Text>
</div>
</div>
<div>
<Text strong style={{ display: 'block', marginBottom: '2px', fontSize: '15px' }}></Text>
<Text style={{
color: result.calculationMethod === '内部' ? '#1890ff' : '#52c41a',
fontSize: '14px'
}}>
{result.calculationMethod || '外部'}
{result.ruleDescription && (
<span style={{
marginLeft: '8px',
color: '#ff7a00',
fontSize: '12px',
fontStyle: 'italic'
}}>
({result.ruleDescription})
</span>
)}
<span
style={{
marginLeft: '4px',
cursor: 'help',
color: '#666',
fontSize: '12px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: '14px',
height: '14px',
borderRadius: '50%',
backgroundColor: '#f0f0f0',
border: '1px solid #d9d9d9'
}}
title={`计算方式详情:\n${result.calculationMethod === '内部' ? '内部计算 (9小时工作制)' : '外部计算 (24小时制)'}${result.ruleDescription ? `\n应用规则${result.ruleDescription}` : ''}\n已跳过周末${result.skippedWeekends || 0}\n${result.weekendDaysConfig && result.weekendDaysConfig.length > 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')}` : ''}`}
>
?
</span>
</Text>
<div style={{ fontSize: '12px', color: '#666', marginTop: '2px' }}>
<Text>{(result.actualExcludedDatesCount && result.actualExcludedDatesCount > 0) ? `${result.actualExcludedDatesCount}` : '无'}</Text>
</div>
</div>
</div>
</Card>
);
})}
{/* 计算公式展示卡片 */}
{timelineResults.length > 0 && (
<Card style={{
marginBottom: '16px',
padding: '16px',
background: 'linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%)',
border: '2px solid #1890ff',
borderRadius: '12px',
boxShadow: '0 4px 12px rgba(24, 144, 255, 0.15)'
}}>
<div style={{ textAlign: 'center' }}>
<Title heading={5} style={{
margin: '0 0 16px 0',
color: '#1890ff',
fontSize: '20px',
fontWeight: 'bold'
}}>
📅
</Title>
<div style={{
display: 'grid',
gridTemplateColumns: expectedDate ? '1fr auto 1fr auto 1fr auto 1fr auto 1fr' : '1fr auto 1fr auto 1fr auto 1fr',
alignItems: 'center',
gap: '12px',
marginBottom: '16px',
padding: '16px',
backgroundColor: 'rgba(255, 255, 255, 0.8)',
borderRadius: '8px',
border: '1px solid rgba(24, 144, 255, 0.2)'
}}>
{(() => {
// 获取有效的最后流程完成日期
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 && (
<>
{/* 客户期望日期 */}
<div style={{ textAlign: 'center' }}>
<Text strong style={{ display: 'block', fontSize: '14px', color: '#666', marginBottom: '4px' }}>
</Text>
<Text style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#722ed1',
display: 'block'
}}>
{formatDate(expectedDate)}
</Text>
<Text style={{ fontSize: '12px', color: '#999' }}>
{getDayOfWeek(expectedDate)}
</Text>
</div>
{/* 减号 */}
<div style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#1890ff',
display: 'flex',
alignItems: 'center'
}}>
-
</div>
{/* 最后流程完成 */}
<div style={{ textAlign: 'center' }}>
<Text strong style={{ display: 'block', fontSize: '14px', color: '#666', marginBottom: '4px' }}>
{effectiveLastProcess === timelineResults[timelineResults.length - 1]
? '最后流程完成'
: `有效流程完成 (${effectiveLastProcess.nodeName})`}
</Text>
<Text style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#52c41a',
display: 'block'
}}>
{formatDate(adjustedCompletionDate)}
</Text>
<Text style={{ fontSize: '12px', color: '#999' }}>
{getDayOfWeek(adjustedCompletionDate)}
</Text>
{effectiveLastProcess !== timelineResults[timelineResults.length - 1] && (
<Text style={{ fontSize: '11px', color: '#fa8c16', fontStyle: 'italic', display: 'block', marginTop: '2px' }}>
(0使)
</Text>
)}
{completionDateAdjustment > 0 && (
<Text style={{ fontSize: '11px', color: '#1890ff', fontStyle: 'italic', display: 'block', marginTop: '2px' }}>
( +{completionDateAdjustment})
</Text>
)}
</div>
{/* 加号 */}
<div style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#1890ff',
display: 'flex',
alignItems: 'center'
}}>
+
</div>
{/* 缓冲期 */}
<div style={{ textAlign: 'center' }}>
<Text strong style={{ display: 'block', fontSize: '14px', color: '#666', marginBottom: '4px' }}>
</Text>
<Text style={{
fontSize: '20px',
fontWeight: 'bold',
color: dynamicBufferDays < baseBufferDays ? '#ff4d4f' : '#fa8c16',
display: 'block'
}}>
{dynamicBufferDays}
</Text>
<Text style={{ fontSize: '12px', color: '#999' }}>
</Text>
</div>
{/* 等号 */}
<div style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#1890ff',
display: 'flex',
alignItems: 'center'
}}>
=
</div>
</>
)}
{!expectedDate && (
<>
{/* 最后流程完成 */}
<div style={{ textAlign: 'center' }}>
<Text strong style={{ display: 'block', fontSize: '14px', color: '#666', marginBottom: '4px' }}>
{effectiveLastProcess === timelineResults[timelineResults.length - 1]
? '最后流程完成'
: `有效流程完成 (${effectiveLastProcess.nodeName})`}
</Text>
<Text style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#52c41a',
display: 'block'
}}>
{formatDate(effectiveLastProcess.estimatedEnd)}
</Text>
<Text style={{ fontSize: '12px', color: '#999' }}>
{getDayOfWeek(effectiveLastProcess.estimatedEnd)}
</Text>
{effectiveLastProcess !== timelineResults[timelineResults.length - 1] && (
<Text style={{ fontSize: '11px', color: '#fa8c16', fontStyle: 'italic', display: 'block', marginTop: '2px' }}>
(0使)
</Text>
)}
</div>
{/* 加号 */}
<div style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#1890ff',
display: 'flex',
alignItems: 'center'
}}>
+
</div>
{/* 缓冲期 */}
<div style={{ textAlign: 'center' }}>
<Text strong style={{ display: 'block', fontSize: '14px', color: '#666', marginBottom: '4px' }}>
</Text>
<Text style={{
fontSize: '20px',
fontWeight: 'bold',
color: dynamicBufferDays < baseBufferDays ? '#ff4d4f' : '#fa8c16',
display: 'block'
}}>
{dynamicBufferDays}
</Text>
<Text style={{ fontSize: '12px', color: '#999' }}>
</Text>
</div>
{/* 减号 */}
<div style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#1890ff',
display: 'flex',
alignItems: 'center'
}}>
-
</div>
{/* 今天 */}
<div style={{ textAlign: 'center' }}>
<Text strong style={{ display: 'block', fontSize: '14px', color: '#666', marginBottom: '4px' }}>
</Text>
<Text style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#666',
display: 'block'
}}>
{formatDate(new Date())}
</Text>
<Text style={{ fontSize: '12px', color: '#999' }}>
{getDayOfWeek(new Date())}
</Text>
</div>
{/* 等号 */}
<div style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#1890ff',
display: 'flex',
alignItems: 'center'
}}>
=
</div>
</>
)}
{/* 结果日期说明(位于等式下方) */}
<div style={{textAlign:'center', marginTop:'8px'}}>
<Text style={{ fontSize: '12px', color: '#999' }}>
+ {formatDate(deliveryDate)}{getDayOfWeek(deliveryDate)}
</Text>
</div>
{/* 交期余量结果 */}
<div style={{
textAlign: 'center',
padding: '12px',
backgroundColor: finalDaysDiff >= 0 ? '#f6ffed' : '#fff2f0',
borderRadius: '8px',
border: `2px solid ${finalDaysDiff >= 0 ? '#52c41a' : '#ff4d4f'}`
}}>
<Text strong style={{ display: 'block', fontSize: '14px', color: '#666', marginBottom: '4px' }}>
</Text>
<Text style={{
fontSize: '24px',
fontWeight: 'bold',
color: finalDaysDiff >= 0 ? '#52c41a' : '#ff4d4f',
display: 'block'
}}>
{finalDaysDiff >= 0 ? '+' : ''}{finalDaysDiff}
</Text>
<Text style={{
fontSize: '12px',
color: finalDaysDiff >= 0 ? '#52c41a' : '#ff4d4f',
fontWeight: 'bold'
}}>
{finalDaysDiff >= 0 ? '✅ 时间充裕' : '⚠️ 时间紧张'}
</Text>
</div>
</>
);
})()}
</div>
<div style={{
padding: '12px',
backgroundColor: 'rgba(255, 255, 255, 0.6)',
borderRadius: '6px',
fontSize: '13px',
color: '#666',
lineHeight: '1.5'
}}>
{(() => {
// 计算动态缓冲期和总调整量
const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0);
const baseBuferDays = baseBufferDays;
const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments);
return (
<Text>
💡 <strong></strong>
{expectedDate
? `交期余量 = 客户期望日期 - (最后流程完成日期 + ${dynamicBufferDays}天缓冲期)`
: `交期余量 = (最后流程完成日期 + ${dynamicBufferDays}天缓冲期) - 今天`
}
<br />
{expectedDate
? '• 正值表示有充裕时间,负值表示可能延期'
: '• 显示预计交付日期距离今天的天数'
}
<br />
= {baseBuferDays} - 0
{totalAdjustments > 0 && (
<>
<br />
+{totalAdjustments}
</>
)}
</Text>
);
})()}
</div>
</div>
</Card>
)}
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: '#f6f8fa', borderRadius: '6px' }}>
<Text strong>📊 </Text>
<Text style={{ display: 'block', marginTop: '4px' }}>
{timelineResults.length} 使
</Text>
<div style={{ marginTop: '8px', padding: '8px', backgroundColor: '#e6f7ff', borderRadius: '4px' }}>
<Text strong style={{ color: '#1890ff' }}>🗓 </Text>
<Text style={{ display: 'block', marginTop: '4px', fontSize: '14px' }}>
<strong></strong>99:00-18:00
<br />
<strong></strong>24
<br />
<br />
<br />
"工作日"
<br />
使 +1/-1 +0.5/-0.5
<br />
</Text>
</div>
{Object.keys(timelineAdjustments).length > 0 && (
<div style={{ marginTop: '8px', padding: '8px', backgroundColor: '#fff7e6', borderRadius: '4px' }}>
<Text strong style={{ color: '#fa8c16' }}></Text>
<div style={{ marginTop: '4px' }}>
{Object.entries(timelineAdjustments).map(([nodeIndex, adjustment]) => {
const nodeName = timelineResults[parseInt(nodeIndex)]?.nodeName;
return (
<Text key={nodeIndex} style={{ display: 'block', fontSize: '12px' }}>
{nodeName}: {adjustment > 0 ? '+' : ''}{adjustment.toFixed(1)}
</Text>
);
})}
</div>
</div>
)}
</div>
</div>
)}
</div>
</Modal>
{/* 标签选择部分,仅在生成模式显示 */}
{mode === 'generate' && labelOptions && Object.keys(labelOptions).length > 0 && (
<Card title="标签选择" className="card-enhanced" style={{ marginBottom: '24px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px' }}>
{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 (
<div key={labelKey}>
<Text strong style={{ display: 'block', marginBottom: '8px' }}>{labelKey}</Text>
<Select
className="select-enhanced"
style={{ width: '100%' }}
placeholder={`请选择${labelKey}或输入关键字搜索`}
value={selectedLabels[labelKey]}
onChange={(value) => handleLabelChange(labelKey, value)}
multiple={isMultiSelect}
filter
showSearch
>
{options.map((option, optionIndex) => (
<Select.Option key={optionIndex} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
</div>
);
})}
</div>
{/* 客户期望日期选择 */}
<div style={{ marginTop: '24px' }}>
<Text strong style={{ display: 'block', marginBottom: '8px' }}></Text>
<DatePicker
className="input-enhanced"
style={{ width: '300px' }}
placeholder="请选择客户期望日期"
value={expectedDate}
onChange={(date) => setExpectedDate(date)}
format="yyyy-MM-dd"
disabledDate={(date) => {
// 禁用今天之前的日期
const today = new Date();
today.setHours(0, 0, 0, 0);
return date < today;
}}
/>
{expectedDate && (
<Text type="secondary" style={{ marginLeft: '12px' }}>
{formatDate(expectedDate, 'CHINESE_DATE')}
</Text>
)}
</div>
{Object.keys(selectedLabels).length > 0 && (
<div style={{
marginTop: '24px',
padding: '20px',
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
borderRadius: '12px',
border: '1px solid #bae6fd'
}}>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<Button
type='primary'
className="btn-gradient-calculate"
size="large"
onClick={handleCalculateTimeline}
loading={timelineLoading}
disabled={timelineLoading}
style={{ minWidth: '160px' }}
>
</Button>
<Text type="secondary" style={{ fontSize: '14px' }}>
{Object.keys(selectedLabels).length}
</Text>
</Space>
<div style={{ marginTop: '16px' }}>
<Text strong style={{ color: '#0369a1' }}></Text>
<div style={{ marginTop: '8px', display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{Object.entries(selectedLabels).map(([key, value]) => {
const displayValue = Array.isArray(value) ? value.join(', ') : value;
return (
<span
key={key}
style={{
background: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
color: 'white',
padding: '4px 12px',
borderRadius: '20px',
fontSize: '12px',
fontWeight: 500
}}
>
{key}: {displayValue}
</span>
);
})}
</div>
</div>
</div>
)}
</Card>
)}
{mode === 'batch' && (
<main className="main" style={{ padding: '20px' }}>
{/* 批量处理配置 */}
<div className="batch-config-card" style={{
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
padding: '24px',
borderRadius: '16px',
border: '1px solid #e2e8f0',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.05)',
marginTop: '16px'
}}>
<Title heading={4} style={{ marginBottom: '20px', color: '#334155' }}>
</Title>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{/* 第一行:表和视图选择 */}
<div style={{ display: 'flex', gap: '20px', flexWrap: 'wrap', alignItems: 'center' }}>
<div className="batch-input-group" style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'white',
padding: '12px 16px',
borderRadius: '12px',
border: '1px solid #e2e8f0',
minWidth: '280px'
}}>
<Text style={{ fontWeight: 500, color: '#475569', minWidth: '60px' }}>:</Text>
<Select
value={selectedBatchTableId}
onChange={handleBatchTableChange}
placeholder="请选择表"
loading={tablesLoading}
style={{ flex: 1, minWidth: '180px' }}
>
{availableTables.map(table => (
<Select.Option key={table.id} value={table.id}>
{table.name}
</Select.Option>
))}
</Select>
</div>
<div className="batch-input-group" style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'white',
padding: '12px 16px',
borderRadius: '12px',
border: '1px solid #e2e8f0',
minWidth: '280px'
}}>
<Text style={{ fontWeight: 500, color: '#475569', minWidth: '60px' }}>:</Text>
<Select
value={selectedBatchViewId}
onChange={(value) => setSelectedBatchViewId(value)}
placeholder="请选择视图(可选)"
loading={viewsLoading}
style={{ flex: 1, minWidth: '180px' }}
allowClear
>
{availableViews.map(view => (
<Select.Option key={view.id} value={view.id}>
{view.name}
</Select.Option>
))}
</Select>
</div>
</div>
{/* 第二行:处理行数和操作按钮 */}
<div style={{ display: 'flex', gap: '20px', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap' }}>
<div className="batch-input-group" style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'white',
padding: '12px 16px',
borderRadius: '12px',
border: '1px solid #e2e8f0'
}}>
<Text style={{ fontWeight: 500, color: '#475569' }}>:</Text>
<InputNumber
value={batchRowCount}
onChange={(value) => setBatchRowCount(value || 10)}
min={1}
max={100}
style={{ width: 100 }}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{batchProcessing && (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'rgba(59, 130, 246, 0.1)',
padding: '8px 16px',
borderRadius: '8px',
border: '1px solid rgba(59, 130, 246, 0.2)'
}}>
<div style={{
width: '12px',
height: '12px',
borderRadius: '50%',
background: 'conic-gradient(from 0deg, #3b82f6, transparent)',
animation: 'spin 1s linear infinite'
}}></div>
<Text style={{ color: '#3b82f6', fontWeight: 500 }}>
: {batchProgress.current}/{batchProgress.total}
</Text>
</div>
)}
<Button
type="primary"
theme="solid"
size="large"
loading={batchProcessing}
onClick={() => handleBatchProcess(batchRowCount)}
disabled={batchProcessing || !selectedBatchTableId}
style={{
background: selectedBatchTableId ? 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)' : undefined,
border: 'none',
borderRadius: '12px',
padding: '12px 24px',
fontWeight: 600,
boxShadow: selectedBatchTableId ? '0 4px 12px rgba(59, 130, 246, 0.3)' : undefined
}}
>
{batchProcessing ? '批量处理中...' : '开始批量生成'}
</Button>
</div>
</div>
</div>
</div>
</main>
)}
{mode === 'generate' && (
<main className="main" style={{ padding: '20px' }}>
<Card className="card-enhanced">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '24px' }}>
<div>
<Title heading={3} style={{
margin: 0,
background: 'linear-gradient(135deg, #1890ff 0%, #722ed1 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text'
}}></Title>
<Text type='secondary' style={{ fontSize: '14px', marginTop: '4px' }}>
</Text>
{selectedRecords.length > 0 && (
<Text type='secondary' style={{ fontSize: '12px', marginTop: '4px' }}>
{selectedRecords.length}
</Text>
)}
</div>
<Space>
<Button
type='primary'
className="btn-gradient-select"
size="large"
onClick={handleSelectRecords}
loading={loading}
disabled={loading}
>
{selectedRecords.length > 0 ? '重新选择' : '选择记录'}
</Button>
{selectedRecords.length > 0 && (
<Button
type='secondary'
onClick={handleClearRecords}
size='small'
>
</Button>
)}
</Space>
</div>
{/* 已选择记录的详细信息 */}
{selectedRecords.length > 0 && recordDetails.length > 0 && (
<div style={{
padding: '16px',
background: 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)',
borderRadius: '12px',
border: '1px solid #dee2e6',
marginBottom: '20px',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div>
<Text strong style={{ color: '#495057' }}>:</Text>
<span style={{
marginLeft: '8px',
fontSize: '12px',
background: 'linear-gradient(135deg, #007bff 0%, #0056b3 100%)',
color: 'white',
padding: '2px 8px',
borderRadius: '12px',
fontWeight: 500
}}>
{recordDetails[0].displayValue || recordDetails[0].id}
</span>
</div>
{recordDetails.length > 1 && (
<div>
<Text type='secondary'>+ {recordDetails.length - 1} </Text>
</div>
)}
</div>
</div>
)}
{/* 加载状态 */}
{loading && (
<div style={{ textAlign: 'center', padding: '20px' }}>
<Spin size="large" />
<div style={{ marginTop: '10px' }}>
<Text>...</Text>
</div>
</div>
)}
{/* 空状态提示 */}
{selectedRecords.length === 0 && !loading && (
<div style={{
textAlign: 'center',
padding: '20px',
backgroundColor: '#fafafa',
borderRadius: '6px',
border: '1px dashed #d9d9d9'
}}>
<Text type="tertiary"></Text>
</div>
)}
</Card>
{/* 批量处理配置 - 移动到版单数据下方 */}
{mode === 'batch' && (
<div className="batch-config-card" style={{
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
padding: '24px',
borderRadius: '16px',
border: '1px solid #e2e8f0',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.05)',
marginTop: '16px'
}}>
<Title heading={4} style={{ marginBottom: '20px', color: '#334155' }}>
</Title>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{/* 第一行:表和视图选择 */}
<div style={{ display: 'flex', gap: '20px', flexWrap: 'wrap', alignItems: 'center' }}>
<div className="batch-input-group" style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'white',
padding: '12px 16px',
borderRadius: '12px',
border: '1px solid #e2e8f0',
minWidth: '280px'
}}>
<Text style={{ fontWeight: 500, color: '#475569', minWidth: '60px' }}>:</Text>
<Select
value={selectedBatchTableId}
onChange={handleBatchTableChange}
placeholder="请选择表"
loading={tablesLoading}
style={{ flex: 1, minWidth: '180px' }}
>
{availableTables.map(table => (
<Select.Option key={table.id} value={table.id}>
{table.name}
</Select.Option>
))}
</Select>
</div>
<div className="batch-input-group" style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'white',
padding: '12px 16px',
borderRadius: '12px',
border: '1px solid #e2e8f0',
minWidth: '280px'
}}>
<Text style={{ fontWeight: 500, color: '#475569', minWidth: '60px' }}>:</Text>
<Select
value={selectedBatchViewId}
onChange={(value) => setSelectedBatchViewId(value)}
placeholder="请选择视图(可选)"
loading={viewsLoading}
style={{ flex: 1, minWidth: '180px' }}
allowClear
>
{availableViews.map(view => (
<Select.Option key={view.id} value={view.id}>
{view.name}
</Select.Option>
))}
</Select>
</div>
</div>
{/* 第二行:处理行数和操作按钮 */}
<div style={{ display: 'flex', gap: '20px', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap' }}>
<div className="batch-input-group" style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'white',
padding: '12px 16px',
borderRadius: '12px',
border: '1px solid #e2e8f0'
}}>
<Text style={{ fontWeight: 500, color: '#475569' }}>:</Text>
<InputNumber
value={batchRowCount}
onChange={(value) => setBatchRowCount(value || 10)}
min={1}
max={100}
style={{ width: 100 }}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{batchProcessing && (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'rgba(59, 130, 246, 0.1)',
padding: '8px 16px',
borderRadius: '8px',
border: '1px solid rgba(59, 130, 246, 0.2)'
}}>
<div style={{
width: '12px',
height: '12px',
borderRadius: '50%',
background: 'conic-gradient(from 0deg, #3b82f6, transparent)',
animation: 'spin 1s linear infinite'
}}></div>
<Text style={{ color: '#3b82f6', fontWeight: 500 }}>
: {batchProgress.current}/{batchProgress.total}
</Text>
</div>
)}
<Button
type="primary"
theme="solid"
size="large"
loading={batchProcessing}
onClick={() => handleBatchProcess(batchRowCount)}
disabled={batchProcessing || !selectedBatchTableId}
style={{
background: selectedBatchTableId ? 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)' : undefined,
border: 'none',
borderRadius: '12px',
padding: '12px 24px',
fontWeight: 600,
boxShadow: selectedBatchTableId ? '0 4px 12px rgba(59, 130, 246, 0.3)' : undefined
}}
>
{batchProcessing ? '批量处理中...' : '开始批量生成'}
</Button>
</div>
</div>
</div>
</div>
)}
</main>
)}
{mode === 'generate' && (
<>
{/* 面料数据查询结果 */}
{queryResults.length > 0 && (
<>
<Divider />
<Title heading={4}> ({queryResults.length} )</Title>
<Table
columns={columns}
dataSource={queryResults.map((item, index) => ({ ...item, key: index }))}
pagination={{ pageSize: 10 }}
style={{ marginTop: '10px' }}
/>
</>
)}
{/* 二次工艺查询结果 */}
{secondaryProcessResults.length > 0 && (
<>
<Divider />
<Title heading={4}> ({secondaryProcessResults.length} )</Title>
<Table
columns={secondaryProcessColumns}
dataSource={secondaryProcessResults.map((item, index) => ({ ...item, key: index }))}
pagination={{ pageSize: 10 }}
style={{ marginTop: '10px' }}
/>
</>
)}
{/* 工艺价格查询结果 */}
{pricingDetailsResults.length > 0 && (
<>
<Divider />
<Title heading={4}> ({pricingDetailsResults.length} )</Title>
<Table
columns={pricingDetailsColumns}
dataSource={pricingDetailsResults.map((item, index) => ({ ...item, key: index }))}
pagination={{ pageSize: 10 }}
style={{ marginTop: '10px' }}
/>
</>
)}
</>
)}
</div>
);
}