2026-03-13 14:41:57 +08:00

8142 lines
359 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, ToastType } from '@lark-base-open/js-sdk';
import { Button, Typography, List, Card, Space, Divider, Spin, Table, Select, Modal, DatePicker, InputNumber, Input, Progress, Switch, Tag } from '@douyinfe/semi-ui';
import { useState, useEffect, useRef } from 'react';
import { addDays, format, differenceInCalendarDays } 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 (typeof val === 'number') {
return String(val);
} else if (val && typeof val === 'object') {
return val.text || val.name || val.value?.toString?.() || '';
}
return '';
};
export default function App() {
type ProcessGroupInstance = {
id: string;
groupName: string;
displayName: string;
};
const nextProcessGroupInstanceIdRef = useRef(1);
const createProcessGroupInstance = (groupName: string, displayName?: string): ProcessGroupInstance => {
const name = (groupName || '').trim();
const id = `${Date.now()}_${nextProcessGroupInstanceIdRef.current++}`;
return { id, groupName: name, displayName: (displayName || name).trim() || name };
};
const deriveGroupOrderDraftFromTimelineResults = (results: any[]): ProcessGroupInstance[] => {
const arr = Array.isArray(results) ? results : [];
const seen = new Set<string>();
const out: ProcessGroupInstance[] = [];
for (const r of arr) {
const g = (typeof r?.processGroup === 'string' ? r.processGroup : extractText(r?.processGroup)).trim();
if (!g || seen.has(g)) continue;
seen.add(g);
out.push(createProcessGroupInstance(g));
}
return out;
};
const deriveGroupOrderDraftByProcessOrder = (nodes: any[]): ProcessGroupInstance[] => {
const arr = Array.isArray(nodes) ? nodes : [];
const groupToMinOrder = new Map<string, number>();
const groupToFirstIndex = new Map<string, number>();
for (let i = 0; i < arr.length; i++) {
const r: any = arr[i];
const g = (typeof r?.processGroup === 'string' ? r.processGroup : extractText(r?.processGroup)).trim();
if (!g) continue;
if (!groupToFirstIndex.has(g)) groupToFirstIndex.set(g, i);
const n = Number(r?.processOrder);
if (Number.isFinite(n)) {
const prev = groupToMinOrder.get(g);
if (typeof prev === 'undefined' || n < prev) groupToMinOrder.set(g, n);
}
}
const groups = Array.from(groupToFirstIndex.keys());
groups.sort((a, b) => {
const ao = groupToMinOrder.get(a);
const bo = groupToMinOrder.get(b);
const aHas = typeof ao !== 'undefined';
const bHas = typeof bo !== 'undefined';
if (aHas && bHas) {
if (ao !== bo) return ao - bo;
} else if (aHas !== bHas) {
return aHas ? -1 : 1;
}
const ai = groupToFirstIndex.get(a) || 0;
const bi = groupToFirstIndex.get(b) || 0;
if (ai !== bi) return ai - bi;
return a.localeCompare(b, 'zh-Hans-CN');
});
return groups.map(g => createProcessGroupInstance(g));
};
const applyGroupOrderConfigToTimelineResults = (results: any[], config: ProcessGroupInstance[]) => {
const base = Array.isArray(results) ? results : [];
const order = Array.isArray(config) ? config : [];
if (base.length === 0 || order.length === 0) return base;
const configuredBaseNames = new Set<string>();
for (const inst of order) {
const bn = (inst?.groupName || '').trim();
if (bn) configuredBaseNames.add(bn);
}
const baseNameToNodes = new Map<string, any[]>();
for (const r of base) {
const bn = (typeof r?.processGroup === 'string' ? r.processGroup : extractText(r?.processGroup)).trim();
if (!bn) continue;
if (!baseNameToNodes.has(bn)) baseNameToNodes.set(bn, []);
baseNameToNodes.get(bn)!.push(r);
}
const expanded: any[] = [];
for (const inst of order) {
const bn = (inst?.groupName || '').trim();
if (!bn) continue;
const nodes = baseNameToNodes.get(bn) || [];
for (const n of nodes) {
expanded.push({
...n,
processGroupBase: bn,
processGroupInstanceId: inst.id,
processGroupInstanceName: inst.displayName,
});
}
}
const leftovers = base.filter(r => {
const bn = (typeof r?.processGroup === 'string' ? r.processGroup : extractText(r?.processGroup)).trim();
return bn ? !configuredBaseNames.has(bn) : true;
});
return expanded.concat(leftovers);
};
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 [labelAdjustmentFlow, setLabelAdjustmentFlow] = useState(false);
const [selectedLabels, setSelectedLabels] = useState<{[key: string]: string | string[]}>({});
const [labelLoading, setLabelLoading] = useState(false);
const [excludedDatesOverride, setExcludedDatesOverride] = useState<string[]>([]);
const [excludedDatesOverrideText, setExcludedDatesOverrideText] = useState<string>('');
const [excludedDatesByNodeOverride, setExcludedDatesByNodeOverride] = useState<Record<string, string[]>>({});
const excludedDatesByNodeOverrideRef = useRef<Record<string, string[]>>({});
const pendingRecalculateAfterExcludedDatesRef = useRef(false);
const pendingRecalculateAfterCalculateRef = useRef(false);
const pendingRecalculateAfterCalculateAdjustmentsRef = useRef<Record<string, number> | null>(null);
const [excludedDatesAdjustVisible, setExcludedDatesAdjustVisible] = useState(false);
const [excludedDatesByNodeDraft, setExcludedDatesByNodeDraft] = useState<Record<string, string[]>>({});
const [excludedDatesAddDraft, setExcludedDatesAddDraft] = useState<Record<string, Date | null>>({});
const pendingGroupConfigCalcRef = useRef<{
overrideData?: {
selectedRecords?: string[],
recordDetails?: any[],
selectedLabels?: { [key: string]: string | string[] },
expectedDate?: Date | null,
startTime?: Date | null,
excludedDates?: string[]
},
showUI: boolean
} | null>(null);
const skipNextGroupConfigPopupRef = useRef(false);
// 客户期望日期状态
const [expectedDate, setExpectedDate] = useState<Date | null>(null);
// 起始时间状态(从货期记录表获取,新记录则使用当前时间)
const [startTime, setStartTime] = useState<Date | null>(null);
const [timelineDirection, setTimelineDirection] = useState<'forward' | 'backward'>('forward');
const [calculatedRequiredStartTime, setCalculatedRequiredStartTime] = useState<Date | null>(null);
const [allocationVisible, setAllocationVisible] = useState(false);
const [allocationMode, setAllocationMode] = useState<'auto' | 'manual'>('auto');
const [allocationExtraDays, setAllocationExtraDays] = useState<number>(0);
const [allocationDraft, setAllocationDraft] = useState<{[key: number]: number}>({});
const [allocationNodesSnapshot, setAllocationNodesSnapshot] = useState<any[]>([]);
const [allocationExcluded, setAllocationExcluded] = useState<{[key: number]: boolean}>({});
const [groupOrderConfig, setGroupOrderConfig] = useState<ProcessGroupInstance[]>([]);
const [groupConfigVisible, setGroupConfigVisible] = useState(false);
const [groupOrderDraft, setGroupOrderDraft] = useState<ProcessGroupInstance[]>([]);
const [draggingGroupIndex, setDraggingGroupIndex] = useState<number | null>(null);
// 预览相关状态(已移除未使用的 previewLoading 状态)
// 时效计算相关状态
const [timelineVisible, setTimelineVisible] = useState(false);
const [timelineLoading, setTimelineLoading] = useState(false);
const [timelineResults, setTimelineResults] = useState<any[]>([]);
const [timelineAdjustments, setTimelineAdjustments] = useState<Record<string, number>>({});
// 交期余量扣减状态:记录从交期余量中扣减的天数
const [deliveryMarginDeductions, setDeliveryMarginDeductions] = useState<number>(0);
// 最后流程完成日期调整状态:记录最后流程完成日期增加的天数
const [completionDateAdjustment, setCompletionDateAdjustment] = useState<number>(0);
// 实际完成日期状态:记录每个节点的实际完成日期
const [actualCompletionDates, setActualCompletionDates] = useState<{[key: number]: Date | null}>({});
// 基础缓冲期天数可配置用于计算动态缓冲期默认0天
const [baseBufferDays, setBaseBufferDays] = useState<number>(0);
const [lockedExpectedDeliveryDateTs, setLockedExpectedDeliveryDateTs] = useState<number | null>(null);
const [isExpectedDeliveryDateLocked, setIsExpectedDeliveryDateLocked] = useState<boolean>(false);
// 快照回填来源foreign_id、款式、颜色、文本2
const [currentForeignId, setCurrentForeignId] = useState<string | null>(null);
const [currentStyleText, setCurrentStyleText] = useState<string>('');
const [currentColorText, setCurrentColorText] = useState<string>('');
// 标题中的款号/颜色编辑开关:默认锁定,点击笔按钮开放编辑
const [styleColorEditable, setStyleColorEditable] = useState<boolean>(false);
const [currentText2, setCurrentText2] = useState<string>('');
const [currentVersionNumber, setCurrentVersionNumber] = useState<number | null>(null);
const [currentDeliveryRecordId, setCurrentDeliveryRecordId] = useState<string | null>(null);
// 功能入口模式与调整相关状态
const [mode, setMode] = useState<'generate' | 'adjust' | null>(null);
const [modeSelectionVisible, setModeSelectionVisible] = useState(true);
const [adjustLoading, setAdjustLoading] = useState(false);
const [batchModalVisible, setBatchModalVisible] = useState(false);
const [batchStartRow, setBatchStartRow] = useState<number>(1);
const [batchEndRow, setBatchEndRow] = useState<number>(1);
const [batchTotalRows, setBatchTotalRows] = useState<number>(0);
const [batchLoading, setBatchLoading] = useState(false);
const [batchProcessedCount, setBatchProcessedCount] = useState<number>(0);
const [batchProcessingTotal, setBatchProcessingTotal] = useState<number>(0);
const [batchSuccessCount, setBatchSuccessCount] = useState<number>(0);
const [batchFailureCount, setBatchFailureCount] = useState<number>(0);
const [batchProgressList, setBatchProgressList] = useState<{ index: number; foreignId: string; status: 'success' | 'failed'; message?: string }[]>([]);
const [batchCurrentRowInfo, setBatchCurrentRowInfo] = useState<{ index: number; foreignId: string; style: string; color: string } | null>(null);
const batchAbortRef = useRef<boolean>(false);
const lastBufferDeficitRef = useRef<number>(0);
// 删除未使用的 deliveryRecords 状态
const [selectedDeliveryRecordId, setSelectedDeliveryRecordId] = useState<string>('');
// 从货期记录读取到的record_ids用于保存回写
const [restoredRecordIds, setRestoredRecordIds] = useState<string[]>([]);
// 原始文本格式的record_ids不做JSON化写回
const [restoredRecordIdsText, setRestoredRecordIdsText] = useState<string>('');
// 已移除:批量处理与表/视图选择相关状态
// 全局变量重置:在切换功能或切换版单/批量数据时,清空页面与计算相关状态
const resetGlobalState = (opts?: { resetMode?: boolean }) => {
// 运行时加载状态
setLoading(false);
setQueryLoading(false);
setSecondaryProcessLoading(false);
setPricingDetailsLoading(false);
setLabelLoading(false);
setAdjustLoading(false);
setTimelineLoading(false);
// 页面与计算数据
setSelectedRecords([]);
setRecordDetails([]);
setSelectedLabels({});
setExcludedDatesOverride([]);
setExcludedDatesOverrideText('');
setExcludedDatesByNodeOverride({});
excludedDatesByNodeOverrideRef.current = {};
setExcludedDatesAdjustVisible(false);
setExcludedDatesByNodeDraft({});
setExcludedDatesAddDraft({});
setExpectedDate(null);
setStartTime(null);
setCalculatedRequiredStartTime(null);
setAllocationVisible(false);
setAllocationExtraDays(0);
setAllocationDraft({});
setAllocationExcluded({});
setAllocationNodesSnapshot([]);
setTimelineVisible(false);
setTimelineResults([]);
setTimelineAdjustments({});
// 新增:重置固定缓冲期、实际完成日期以及一次性建议缓冲期应用标志
setBaseBufferDays(0);
setActualCompletionDates({});
setHasAppliedSuggestedBuffer(false);
setIsRestoringSnapshot(false);
setRestoredRecordIds([]);
setRestoredRecordIdsText('');
setLockedExpectedDeliveryDateTs(null);
setIsExpectedDeliveryDateLocked(false);
// 重置初始快照捕获状态
try {
hasCapturedInitialSnapshotRef.current = false;
initialSnapshotRef.current = null;
} catch {}
try {
lastBufferDeficitRef.current = 0;
} catch {}
// 移除:批量模式当前记录信息
// 当前回填状态
setCurrentForeignId(null);
setCurrentStyleText('');
setCurrentColorText('');
setCurrentText2('');
setCurrentVersionNumber(null);
setCurrentDeliveryRecordId(null);
setLabelAdjustmentFlow(false);
setGroupConfigVisible(false);
setGroupOrderDraft([]);
setDraggingGroupIndex(null);
pendingGroupConfigCalcRef.current = null;
// 可选:重置模式
if (opts?.resetMode) {
setMode(null);
setModeSelectionVisible(true);
}
};
// 指定的数据表ID和视图ID
const TABLE_ID = 'tblPIJ7unndydSMu';
// 当弹窗打开时,默认从记录/选择数据预填款号与颜色(不覆盖已有值)
const ensureStyleColorDefaults = async () => {
try {
const needStyle = !currentStyleText || currentStyleText.trim() === '';
const needColor = !currentColorText || currentColorText.trim() === '';
if (!needStyle && !needColor) return;
// 优先使用已读取的记录详情
const currentRecordDetails = recordDetails;
if (currentRecordDetails && currentRecordDetails.length > 0) {
const first = currentRecordDetails[0];
const styleVal = needStyle ? extractText(first?.fields?.['fld6Uw95kt']) || '' : currentStyleText;
const colorVal = needColor ? extractText(first?.fields?.['flde85ni4O']) || '' : currentColorText;
if (needStyle && styleVal) setCurrentStyleText(styleVal);
if (needColor && colorVal) setCurrentColorText(colorVal);
return;
}
// 其次使用当前选择记录,做一次读取
if (selectedRecords && selectedRecords.length > 0) {
try {
const table = await bitable.base.getTable(TABLE_ID);
const firstRecord = await table.getRecordById(selectedRecords[0]);
const styleVal2 = needStyle ? extractText(firstRecord?.fields?.['fld6Uw95kt']) || '' : currentStyleText;
const colorVal2 = needColor ? extractText(firstRecord?.fields?.['flde85ni4O']) || '' : currentColorText;
if (needStyle && styleVal2) setCurrentStyleText(styleVal2);
if (needColor && colorVal2) setCurrentColorText(colorVal2);
} catch (e) {
console.warn('读取所选记录的款号/颜色失败,保持现有值', e);
}
}
} catch (e) {
console.warn('预填款号/颜色失败', e);
}
};
useEffect(() => {
if (timelineVisible) {
// 打开弹窗时进行一次预填,避免手动输入
ensureStyleColorDefaults();
}
}, [timelineVisible]);
useEffect(() => {
excludedDatesByNodeOverrideRef.current = excludedDatesByNodeOverride || {};
}, [excludedDatesByNodeOverride]);
useEffect(() => {
if (!groupConfigVisible && groupOrderConfig.length > 0 && pendingGroupConfigCalcRef.current) {
const payload = pendingGroupConfigCalcRef.current;
pendingGroupConfigCalcRef.current = null;
setTimelineLoading(true);
handleCalculateTimeline(true, payload?.overrideData, payload?.showUI ?? true);
}
}, [groupConfigVisible, groupOrderConfig]);
const VIEW_ID = 'vewb28sjuX';
// 标签表ID
const LABEL_TABLE_ID = 'tblPnQscqwqopJ8V';
const PROCESS_CONFIG_TABLE_ID = 'tblMygOc6T9o4sYU';
const NODE_NAME_FIELD_ID = 'fld0g9L9Fw';
const PROCESS_LABEL_FIELD_ID = 'fldrVTa23X';
const PROCESS_ORDER_FIELD_ID = 'fldbfJQ4Zs';
const WEEKEND_DAYS_FIELD_ID = 'fld2BvjbIN';
const START_DATE_RULE_FIELD_ID = 'fld0KsQ2j3';
const DATE_ADJUSTMENT_RULE_FIELD_ID = 'fld0KsQ2j3';
const EXCLUDED_DATES_FIELD_ID = 'fldGxzC5uG';
const PROCESS_GROUP_FIELD_ID = 'fldtjxS4oO';
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
const TIMELINE_LABEL12_FIELD_ID = 'fldZZmb2LV'; // 标签12时效数据表
// 新表ID批量生成表
const BATCH_TABLE_ID = 'tblXO7iSxBYxrqtY';
const BATCH_ROW_NUMBER_FIELD_ID = 'fldiqlTVsU';
const BATCH_LABEL11_FIELD_ID = 'fld4BZHtBV';
const BATCH_LABEL12_FIELD_ID = 'fldnRlMeaD';
const BATCH_BUFFER_FIELD_ID = 'fldLBXEAo0';
const activateTableForPaging = async (table: any) => {
try {
if (typeof (table as any)?.getRecordIdListByPage === 'function') {
await (table as any).getRecordIdListByPage({ pageSize: 1 });
return;
}
} catch {}
try {
if (typeof (table as any)?.getRecordIdList === 'function') {
await (table as any).getRecordIdList();
}
} catch {}
};
const safeGetRecordsByPage = async (table: any, req: any) => {
const cleaned: any = { ...(req || {}) };
if (!cleaned.viewId) delete cleaned.viewId;
if (!cleaned.pageToken) delete cleaned.pageToken;
if (!cleaned.pageSize || Number(cleaned.pageSize) <= 0) cleaned.pageSize = 200;
if (Number(cleaned.pageSize) > 200) cleaned.pageSize = 200;
try {
return await table.getRecordsByPage(cleaned);
} catch (e: any) {
if (e?.code === 12) {
await activateTableForPaging(table);
return await table.getRecordsByPage(cleaned);
}
throw e;
}
};
const fetchAllRecordsByPage = async (table: any, params?: any) => {
let token: any = undefined;
let all: any[] = [];
for (let i = 0; i < 10000; i++) {
const requestedPageSize = Math.min(200, (params && params.pageSize) ? params.pageSize : 200);
const req: any = { pageSize: requestedPageSize, ...(params || {}) };
if (token) req.pageToken = token;
const res: any = await safeGetRecordsByPage(table, req);
const recs: any[] = Array.isArray(res?.records) ? res.records : [];
all = all.concat(recs);
const nextToken = res?.pageToken || res?.nextPageToken;
const hm = !!res?.hasMore;
token = nextToken || undefined;
if (!hm && !nextToken) break;
}
return all;
};
const getRecordTotalByPage = async (table: any, params?: any) => {
const res: any = await table.getRecordIdListByPage({ pageSize: 1, ...(params || {}) });
return res?.total || 0;
};
const parseBatchRowNumber = (raw: any): number | null => {
const text = extractText(raw);
const n = typeof raw === 'number'
? raw
: (text && text.trim() !== '' ? Number(text) : NaN);
return Number.isFinite(n) ? n : null;
};
// 已移除:调整模式不再加载货期记录列表
// 入口选择处理
const chooseMode = (m: 'generate' | 'adjust') => {
// 切换功能时重置全局变量但保留新的mode
resetGlobalState({ resetMode: false });
setMode(m);
setIsExpectedDeliveryDateLocked(false);
setLockedExpectedDeliveryDateTs(null);
setModeSelectionVisible(false);
};
const openBatchModal = async () => {
try {
const batchTable = await bitable.base.getTable(BATCH_TABLE_ID);
const total = await getRecordTotalByPage(batchTable);
let totalByRowNumber = 0;
try {
const rows = await fetchAllRecordsByPage(batchTable, undefined);
for (const r of rows) {
const no = parseBatchRowNumber((r?.fields || {})[BATCH_ROW_NUMBER_FIELD_ID]);
if (no !== null) totalByRowNumber = Math.max(totalByRowNumber, no);
}
} catch {}
const totalRows = totalByRowNumber > 0 ? totalByRowNumber : total;
setBatchTotalRows(totalRows);
setBatchStartRow(1);
setBatchEndRow(totalRows > 0 ? totalRows : 1);
setBatchProcessingTotal(totalRows);
} catch {
setBatchTotalRows(0);
setBatchStartRow(1);
setBatchEndRow(1);
setBatchProcessingTotal(0);
}
setBatchModalVisible(true);
};
const restoreBaseBufferDaysFromSnapshot = (snapshot: any) => {
const snapshotDirection = snapshot?.timelineDirection;
if (snapshotDirection === 'backward') return;
const candidates: any[] = [
snapshot?.bufferManagement?.baseDays,
snapshot?.baseBufferDays,
snapshot?.baseBuferDays,
snapshot?.baseDays,
snapshot?.bufferManagement?.baseBuferDays,
];
for (const c of candidates) {
const n = typeof c === 'number'
? c
: (typeof c === 'string' ? parseFloat(c) : NaN);
if (Number.isFinite(n)) {
setBaseBufferDays(Math.max(0, Math.ceil(n)));
return;
}
}
};
const restoreTimelineDirectionFromSnapshot = (snapshot: any) => {
const candidates: any[] = [
snapshot?.timelineDirection,
snapshot?.timelineCalculationState?.timelineDirection,
snapshot?.timelineCalculationState?.calculationDirection,
snapshot?.calculationDirection,
];
for (const c of candidates) {
if (c === 'forward' || c === 'backward') {
setTimelineDirection(c);
return;
}
}
};
const normalizeExcludedDatesOverride = (raw: any): string[] => {
const parts: string[] = [];
if (Array.isArray(raw)) {
for (const el of raw) {
const s = typeof el === 'string' ? el : extractText(el);
if (s) parts.push(s);
}
} else if (typeof raw === 'string') {
parts.push(...raw.split(/[\,\n\r\s]+/));
} else if (raw && typeof raw === 'object') {
const s = extractText(raw);
if (s) parts.push(...s.split(/[\,\n\r\s]+/));
}
const out: string[] = [];
const seen = new Set<string>();
for (const p of parts) {
const t = (p || '').trim();
if (!t) continue;
const d = parseDate(t);
if (!d || isNaN(d.getTime())) continue;
const normalized = formatDate(d, 'DISPLAY_DATE_ONLY');
if (!normalized) continue;
if (!seen.has(normalized)) {
seen.add(normalized);
out.push(normalized);
}
}
out.sort();
return out;
};
const restoreExcludedDatesOverrideFromSnapshot = (snapshot: any, timelineResultsCandidate?: any[]) => {
const candidates: any[] = [
snapshot?.excludedDatesOverride,
snapshot?.timelineCalculationState?.excludedDatesOverride,
snapshot?.timelineCalculationState?.excludedDates,
];
for (const c of candidates) {
const normalized = normalizeExcludedDatesOverride(c);
if (normalized.length > 0) {
setExcludedDatesOverride(normalized);
setExcludedDatesOverrideText(normalized.join('\n'));
return;
}
}
const results = Array.isArray(timelineResultsCandidate)
? timelineResultsCandidate
: (Array.isArray(snapshot?.timelineResults) ? snapshot.timelineResults : []);
if (results.length > 0) {
const union: string[] = [];
for (const r of results) {
const arr = Array.isArray(r?.excludedDates) ? r.excludedDates : [];
union.push(...arr);
}
const normalized = normalizeExcludedDatesOverride(union);
if (normalized.length > 0) {
setExcludedDatesOverride(normalized);
setExcludedDatesOverrideText(normalized.join('\n'));
return;
}
}
setExcludedDatesOverride([]);
setExcludedDatesOverrideText('');
};
const handleExcludedDatesOverrideTextChange = (next: any) => {
const text = typeof next === 'string'
? next
: (next?.target?.value ?? '');
setExcludedDatesOverrideText(text);
setExcludedDatesOverride(normalizeExcludedDatesOverride(text));
};
const buildExcludedDatesNodeKey = (nodeName: any, processOrder: any, indexFallback?: number, instanceKey?: any) => {
const name = (typeof nodeName === 'string' ? nodeName : extractText(nodeName)).trim();
const order = (typeof processOrder === 'number' || typeof processOrder === 'string')
? String(processOrder)
: '';
const inst = (typeof instanceKey === 'string' ? instanceKey : extractText(instanceKey)).trim();
if (name && order) return inst ? `${order}::${name}::${inst}` : `${order}::${name}`;
if (name) return inst ? `${name}::${inst}` : name;
if (indexFallback !== undefined) return `#${indexFallback + 1}`;
return '#unknown';
};
const buildTimelineAdjustmentKey = (node: any, indexFallback?: number) => {
return buildExcludedDatesNodeKey(
node?.nodeName,
node?.processOrder,
indexFallback,
node?.processGroupInstanceId || node?.processGroupInstanceName
);
};
const normalizeTimelineAdjustmentsFromSnapshot = (raw: any, resultsFromSnapshot?: any[]): Record<string, number> => {
if (!raw || typeof raw !== 'object') return {};
const entries = Object.entries(raw)
.map(([k, v]) => [String(k).trim(), Number(v)] as const)
.filter(([k, v]) => k && Number.isFinite(v) && v !== 0);
if (entries.length === 0) return {};
const snapshotResults = Array.isArray(resultsFromSnapshot) ? resultsFromSnapshot : [];
const numericKeys: number[] = [];
for (const [k] of entries) {
const n = Number(k);
if (Number.isInteger(n)) numericKeys.push(n);
}
if (snapshotResults.length > 0 && numericKeys.length > 0) {
const byProcessOrder = new Map<number, { node: any; idx: number }>();
for (let i = 0; i < snapshotResults.length; i++) {
const r = snapshotResults[i];
const orderRaw = r?.processOrder;
const order = typeof orderRaw === 'number'
? orderRaw
: (typeof orderRaw === 'string' ? Number(orderRaw) : NaN);
if (!Number.isFinite(order)) continue;
if (!byProcessOrder.has(order)) byProcessOrder.set(order, { node: r, idx: i });
}
const eps = 1e-6;
const indexMatchHits = (() => {
let hits = 0;
for (const [k, v] of entries) {
const n = Number(k);
if (!Number.isInteger(n)) continue;
if (n < 0 || n >= snapshotResults.length) continue;
const r = snapshotResults[n];
const adj = Number(r?.adjustment);
if (Number.isFinite(adj) && Math.abs(adj - v) < eps) {
hits++;
continue;
}
const base = Number(r?.timelineValue);
const adjusted = Number(r?.adjustedTimelineValue);
if (Number.isFinite(base) && Number.isFinite(adjusted) && Math.abs((adjusted - base) - v) < eps) {
hits++;
continue;
}
}
return hits;
})();
const processOrderHits = numericKeys.filter(k => k !== 0 && byProcessOrder.has(k)).length;
const shouldTreatAsIndex = indexMatchHits >= Math.ceil(numericKeys.length / 2);
const shouldTreatAsProcessOrder = !shouldTreatAsIndex && processOrderHits >= Math.ceil(numericKeys.length / 2);
const out: Record<string, number> = {};
for (const [k, v] of entries) {
const n = Number(k);
if (!Number.isInteger(n)) {
out[k] = Math.round(v * 100) / 100;
continue;
}
if (shouldTreatAsIndex && n >= 0 && n < snapshotResults.length) {
out[buildTimelineAdjustmentKey(snapshotResults[n], n)] = Math.round(v * 100) / 100;
continue;
}
if (shouldTreatAsProcessOrder) {
const hit = byProcessOrder.get(n);
if (hit) {
out[buildTimelineAdjustmentKey(hit.node, hit.idx)] = Math.round(v * 100) / 100;
continue;
}
}
if (n >= 0 && n < snapshotResults.length) {
out[buildTimelineAdjustmentKey(snapshotResults[n], n)] = Math.round(v * 100) / 100;
continue;
}
out[k] = Math.round(v * 100) / 100;
}
return out;
}
const out: Record<string, number> = {};
for (const [k, v] of entries) {
out[k] = Math.round((v as number) * 100) / 100;
}
return out;
};
const deriveTimelineAdjustmentsFromResults = (resultsFromSnapshot: any[]): Record<string, number> => {
const results = Array.isArray(resultsFromSnapshot) ? resultsFromSnapshot : [];
const out: Record<string, number> = {};
for (let i = 0; i < results.length; i++) {
const r = results[i];
const v = Number(r?.adjustment);
if (!Number.isFinite(v) || v === 0) continue;
const key = buildTimelineAdjustmentKey(r, i);
out[key] = Math.round(v * 100) / 100;
}
return out;
};
const tryParseProcessOrderFromKey = (nodeKey: string): number | null => {
const key = String(nodeKey || '').trim();
const idx = key.indexOf('::');
if (idx <= 0) return null;
const left = key.slice(0, idx).trim();
const order = Number(left);
if (!Number.isFinite(order)) return null;
return order;
};
const extractNodeNameFromKey = (nodeKey: string): string => {
const key = String(nodeKey || '').trim();
const idx = key.indexOf('::');
if (idx < 0) return key;
return key.slice(idx + 2).trim();
};
const normalizeNodeNameForMatch = (name: any): string => {
const raw = (typeof name === 'string' ? name : extractText(name)).trim();
if (!raw) return '';
return raw
.replace(/[\s:\-_.()()【】\[\]<>《》]/g, '')
.toLowerCase();
};
const remapTimelineAdjustmentsToNewResults = (
prevResults: any[],
nextResults: any[]
): Record<string, number> => {
const out: Record<string, number> = {};
const prevArr = Array.isArray(prevResults) ? prevResults : [];
const prevAdjustmentByName = new Map<string, number>();
for (let i = 0; i < prevArr.length; i++) {
const r = prevArr[i];
const name = (typeof r?.nodeName === 'string' ? r.nodeName : extractText(r?.nodeName)).trim();
if (!name) continue;
const rowAdj = Number(r?.adjustment);
if (Number.isFinite(rowAdj) && rowAdj !== 0) {
const existed = prevAdjustmentByName.get(name);
if (existed === undefined || existed === 0) prevAdjustmentByName.set(name, Math.round(rowAdj * 100) / 100);
continue;
}
const base = Number(r?.timelineValue);
const adjusted = Number(r?.adjustedTimelineValue);
if (Number.isFinite(base) && Number.isFinite(adjusted)) {
const d = adjusted - base;
if (Number.isFinite(d) && d !== 0) {
const existed = prevAdjustmentByName.get(name);
if (existed === undefined || existed === 0) prevAdjustmentByName.set(name, Math.round(d * 100) / 100);
}
}
}
const nextArr = Array.isArray(nextResults) ? nextResults : [];
for (let i = 0; i < nextArr.length; i++) {
const node = nextArr[i];
const name = (typeof node?.nodeName === 'string' ? node.nodeName : extractText(node?.nodeName)).trim();
if (!name) continue;
const adj = prevAdjustmentByName.get(name);
if (adj === undefined || adj === 0) continue;
out[buildTimelineAdjustmentKey(node, i)] = Math.round(adj * 100) / 100;
}
return out;
};
const buildExcludedDatesByNodeFromTimeline = (results: any[]): Record<string, string[]> => {
const map: Record<string, string[]> = {};
for (let i = 0; i < results.length; i++) {
const r = results[i];
const key = buildExcludedDatesNodeKey(
r?.nodeName,
r?.processOrder,
i,
r?.processGroupInstanceId || r?.processGroupInstanceName
);
const arr = Array.isArray(r?.excludedDates) ? r.excludedDates : [];
map[key] = normalizeExcludedDatesOverride(arr);
}
return map;
};
const normalizeExcludedDatesByNodeMap = (raw: any): Record<string, string[]> => {
const out: Record<string, string[]> = {};
if (!raw) return out;
if (typeof raw !== 'object') return out;
for (const [k, v] of Object.entries(raw)) {
const key = (k || '').trim();
if (!key) continue;
const normalized = normalizeExcludedDatesOverride(v);
out[key] = normalized;
}
return out;
};
const restoreExcludedDatesByNodeOverrideFromSnapshot = (snapshot: any, timelineResultsCandidate?: any[]) => {
const candidates: any[] = [
snapshot?.excludedDatesByNodeOverride,
snapshot?.excludedDatesByNode,
snapshot?.timelineCalculationState?.excludedDatesByNodeOverride,
snapshot?.timelineCalculationState?.excludedDatesByNode,
];
for (const c of candidates) {
const normalized = normalizeExcludedDatesByNodeMap(c);
if (Object.keys(normalized).length > 0) {
excludedDatesByNodeOverrideRef.current = normalized;
setExcludedDatesByNodeOverride(normalized);
return;
}
}
const results = Array.isArray(timelineResultsCandidate)
? timelineResultsCandidate
: (Array.isArray(snapshot?.timelineResults) ? snapshot.timelineResults : []);
if (results.length > 0) {
const derived = buildExcludedDatesByNodeFromTimeline(results);
excludedDatesByNodeOverrideRef.current = derived;
setExcludedDatesByNodeOverride(derived);
return;
}
excludedDatesByNodeOverrideRef.current = {};
setExcludedDatesByNodeOverride({});
};
const groupDatesByMonth = (dates: string[]) => {
const groups = new Map<string, string[]>();
for (const d of dates) {
const ds = (d || '').trim();
if (!ds) continue;
const month = ds.slice(0, 7);
if (!groups.has(month)) groups.set(month, []);
groups.get(month)!.push(ds);
}
const out = Array.from(groups.entries())
.map(([month, ds]) => ({
month,
dates: Array.from(new Set(ds)).sort(),
}))
.sort((a, b) => a.month.localeCompare(b.month));
return out;
};
const openExcludedDatesAdjustModal = () => {
const baseResults = Array.isArray(timelineResults) ? timelineResults : [];
const draft: Record<string, string[]> = {};
for (let i = 0; i < baseResults.length; i++) {
const r = baseResults[i];
const key = buildExcludedDatesNodeKey(
r?.nodeName,
r?.processOrder,
i,
r?.processGroupInstanceId || r?.processGroupInstanceName
);
const existing = excludedDatesByNodeOverrideRef.current?.[key];
const fromResult = Array.isArray(r?.excludedDates) ? r.excludedDates : [];
draft[key] = normalizeExcludedDatesOverride(existing ?? fromResult);
}
setExcludedDatesByNodeDraft(draft);
setExcludedDatesAddDraft({});
setExcludedDatesAdjustVisible(true);
};
const closeExcludedDatesAdjustModal = () => {
setExcludedDatesAdjustVisible(false);
setExcludedDatesByNodeDraft({});
setExcludedDatesAddDraft({});
};
const removeExcludedDateFromDraft = (nodeKey: string, date: string) => {
setExcludedDatesByNodeDraft(prev => {
const current = Array.isArray(prev?.[nodeKey]) ? prev[nodeKey] : [];
const next = current.filter(d => d !== date);
return { ...(prev || {}), [nodeKey]: next };
});
};
const addExcludedDateToDraft = (nodeKey: string, date: Date | null) => {
if (!(date instanceof Date) || isNaN(date.getTime())) return;
const normalized = formatDate(date, 'DISPLAY_DATE_ONLY');
if (!normalized) return;
setExcludedDatesByNodeDraft(prev => {
const current = Array.isArray(prev?.[nodeKey]) ? prev[nodeKey] : [];
const next = Array.from(new Set([...current, normalized])).sort();
return { ...(prev || {}), [nodeKey]: next };
});
};
const clearExcludedDatesDraftForNode = (nodeKey: string) => {
setExcludedDatesByNodeDraft(prev => ({ ...(prev || {}), [nodeKey]: [] }));
};
// 根据货期记录ID读取节点详情并还原流程数据
const loadProcessDataFromDeliveryRecord = async (deliveryRecordId: string) => {
if (!deliveryRecordId) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: '请先选择一条货期记录' });
}
return;
}
// 新增:在读取新单据前重置关键状态,避免跨单据串值(缓冲期/实际完成日期/起始日期等)
// 保留当前模式不变
resetGlobalState({ resetMode: false });
setTimelineLoading(true);
try {
const deliveryTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID);
const deliveryRecord: any = await deliveryTable.getRecordById(deliveryRecordId);
const deliveryFields: any = deliveryRecord?.fields || {};
const nodeDetailsVal = deliveryFields?.[DELIVERY_NODE_DETAILS_FIELD_ID];
let expectedDeliveryTsFromField: number | null = null;
try {
const expectedDeliveryVal = deliveryFields?.[DELIVERY_EXPECTED_DATE_FIELD_ID];
let ts: number | null = null;
if (typeof expectedDeliveryVal === 'number') {
ts = expectedDeliveryVal;
} else if (Array.isArray(expectedDeliveryVal) && expectedDeliveryVal.length > 0 && typeof expectedDeliveryVal[0] === 'number') {
ts = expectedDeliveryVal[0];
} else {
const extracted = extractText(expectedDeliveryVal);
if (extracted && extracted.trim() !== '') {
const parsed = new Date(extracted);
if (!isNaN(parsed.getTime())) ts = parsed.getTime();
}
}
expectedDeliveryTsFromField = ts;
} catch {}
// 读取record_ids文本字段并保留原始文本用于原样写回
try {
const recordIdsTextVal = deliveryFields?.[DELIVERY_RECORD_IDS_FIELD_ID];
const raw = extractText(recordIdsTextVal);
if (raw && raw.trim() !== '') {
setRestoredRecordIdsText(raw.trim());
// 若需要解析为数组供内部使用,可保留解析逻辑(不影响写回原始文本)
try {
const json = JSON.parse(raw);
if (Array.isArray(json)) {
const parsedFromText = json.filter((id: any) => typeof id === 'string' && id.trim() !== '');
if (parsedFromText.length > 0) setRestoredRecordIds(parsedFromText);
}
} catch {
const parsedFromText = raw.split(/[\,\s]+/).map((s: string) => s.trim()).filter(Boolean);
if (parsedFromText.length > 0) setRestoredRecordIds(parsedFromText);
}
}
} catch (e) {
console.warn('解析record_ids文本字段失败忽略:', e);
}
// 优先使用货期记录表中的快照字段进行一键还原(新方案)
try {
const extractAllText = (val: any): string => {
if (val === null || val === undefined) return '';
if (typeof val === 'string') return val;
if (typeof val === 'number') return String(val);
if (Array.isArray(val)) {
return val
.map((el: any) => {
if (el === null || el === undefined) return '';
if (typeof el === 'string') return el;
if (typeof el === 'number') return String(el);
if (typeof el === 'object') return el.text || el.name || el.value?.toString?.() || '';
return '';
})
.join('');
}
if (typeof val === 'object') return (val as any).text || (val as any).name || (val as any).value?.toString?.() || '';
return '';
};
const snapPart1 = extractAllText(deliveryFields?.[DELIVERY_SNAPSHOT_JSON_FIELD_ID]);
const snapPart2 = extractAllText(deliveryFields?.[DELIVERY_SNAPSHOT_JSON_2_FIELD_ID]);
const deliverySnapStr = `${snapPart1 || ''}${snapPart2 || ''}`;
if (deliverySnapStr && deliverySnapStr.trim() !== '') {
setIsRestoringSnapshot(true);
const snapshot = JSON.parse(deliverySnapStr);
restoreTimelineDirectionFromSnapshot(snapshot);
restoreBaseBufferDaysFromSnapshot(snapshot);
restoreExcludedDatesOverrideFromSnapshot(snapshot, snapshot?.timelineResults);
restoreExcludedDatesByNodeOverrideFromSnapshot(snapshot, snapshot?.timelineResults);
// 恢复页面与全局状态
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);
}
{
const normalized = snapshot.timelineAdjustments
? normalizeTimelineAdjustmentsFromSnapshot(snapshot.timelineAdjustments, snapshot?.timelineResults)
: deriveTimelineAdjustmentsFromResults(snapshot?.timelineResults);
setTimelineAdjustments(normalized);
}
{
const snapLockedFlag = snapshot?.isExpectedDeliveryDateLocked;
const snapLockedTs = snapshot?.lockedExpectedDeliveryDateTs;
const shouldLock = snapLockedFlag === true || (snapLockedFlag === undefined && snapLockedTs !== undefined && snapLockedTs !== null);
if (shouldLock) {
const ts = (typeof snapLockedTs === 'number' && Number.isFinite(snapLockedTs))
? snapLockedTs
: expectedDeliveryTsFromField;
if (ts !== null && ts !== undefined) {
setLockedExpectedDeliveryDateTs(ts);
setIsExpectedDeliveryDateLocked(true);
} else {
setLockedExpectedDeliveryDateTs(null);
setIsExpectedDeliveryDateLocked(false);
}
} else {
setLockedExpectedDeliveryDateTs(null);
setIsExpectedDeliveryDateLocked(false);
}
}
restoreExcludedDatesOverrideFromSnapshot(snapshot, snapshot.timelineResults);
restoreExcludedDatesByNodeOverrideFromSnapshot(snapshot, snapshot.timelineResults);
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 = deliveryFields?.[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);
Modal.confirm({
title: '是否调整标签?',
content: '选择“是”将允许修改标签并重新生成计划版本按V2/V3/V4递增',
okText: '是,调整标签',
cancelText: '否,直接还原',
onOk: async () => {
setLabelAdjustmentFlow(true);
setTimelineVisible(false);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.info, message: '请在下方修改标签后点击重新生成计划' });
}
},
onCancel: async () => {
setLabelAdjustmentFlow(false);
setTimelineVisible(true);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 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);
}
// 注意不将节点详情的recordIds写入restoredRecordIds避免读取为空时写入非空
if (!recordIds || recordIds.length === 0) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: '该货期记录未包含节点详情或为空' });
}
setTimelineLoading(false);
return;
}
const processTable = await bitable.base.getTable(PROCESS_DATA_TABLE_ID);
try {
const processSnapshotField: any = await processTable.getField(PROCESS_SNAPSHOT_JSON_FIELD_ID);
let snapStr: string | null = null;
for (const id of recordIds) {
const snapVal = await processSnapshotField.getValue(id);
const candidate = extractText(snapVal);
if (candidate && candidate.trim() !== '') {
snapStr = candidate;
break;
}
}
if (snapStr && snapStr.trim() !== '') {
const snapshot = JSON.parse(snapStr);
if (Array.isArray(snapshot.timelineResults)) {
setIsRestoringSnapshot(true);
restoreTimelineDirectionFromSnapshot(snapshot);
restoreBaseBufferDaysFromSnapshot(snapshot);
restoreExcludedDatesOverrideFromSnapshot(snapshot, snapshot?.timelineResults);
restoreExcludedDatesByNodeOverrideFromSnapshot(snapshot, snapshot?.timelineResults);
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);
}
{
const normalized = snapshot.timelineAdjustments
? normalizeTimelineAdjustmentsFromSnapshot(snapshot.timelineAdjustments, snapshot?.timelineResults)
: deriveTimelineAdjustmentsFromResults(snapshot?.timelineResults);
setTimelineAdjustments(normalized);
}
{
const snapLockedFlag = snapshot?.isExpectedDeliveryDateLocked;
const snapLockedTs = snapshot?.lockedExpectedDeliveryDateTs;
const shouldLock = snapLockedFlag === true || (snapLockedFlag === undefined && snapLockedTs !== undefined && snapLockedTs !== null);
if (shouldLock && typeof snapLockedTs === 'number' && Number.isFinite(snapLockedTs)) {
setLockedExpectedDeliveryDateTs(snapLockedTs);
setIsExpectedDeliveryDateLocked(true);
} else {
setLockedExpectedDeliveryDateTs(null);
setIsExpectedDeliveryDateLocked(false);
}
}
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 = deliveryFields?.[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);
}
}
}
setTimelineResults(snapshot.timelineResults);
Modal.confirm({
title: '是否调整标签?',
content: '选择“是”将允许修改标签并重新生成计划版本按V2/V3/V4递增',
okText: '是,调整标签',
cancelText: '否,直接还原',
onOk: async () => {
setLabelAdjustmentFlow(true);
setTimelineVisible(false);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.info, message: '请在下方修改标签后点击重新生成计划' });
}
},
onCancel: async () => {
setLabelAdjustmentFlow(false);
setTimelineVisible(true);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.success, message: '已按快照一模一样还原流程数据' });
}
}
});
setTimelineLoading(false);
setIsRestoringSnapshot(false);
return;
}
}
} catch (e) {
console.warn('从节点快照快速还原失败,继续旧流程:', e);
}
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);
restoreTimelineDirectionFromSnapshot(snapshot);
restoreBaseBufferDaysFromSnapshot(snapshot);
// 恢复页面状态
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);
}
{
const normalized = snapshot.timelineAdjustments
? normalizeTimelineAdjustmentsFromSnapshot(snapshot.timelineAdjustments, snapshot?.timelineResults)
: deriveTimelineAdjustmentsFromResults(snapshot?.timelineResults);
setTimelineAdjustments(normalized);
}
restoreExcludedDatesOverrideFromSnapshot(snapshot, snapshot.timelineResults);
restoreExcludedDatesByNodeOverrideFromSnapshot(snapshot, snapshot.timelineResults);
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 = deliveryFields?.[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);
Modal.confirm({
title: '是否调整标签?',
content: '选择“是”将允许修改标签并重新生成计划版本按V2/V3/V4递增',
okText: '是,调整标签',
cancelText: '否,直接还原',
onOk: async () => {
setLabelAdjustmentFlow(true);
setTimelineVisible(false);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.info, message: '请在下方修改标签后点击重新生成计划' });
}
},
onCancel: async () => {
setLabelAdjustmentFlow(false);
setTimelineVisible(true);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 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) {
setTimelineResults(snapshot.timelineResults);
Modal.confirm({
title: '是否调整标签?',
content: '选择“是”将允许修改标签并重新生成计划版本按V2/V3/V4递增',
okText: '是,调整标签',
cancelText: '否,直接还原',
onOk: async () => {
setLabelAdjustmentFlow(true);
setTimelineVisible(false);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.info, message: '请在下方修改标签后点击重新生成计划' });
}
},
onCancel: async () => {
setLabelAdjustmentFlow(false);
setTimelineVisible(true);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.success, message: '已按快照一模一样还原流程数据' });
}
}
});
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: 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,
timelineDirection: nodeSnapshot.timelineDirection,
lockedExpectedDeliveryDateTs: nodeSnapshot.lockedExpectedDeliveryDateTs,
isExpectedDeliveryDateLocked: nodeSnapshot.isExpectedDeliveryDateLocked,
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
restoreTimelineDirectionFromSnapshot(globalSnapshotData);
restoreBaseBufferDaysFromSnapshot(globalSnapshotData);
restoreExcludedDatesOverrideFromSnapshot(globalSnapshotData, nodeSnapshots);
restoreExcludedDatesByNodeOverrideFromSnapshot(globalSnapshotData, nodeSnapshots);
setTimelineAdjustments(deriveTimelineAdjustmentsFromResults(nodeSnapshots));
setTimelineResults(nodeSnapshots);
setTimelineVisible(true);
{
const snapLockedFlag = (globalSnapshotData as any)?.isExpectedDeliveryDateLocked;
const snapLockedTs = (globalSnapshotData as any)?.lockedExpectedDeliveryDateTs;
const shouldLock = snapLockedFlag === true || (snapLockedFlag === undefined && snapLockedTs !== undefined && snapLockedTs !== null);
if (shouldLock && typeof snapLockedTs === 'number' && Number.isFinite(snapLockedTs)) {
setLockedExpectedDeliveryDateTs(snapLockedTs);
setIsExpectedDeliveryDateLocked(true);
} else {
setLockedExpectedDeliveryDateTs(null);
setIsExpectedDeliveryDateLocked(false);
}
}
// 恢复智能缓冲期状态(如果存在)
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: 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 = deliveryFields?.[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: ToastType.success, message: '已从货期记录还原流程数据' });
}
} catch (error) {
console.error('从货期记录还原流程数据失败:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 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 PROCESS_GROUP_FIELD_ID_DATA = 'fldq6PTX5F'; // 流程组字段(流程数据表,单选)
// 货期记录表相关常量
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_SNAPSHOT_JSON_2_FIELD_ID = 'fldFr999QY';
// 起始时间字段(货期记录表新增)
const DELIVERY_START_TIME_FIELD_ID = 'fld727qCAv';
// 文本2字段货期记录表新增
const DELIVERY_TEXT2_FIELD_ID = 'fldG6LZnmU';
// 记录ID文本字段货期记录表新增
const DELIVERY_RECORD_IDS_FIELD_ID = 'fldq3u7h7H';
const DELIVERY_FACTORY_DEPARTURE_DATE_FIELD_ID = 'fldZFdZDKj';
// OMS看板表相关常量新增
const OMS_BOARD_TABLE_ID = 'tbl7j8bCpUbFmGuk'; // OMS看板表ID
const OMS_PLAN_TEXT_FIELD_ID = 'fldH0jPZE0'; // OMS看板货期计划文本结构
const OMS_PLAN_VERSION_FIELD_ID = 'fldwlIUf4z'; // OMS看板计划版本公式数字
const OMS_DELIVERY_RECORD_ID_FIELD_ID = 'fldjEIP9yC'; // OMS看板货期记录表record_id
// 已移除中国法定节假日相关常量和配置
// 这个变量声明也不需要了
// 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: any) {
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 hasNonEmptyRuleValue = (value: any): boolean => {
if (value === null || value === undefined) return false;
if (typeof value === 'string') return value.trim().length > 0;
if (typeof value === 'number') return true;
if (Array.isArray(value)) return value.some(item => hasNonEmptyRuleValue(item));
if (typeof value === 'object') {
const v: any = value;
if (typeof v.text === 'string') return v.text.trim().length > 0;
if (typeof v.name === 'string') return v.name.trim().length > 0;
if (v.value !== undefined) return hasNonEmptyRuleValue(v.value);
return Object.keys(v).length > 0;
}
return Boolean(value);
};
const setTimeOfDay = (date: Date, hour: number, minute: number): Date => {
const result = new Date(date);
result.setHours(hour, minute, 0, 0);
return result;
};
const shouldForceEndTimeTo18 = (nodeLike: any): boolean => {
return hasNonEmptyRuleValue(nodeLike?.startDateRule) || hasNonEmptyRuleValue(nodeLike?.dateAdjustmentRule);
};
// 内部工作时间计算函数
const addInternalBusinessTime = (startDate: Date, businessDays: number, weekendDays: number[] = [], excludedDates: string[] = []): Date => {
const result = new Date(startDate);
if (!businessDays || businessDays === 0) return result;
const isNegative = businessDays < 0;
const absDays = Math.abs(businessDays);
// 处理整数天(仅计入工作日)
const wholeDays = Math.floor(absDays);
let processedDays = 0;
while (processedDays < wholeDays) {
result.setDate(result.getDate() + (isNegative ? -1 : 1));
if (isBusinessDay(result, weekendDays, excludedDates)) {
processedDays++;
}
}
// 处理小数部分按9小时工作制
const fractionalDays = absDays - wholeDays;
if (fractionalDays > 0) {
const workingHours = fractionalDays * 9; // 内部按9小时工作制
let currentHour = result.getHours();
let currentMinute = result.getMinutes();
if (!isNegative) {
// 正向:确保在工作时间内开始
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 + workingHours * 60;
const finalHour = Math.floor(totalMinutes / 60);
const finalMinute = totalMinutes % 60;
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);
}
} else {
// 负向:从当前时间向前回退工作小时,规范到工作时间窗口
if (currentHour > 18) {
// 当天超过18:00先归位到18:00
result.setHours(18, 0, 0, 0);
currentHour = 18;
currentMinute = 0;
} else if (currentHour < 9) {
// 早于9:00跳到前一个工作日的18:00
result.setDate(result.getDate() - 1);
while (!isBusinessDay(result, weekendDays, excludedDates)) {
result.setDate(result.getDate() - 1);
}
result.setHours(18, 0, 0, 0);
currentHour = 18;
currentMinute = 0;
}
const totalMinutes = currentHour * 60 + currentMinute - workingHours * 60;
if (totalMinutes >= 9 * 60) {
const finalHour = Math.floor(totalMinutes / 60);
const finalMinute = totalMinutes % 60;
result.setHours(finalHour, finalMinute, 0, 0);
} else {
// 需要跨到前一个工作日,计算欠缺分钟数
let deficit = 9 * 60 - totalMinutes; // 需要从前一工作日的18:00再退回的分钟数
// 跳到前一个工作日
result.setDate(result.getDate() - 1);
while (!isBusinessDay(result, weekendDays, excludedDates)) {
result.setDate(result.getDate() - 1);
}
// 从18:00开始退 deficit 分钟
result.setHours(18, 0, 0, 0);
result.setMinutes(result.getMinutes() - deficit);
}
}
}
return result;
};
// 添加工作日 - 使用表格配置的休息日与节点自定义跳过日期
const addBusinessDaysWithHolidays = (startDate: Date, businessDays: number, weekendDays: number[] = [], excludedDates: string[] = []): Date => {
const result = new Date(startDate);
if (!businessDays || businessDays === 0) return result;
const isNegative = businessDays < 0;
const absDays = Math.abs(businessDays);
let processedDays = 0;
// 先处理整数工作日
const wholeDays = Math.floor(absDays);
while (processedDays < wholeDays) {
result.setDate(result.getDate() + (isNegative ? -1 : 1));
if (isBusinessDay(result, weekendDays, excludedDates)) {
processedDays++;
}
}
// 再处理小数部分按24小时制
const fractionalDays = absDays - wholeDays;
if (fractionalDays > 0) {
const hours = fractionalDays * 24;
result.setHours(result.getHours() + (isNegative ? -hours : hours));
}
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-标签11的字段
const labelFields: {[key: string]: string} = {}; // 存储字段名到字段ID的映射
for (const fieldMeta of fieldMetaList) {
if (!fieldMeta || typeof (fieldMeta as any).name !== 'string' || typeof (fieldMeta as any).id !== 'string') {
continue;
}
const match = (fieldMeta as any).name.match(/^标签([1-9]|10|11)$/);
if (match) {
const labelKey = `标签${match[1]}`;
labelFields[labelKey] = (fieldMeta as any).id;
}
}
console.log('找到的标签字段:', labelFields);
// 3. 处理标签数据 - 从字段选项获取而不是从记录数据获取
const options: {[key: string]: any[]} = {};
// 初始化标签的选项数组
for (let i = 1; i <= 12; i++) {
options[`标签${i}`] = [];
}
const getSelectFieldOptions = async (table: any, fieldId: string): Promise<any[]> => {
const field = await table.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();
return (fieldOptions || [])
.filter((option: any) => option && typeof option.name === 'string')
.map((option: any) => ({ label: option.name, value: option.name }));
}
const records = await fetchAllRecordsByPage(table, undefined);
const values = new Set<string>();
for (const r of records) {
const raw = (r?.fields || {})[fieldId];
const tokens = Array.isArray(raw) ? raw : [raw];
for (const t of tokens) {
const s = extractText(t)?.trim();
if (s) values.add(s);
}
}
return Array.from(values)
.sort((a, b) => a.localeCompare(b, 'zh-Hans-CN'))
.map(v => ({ label: v, value: v }));
};
// 4. 遍历标签字段,获取每个字段的选项
for (const [labelKey, fieldId] of Object.entries(labelFields)) {
try {
options[labelKey] = await getSelectFieldOptions(labelTable, fieldId);
} catch (error) {
console.warn(`获取${labelKey}字段选项失败:`, error);
// 如果获取选项失败,保持空数组
options[labelKey] = [];
}
}
try {
const timelineTable = await bitable.base.getTable(TIMELINE_TABLE_ID);
const label12Options = await getSelectFieldOptions(timelineTable, TIMELINE_LABEL12_FIELD_ID);
const unique = new Map<string, any>();
for (const opt of label12Options) {
if (opt && typeof opt.value === 'string' && opt.value.trim()) {
unique.set(opt.value.trim(), { label: opt.value.trim(), value: opt.value.trim() });
}
}
options['标签12'] = Array.from(unique.values()).sort((a, b) => a.value.localeCompare(b.value, 'zh-Hans-CN'));
} catch (error) {
console.warn('获取标签12字段选项失败:', error);
options['标签12'] = [];
}
console.log('处理后的标签选项:', options);
setLabelOptions(options);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: ToastType.success,
message: `标签选项加载成功,共找到 ${Object.keys(labelFields).length} 个标签字段`
});
}
} catch (error) {
console.error('获取标签选项失败:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: ToastType.error,
message: '获取标签选项失败,请检查表格配置'
});
}
} finally {
setLabelLoading(false);
}
};
// 组件加载时获取标签数据和初始化起始时间
useEffect(() => {
const initializeData = async () => {
await fetchLabelOptions();
await initializeStartTime();
};
initializeData();
}, []);
const getDeliveryRecordIdFromOmsRecord = async (omsRecordId: string): Promise<string | null> => {
if (!omsRecordId) return null;
const omsTable = await bitable.base.getTable(OMS_BOARD_TABLE_ID);
const omsRecord = await omsTable.getRecordById(omsRecordId);
const raw = omsRecord?.fields?.[OMS_DELIVERY_RECORD_ID_FIELD_ID];
const text = extractText(raw)?.trim();
return text ? text : null;
};
// 初始化起始时间:从货期记录表获取,新记录则使用当前时间
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 startTimeField: any = await deliveryTable.getField(DELIVERY_START_TIME_FIELD_ID);
const startTimeValue = await startTimeField.getValue(recordId);
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;
}
}
} else if (recordId && tableId === OMS_BOARD_TABLE_ID) {
// 从OMS看板读取货期记录ID后尝试获取其起始时间
try {
const deliveryRecordId = await getDeliveryRecordIdFromOmsRecord(recordId);
if (deliveryRecordId) {
const deliveryTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID);
const deliveryRecord: any = await deliveryTable.getRecordById(deliveryRecordId);
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;
}
}
}
/*
// 原始逻辑从OMS看板用“货期计划 + 计划版本”匹配货期记录(已停用)
const omsTable = await bitable.base.getTable(OMS_BOARD_TABLE_ID);
const planTextField: any = await omsTable.getField(OMS_PLAN_TEXT_FIELD_ID);
const planVersionField: any = await omsTable.getField(OMS_PLAN_VERSION_FIELD_ID);
const planTextRaw = await planTextField.getValue(recordId);
const planVersionRaw = await planVersionField.getValue(recordId);
const planText = extractText(planTextRaw)?.trim();
let planVersion: number | null = null;
if (typeof planVersionRaw === 'number') {
planVersion = planVersionRaw;
} else if (typeof planVersionRaw === 'string') {
const m = planVersionRaw.match(/\d+(?:\.\d+)?/);
if (m) planVersion = parseFloat(m[0]);
} else if (planVersionRaw && typeof planVersionRaw === 'object') {
const v = (planVersionRaw as any).value ?? (planVersionRaw as any).text;
if (typeof v === 'number') planVersion = v;
else if (typeof v === 'string') {
const m = v.match(/\d+(?:\.\d+)?/);
if (m) planVersion = parseFloat(m[0]);
}
}
if (planText && planVersion !== null) {
const deliveryRecordId = await findDeliveryRecordIdByPlan(planText, planVersion);
if (deliveryRecordId) {
const deliveryTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID);
const startTimeField: any = await deliveryTable.getField(DELIVERY_START_TIME_FIELD_ID);
const startTimeValue = await startTimeField.getValue(deliveryRecordId);
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;
}
}
}
}
*/
} catch (e) {
console.warn('从OMS看板匹配起始时间失败使用当前时间:', e);
}
}
// 如果没有找到有效的起始时间,使用当前时间
setStartTime(new Date());
} catch (error) {
console.error('初始化起始时间失败:', error);
// 出错时使用当前时间作为默认值
setStartTime(new Date());
}
};
// 根据OMS看板的“货期计划”和“计划版本”匹配货期记录ID
const findDeliveryRecordIdByPlan = async (planText: string, planVersion: number): Promise<string | null> => {
try {
const deliveryTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID);
const versionField: any = await deliveryTable.getField(DELIVERY_VERSION_FIELD_ID);
const planFilter: any = {
conjunction: 'and',
conditions: [{ fieldId: DELIVERY_RECORD_IDS_FIELD_ID, operator: 'is', value: planText }]
};
const sort = DELIVERY_CREATE_TIME_FIELD_ID
? [{ fieldId: DELIVERY_CREATE_TIME_FIELD_ID, desc: true }]
: undefined;
const eps = 1e-9;
const normalizePlanText = (s: string) => s.replace(/\s+/g, '').trim();
const parseVersionFromRaw = (raw: any): number | null => {
if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
const s = typeof raw === 'string' ? raw : extractText(raw);
const m = (s || '').match(/\d+(?:\.\d+)?/);
return m ? parseFloat(m[0]) : null;
};
{
try {
let token: any = undefined;
for (let i = 0; i < 10000; i++) {
const req: any = { pageSize: 200, filter: planFilter, sort };
if (token) req.pageToken = token;
const res: any = await safeGetRecordsByPage(deliveryTable, req);
const recs: any[] = Array.isArray(res?.records) ? res.records : [];
for (const r of recs) {
const recordId = r?.recordId || r?.id || null;
if (!recordId) continue;
const recordPlanRaw = (r?.fields || {})[DELIVERY_RECORD_IDS_FIELD_ID];
const recordPlanText = typeof recordPlanRaw === 'string' ? recordPlanRaw : extractText(recordPlanRaw);
if (!recordPlanText) continue;
if (normalizePlanText(recordPlanText) !== normalizePlanText(planText)) continue;
const recordVersionRaw = (r?.fields || {})[DELIVERY_VERSION_FIELD_ID];
const recordVersion = parseVersionFromRaw(recordVersionRaw);
if (recordVersion !== null && Math.abs(recordVersion - planVersion) < eps) {
return recordId;
}
}
const nextToken = res?.pageToken;
const hasMore = !!res?.hasMore;
token = nextToken;
if (!hasMore && !nextToken) break;
}
} catch {}
}
{
try {
let token: any = undefined;
for (let i = 0; i < 10000; i++) {
const res: any = await versionField.getFieldValueListByPage({
pageSize: 200,
pageToken: token,
filter: planFilter,
sort,
stringValue: true
});
const fieldValues: any[] = Array.isArray(res?.fieldValues) ? res.fieldValues : [];
for (const fv of fieldValues) {
const recordId = fv?.recordId;
const v = parseVersionFromRaw(fv?.value);
if (v !== null && Math.abs(v - planVersion) < eps) {
return recordId || null;
}
}
const nextToken = res?.pageToken;
const hasMore = !!res?.hasMore;
token = nextToken;
if (!hasMore && !nextToken) break;
}
} catch {}
}
{
try {
let token: any = undefined;
for (let i = 0; i < 10000; i++) {
const req: any = { pageSize: 200, sort };
if (token) req.pageToken = token;
const res: any = await safeGetRecordsByPage(deliveryTable, req);
const recs: any[] = Array.isArray(res?.records) ? res.records : [];
for (const r of recs) {
const recordId = r?.recordId || r?.id || null;
if (!recordId) continue;
const recordPlanRaw = (r?.fields || {})[DELIVERY_RECORD_IDS_FIELD_ID];
const recordPlanText = typeof recordPlanRaw === 'string' ? recordPlanRaw : extractText(recordPlanRaw);
if (!recordPlanText) continue;
if (normalizePlanText(recordPlanText) !== normalizePlanText(planText)) continue;
const recordVersionRaw = (r?.fields || {})[DELIVERY_VERSION_FIELD_ID];
const recordVersion = parseVersionFromRaw(recordVersionRaw);
if (recordVersion !== null && Math.abs(recordVersion - planVersion) < eps) {
return recordId;
}
}
const nextToken = res?.pageToken;
const hasMore = !!res?.hasMore;
token = nextToken;
if (!hasMore && !nextToken) break;
}
} catch {}
}
return null;
} catch (error) {
console.error('匹配货期记录失败:', error);
return null;
}
};
// 已移除:批量表/视图加载与切换逻辑
// 处理标签选择变化
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,
excludedDates?: string[]
},
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;
const currentExcludedDates = Array.isArray(overrideData?.excludedDates) ? overrideData!.excludedDates : [];
const globalExcludedDates = normalizeExcludedDatesOverride(currentExcludedDates);
const excludedByNode = excludedDatesByNodeOverrideRef.current || {};
const isBackward = timelineDirection === 'backward';
console.log('=== handleCalculateTimeline - 使用的数据 ===');
console.log('currentSelectedRecords:', currentSelectedRecords);
console.log('currentSelectedLabels:', currentSelectedLabels);
console.log('currentExpectedDate:', currentExpectedDate);
console.log('currentStartTime:', currentStartTime);
{
const requiredLabelKeys = Array.from({ length: 11 }, (_, i) => `标签${i + 1}`);
const label7Val = (currentSelectedLabels as any)['标签7'];
const label7Values = Array.isArray(label7Val)
? label7Val.map((s: any) => String(s ?? '').trim()).filter(Boolean)
: (typeof label7Val === 'string' ? [label7Val.trim()].filter(Boolean) : []);
const shouldRequireLabel12 = label7Values.length > 0 && !(label7Values.length === 1 && label7Values[0] === '无(二次工艺)');
if (shouldRequireLabel12) requiredLabelKeys.push('标签12');
const missing = requiredLabelKeys.filter(key => {
const val = (currentSelectedLabels as any)[key];
if (Array.isArray(val)) return val.length === 0;
return !(typeof val === 'string' && val.trim().length > 0);
});
if (missing.length > 0) {
if (showUI && bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: `请填写以下必填标签:${missing.join('、')}` });
}
return [];
}
}
if (isBackward && !currentExpectedDate) {
if (showUI && bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: '倒推模式需要先选择客户期望日期' });
}
return [];
}
if (isBackward) {
if (!currentStartTime || isNaN(currentStartTime.getTime())) {
if (showUI && bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: '倒推模式需要先选择起始日期' });
}
return [];
}
if (!currentExpectedDate || isNaN(currentExpectedDate.getTime())) {
if (showUI && bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: '倒推模式需要先选择客户期望日期' });
}
return [];
}
}
// 跳过验证(用于批量模式)
if (!skipValidation) {
// 检查是否选择了多条记录
if (currentSelectedRecords.length > 1) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: ToastType.warning,
message: '计算时效功能仅支持单条记录,请重新选择单条记录后再试'
});
}
return;
}
// 检查是否选择了记录
if (currentSelectedRecords.length === 0) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: ToastType.warning,
message: '请先选择一条记录'
});
}
return;
}
// 可选:检查是否选择了客户期望日期
if (!currentExpectedDate) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 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 splitLabelTokens = (text: string): string[] => {
return text
.split(/[,;;、\n]+/)
.map(s => s.trim())
.filter(Boolean);
};
const extractLabelTokens = (value: any): string[] => {
if (!value) return [];
if (typeof value === 'string') return splitLabelTokens(value);
if (typeof value === 'number') return [String(value)];
if (value && typeof value === 'object') {
if (typeof value.text === 'string') return splitLabelTokens(value.text);
if (typeof value.name === 'string') return splitLabelTokens(value.name);
if (typeof value.value === 'string') return splitLabelTokens(value.value);
if (typeof value.value === 'number') return [String(value.value)];
}
if (Array.isArray(value)) {
const tokens: string[] = [];
for (const item of value) {
tokens.push(...extractLabelTokens(item));
}
return tokens.filter(Boolean);
}
return [];
};
const businessLabelValues = new Set<string>();
for (const selectedValue of Object.values(currentSelectedLabels)) {
for (const token of extractLabelTokens(selectedValue)) {
businessLabelValues.add(token);
}
}
// 已移除冗余日志:业务选择标签值
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) {
if (!fieldMeta || typeof (fieldMeta as any).name !== 'string' || typeof (fieldMeta as any).id !== 'string') {
continue;
}
const match = (fieldMeta as any).name.match(/^标签(\d+)$/);
if (match) {
const labelKey = `标签${match[1]}`;
timelineLabelFields[labelKey] = (fieldMeta as any).id;
}
if ((fieldMeta as any).name === '关系' || (fieldMeta as any).id === 'fldaIDAhab') {
relationshipFieldId = (fieldMeta as any).id;
}
if ((fieldMeta as any).name === '时效计算方式' || (fieldMeta as any).id === 'fldxfLZNUu') {
calculationMethodFieldId = (fieldMeta as any).id;
}
}
console.log('时效表标签字段映射:', timelineLabelFields);
console.log('关系字段ID:', relationshipFieldId);
// 1. 先获取匹配的流程节点(复用预览功能的逻辑)
const processTable = await bitable.base.getTable(PROCESS_CONFIG_TABLE_ID);
const processRecords = await fetchAllRecordsByPage(processTable, undefined);
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]; // 获取流程顺序
const processGroup = fields[PROCESS_GROUP_FIELD_ID];
let processGroupText = '';
if (typeof processGroup === 'string') {
processGroupText = processGroup;
} else {
processGroupText = extractText(processGroup);
}
processGroupText = (processGroupText || '').trim();
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 => {
return String(processLabelText ?? '').trim() === String(value ?? '').trim();
});
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格式日期调整规则
processGroup: processGroupText
});
}
}
const allGroups = Array.from(
new Set(
matchedProcessNodes
.map(n => (n.processGroup || '').trim())
.filter(g => g && g.length > 0)
)
);
if (showUI && allGroups.length > 0) {
if (skipNextGroupConfigPopupRef.current) {
skipNextGroupConfigPopupRef.current = false;
} else {
const groupSet = new Set(allGroups);
const existing = Array.isArray(groupOrderConfig)
? groupOrderConfig.filter(inst => groupSet.has((inst?.groupName || '').trim()))
: [];
const initial = existing.length > 0 ? existing : deriveGroupOrderDraftByProcessOrder(matchedProcessNodes);
setGroupOrderDraft(initial);
setGroupConfigVisible(true);
setTimelineLoading(false);
pendingGroupConfigCalcRef.current = {
overrideData: {
selectedRecords: currentSelectedRecords,
recordDetails: currentRecordDetails,
selectedLabels: currentSelectedLabels,
expectedDate: currentExpectedDate,
startTime: currentStartTime,
excludedDates: currentExcludedDates,
},
showUI,
};
return [];
}
}
let orderedProcessNodes: any[] = matchedProcessNodes;
if (groupOrderConfig.length > 0) {
const configuredBaseNames = new Set<string>();
for (const inst of groupOrderConfig) {
const base = (inst?.groupName || '').trim();
if (base) configuredBaseNames.add(base);
}
const baseNameToNodes = new Map<string, any[]>();
for (const n of matchedProcessNodes) {
const base = (n?.processGroup || '').trim();
if (!base) continue;
if (!baseNameToNodes.has(base)) baseNameToNodes.set(base, []);
baseNameToNodes.get(base)!.push(n);
}
for (const [base, list] of baseNameToNodes.entries()) {
list.sort((a, b) => (Number(a?.processOrder) || 0) - (Number(b?.processOrder) || 0));
baseNameToNodes.set(base, list);
}
const expanded: any[] = [];
for (const inst of groupOrderConfig) {
const base = (inst?.groupName || '').trim();
if (!base) continue;
const nodesInGroup = baseNameToNodes.get(base) || [];
for (const n of nodesInGroup) {
expanded.push({
...n,
processGroupBase: base,
processGroupInstanceId: inst.id,
processGroupInstanceName: inst.displayName,
});
}
}
const leftovers = matchedProcessNodes.filter(n => {
const base = (n?.processGroup || '').trim();
return base ? !configuredBaseNames.has(base) : true;
});
leftovers.sort((a, b) => (Number(a?.processOrder) || 0) - (Number(b?.processOrder) || 0));
orderedProcessNodes = expanded.concat(leftovers);
} else {
orderedProcessNodes = [...matchedProcessNodes].sort((a, b) => (Number(a?.processOrder) || 0) - (Number(b?.processOrder) || 0));
}
console.log('按顺序排列的流程节点:', orderedProcessNodes);
// 2. 优化:预先获取所有时效数据并建立索引
const timelineRecords = await fetchAllRecordsByPage(timelineTable, undefined);
// 优化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: string) => 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: string) => 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 cumulativeTime = isBackward
? new Date(currentExpectedDate as Date)
: (currentStartTime ? new Date(currentStartTime) : new Date());
const nodesToProcess = isBackward ? [...orderedProcessNodes].reverse() : orderedProcessNodes;
for (let i = 0; i < nodesToProcess.length; i++) {
const processNode = nodesToProcess[i];
const nodeKey = buildExcludedDatesNodeKey(
processNode?.nodeName,
processNode?.processOrder,
i,
processNode?.processGroupInstanceId || processNode?.processGroupInstanceName
);
const overrideList = Object.prototype.hasOwnProperty.call(excludedByNode, nodeKey)
? excludedByNode[nodeKey]
: undefined;
const baseList = Array.isArray(processNode?.excludedDates) ? processNode.excludedDates : [];
const selectedList = Array.isArray(overrideList) ? overrideList : baseList;
const nodeExcludedDates = Array.from(new Set([
...normalizeExcludedDatesOverride(selectedList),
...globalExcludedDates,
])).filter(Boolean);
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];
const timelineLabelTexts = extractLabelTokens(timelineLabelValue);
// 如果时效表中该标签有值,则检查该标签的所有值是否都包含在业务标签中
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, nodeExcludedDates);
let endDate: Date;
if (calculationMethod === '内部') {
// 使用内部工作时间计算
endDate = addInternalBusinessTime(adjustedStartDate, timelineValue, processNode.weekendDays, nodeExcludedDates);
} else {
// 使用原有的24小时制计算
endDate = addBusinessDaysWithHolidays(adjustedStartDate, timelineValue, processNode.weekendDays, nodeExcludedDates);
}
return {
startDate: formatDate(adjustedStartDate, 'STORAGE_FORMAT'),
endDate: formatDate(endDate, 'STORAGE_FORMAT')
};
};
// 获取当前节点的计算方式
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;
}
}
}
let nodeStartTime: Date;
let nodeEndTime: Date;
let ruleDescription = '';
let timelineResult: { startDate: string; endDate: string };
const forceEndTimeTo18 = shouldForceEndTimeTo18(processNode);
if (!isBackward) {
nodeStartTime = new Date(cumulativeTime);
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));
}
}
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: any) => item.type === 'text')
.map((item: any) => 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));
}
}
timelineResult = timelineValue
? calculateTimeline(nodeStartTime, timelineValue, nodeCalculationMethod)
: {
startDate: formatDate(nodeStartTime, 'STORAGE_FORMAT'),
endDate: '未找到时效数据'
};
if (timelineValue) {
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, processNode.weekendDays, nodeExcludedDates);
if (nodeCalculationMethod === '内部') {
nodeEndTime = addInternalBusinessTime(adjustedStartTime, timelineValue, processNode.weekendDays, nodeExcludedDates);
} else {
nodeEndTime = addBusinessDaysWithHolidays(adjustedStartTime, timelineValue, processNode.weekendDays, nodeExcludedDates);
}
} else {
nodeEndTime = new Date(nodeStartTime);
}
if (forceEndTimeTo18 && nodeEndTime && !isNaN(nodeEndTime.getTime())) {
nodeEndTime = setTimeOfDay(nodeEndTime, 18, 0);
timelineResult = {
...timelineResult,
endDate: timelineResult.endDate.includes('未找到') ? timelineResult.endDate : formatDate(nodeEndTime, 'STORAGE_FORMAT')
};
}
} else {
nodeEndTime = new Date(cumulativeTime);
if (forceEndTimeTo18 && nodeEndTime && !isNaN(nodeEndTime.getTime())) {
nodeEndTime = setTimeOfDay(nodeEndTime, 18, 0);
}
if (timelineValue) {
if (nodeCalculationMethod === '内部') {
nodeStartTime = addInternalBusinessTime(nodeEndTime, -timelineValue, processNode.weekendDays, nodeExcludedDates);
} else {
nodeStartTime = addBusinessDaysWithHolidays(nodeEndTime, -timelineValue, processNode.weekendDays, nodeExcludedDates);
}
} else {
nodeStartTime = new Date(nodeEndTime);
}
timelineResult = timelineValue
? {
startDate: formatDate(nodeStartTime, 'STORAGE_FORMAT'),
endDate: formatDate(nodeEndTime, 'STORAGE_FORMAT')
}
: {
startDate: formatDate(nodeStartTime, 'STORAGE_FORMAT'),
endDate: '未找到时效数据'
};
}
// 计算跳过的天数
const skippedWeekends = calculateSkippedWeekends(nodeStartTime, nodeEndTime, processNode.weekendDays);
const actualDays = calculateActualDays(timelineResult.startDate, timelineResult.endDate);
// 计算时间范围内实际跳过的自定义日期
const excludedDatesInRange = calculateExcludedDatesInRange(nodeStartTime, nodeEndTime, nodeExcludedDates);
(isBackward ? results.unshift.bind(results) : results.push.bind(results))({
processOrder: processNode.processOrder,
nodeName: processNode.nodeName,
processGroup: processNode.processGroup,
processGroupInstanceId: processNode.processGroupInstanceId,
processGroupInstanceName: processNode.processGroupInstanceName,
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: nodeExcludedDates, // 新增:保存不参与计算日期用于显示与快照
// 新增:保存时间范围内实际跳过的日期
actualExcludedDates: excludedDatesInRange.dates,
actualExcludedDatesCount: excludedDatesInRange.count,
calculationMethod: nodeCalculationMethod, // 新增:保存计算方式
ruleDescription: ruleDescription, // 新增:保存规则描述
skippedWeekends: skippedWeekends,
actualDays: actualDays,
// 新增:保存调整规则用于重新计算
startDateRule: processNode.startDateRule,
dateAdjustmentRule: processNode.dateAdjustmentRule,
adjustmentDescription: ruleDescription // 新增:保存调整规则描述
});
// 更新累积时间:当前节点的完成时间成为下一个节点的开始时间
if (timelineValue) {
cumulativeTime = isBackward ? new Date(nodeStartTime) : new Date(nodeEndTime);
}
console.log(`节点 ${processNode.nodeName} (顺序: ${processNode.processOrder}):`, {
开始时间: formatDate(nodeStartTime),
完成时间: formatDate(nodeEndTime),
时效天数: timelineValue,
计算方式: nodeCalculationMethod
});
}
if (isBackward && results.length > 0) {
const parsed = typeof results[0]?.estimatedStart === 'string' ? parseDate(results[0].estimatedStart) : null;
if (parsed && !isNaN(parsed.getTime())) {
setCalculatedRequiredStartTime(parsed);
} else {
setCalculatedRequiredStartTime(null);
}
}
let delayShowTimelineModal = false;
if (isBackward) {
const required = typeof results[0]?.estimatedStart === 'string' ? parseDate(results[0].estimatedStart) : null;
setCalculatedRequiredStartTime(required && !isNaN(required.getTime()) ? required : null);
if (required && currentStartTime && !isNaN(currentStartTime.getTime())) {
const diffDays = differenceInCalendarDays(required, currentStartTime);
const extraDays = Math.max(0, diffDays);
if (extraDays > 0) {
setAllocationNodesSnapshot(results);
setAllocationExtraDays(extraDays);
setAllocationMode('auto');
setAllocationVisible(true);
delayShowTimelineModal = true;
} else if (required.getTime() < currentStartTime.getTime()) {
if (showUI && bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: '当前起始日期晚于倒推要求起始日期,可能无法满足客户期望日期' });
}
}
}
}
if (!isBackward && showUI && mode === 'generate' && (!currentExpectedDate || isNaN(currentExpectedDate.getTime()))) {
const autoExpected = computeExpectedDateByBufferDays(results, 14, completionDateAdjustment);
if (autoExpected) {
setExpectedDate(autoExpected);
}
}
const nextAdjustments = remapTimelineAdjustmentsToNewResults(timelineResults, results);
setTimelineAdjustments(nextAdjustments);
pendingRecalculateAfterCalculateAdjustmentsRef.current = nextAdjustments;
pendingRecalculateAfterCalculateRef.current = true;
setTimelineResults(results);
if (showUI && !delayShowTimelineModal) {
setTimelineVisible(true);
} else if (showUI && delayShowTimelineModal) {
setTimelineVisible(false);
}
console.log('按流程顺序计算的时效结果:', results);
return results; // 返回计算结果
} catch (error) {
console.error('计算时效失败:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: ToastType.error,
message: '计算时效失败,请检查表格配置'
});
}
throw error; // 重新抛出错误
} finally {
setTimelineLoading(false);
}
};
// 复合调整处理函数:根据缓冲期和交期余量状态决定调整方式
const handleComplexAdjustment = (nodeKey: string, nodeIndex: number, adjustment: number) => {
const nextAdjustments = handleTimelineAdjustment(nodeKey, nodeIndex, adjustment);
if (!nextAdjustments) return;
const deficit = computeBufferDeficitDaysUsingEndDelta(nextAdjustments);
const prevDeficit = lastBufferDeficitRef.current;
lastBufferDeficitRef.current = deficit;
if (adjustment > 0 && deficit > 0 && Math.ceil(deficit) > Math.ceil(prevDeficit)) {
Modal.warning({
title: '缓冲期不足',
content: `当前缓冲期无法覆盖本次超期,缺口 ${Math.ceil(deficit)}`,
});
}
};
// 调整时效值的函数
const handleTimelineAdjustment = (nodeKey: string, nodeIndex: number, adjustment: number) => {
const newAdjustments = { ...timelineAdjustments };
const currentAdjustment = newAdjustments[nodeKey] || 0;
const newAdjustment = currentAdjustment + adjustment;
// 允许调整后的时效值为负数,用于向前回退结束时间
const baseValue = (typeof timelineResults[nodeIndex]?.timelineValue === 'number')
? timelineResults[nodeIndex]!.timelineValue
: (typeof timelineResults[nodeIndex]?.adjustedTimelineValue === 'number')
? timelineResults[nodeIndex]!.adjustedTimelineValue
: 0;
// 检查当前调整的节点是否为周转周期节点
const currentNodeName = timelineResults[nodeIndex]?.nodeName;
const isTurnoverNode = currentNodeName === '周转周期';
// 如果调整的是周转周期节点,直接返回(禁用手动调整)
if (isTurnoverNode) {
return null;
}
newAdjustments[nodeKey] = newAdjustment;
setTimelineAdjustments(newAdjustments);
// 使用智能重算逻辑,只重算被调整的节点及其后续节点
const hasAnyNonZeroAdjustment = Object.values(newAdjustments).some(v => v !== 0);
recalculateTimeline(newAdjustments, !hasAnyNonZeroAdjustment);
return newAdjustments;
};
// 获取重新计算后的时间线结果(不更新状态,逻辑对齐页面的重算口径)
const getRecalculatedTimeline = (
adjustments: Record<string, number>,
opts?: { ignoreActualCompletionDates?: boolean; actualCompletionDatesOverride?: { [key: number]: Date | null } }
) => {
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 nodeKey = buildTimelineAdjustmentKey(result, i);
const adjustment = adjustments[nodeKey] || 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: any) => item.type === 'text')
.map((item: any) => 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 || '外部';
const nodeExcludedDates = Array.isArray(result.excludedDates) ? result.excludedDates : [];
// 计算节点的结束时间(允许负时效值向前回退)
let nodeEndTime: Date;
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);
}
if (shouldForceEndTimeTo18(result) && nodeEndTime && !isNaN(nodeEndTime.getTime())) {
nodeEndTime = setTimeOfDay(nodeEndTime, 18, 0);
}
const actualEnd = opts?.ignoreActualCompletionDates
? null
: (opts?.actualCompletionDatesOverride ?? actualCompletionDates)?.[i] ?? null;
// 计算跳过的天数及日期范围内的自定义跳过日期
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates);
const skippedWeekends = calculateSkippedWeekends(adjustedStartTime, nodeEndTime, nodeWeekendDays);
const estimatedStartStr = formatDate(adjustedStartTime);
let estimatedEndStr = adjustedTimelineValue !== 0 ? formatDate(nodeEndTime) : '时效值为0';
if (actualEnd && actualEnd instanceof Date && !isNaN(actualEnd.getTime())) {
estimatedEndStr = formatDate(actualEnd);
}
const actualDays = calculateActualDays(estimatedStartStr, estimatedEndStr);
const excludedDatesInRange = calculateExcludedDatesInRange(adjustedStartTime, nodeEndTime, nodeExcludedDates);
// 更新结果
updatedResults[i] = {
...result,
adjustedTimelineValue,
estimatedStart: estimatedStartStr,
estimatedEnd: estimatedEndStr,
adjustment,
calculationMethod: nodeCalculationMethod,
skippedWeekends,
actualDays,
actualExcludedDates: excludedDatesInRange.dates,
actualExcludedDatesCount: excludedDatesInRange.count,
adjustmentDescription: result.adjustmentDescription,
ruleDescription
};
// 更新累积开始时间:使用当前节点的预计完成时间
if (adjustedTimelineValue !== 0) {
if (actualEnd && actualEnd instanceof Date && !isNaN(actualEnd.getTime())) {
cumulativeStartTime = new Date(actualEnd);
} else {
cumulativeStartTime = new Date(nodeEndTime);
}
}
}
return updatedResults;
};
const getRecalculatedTimelineBackward = (adjustments: Record<string, number>) => {
const updatedResults = [...timelineResults];
const fallbackEnd = (() => {
if (expectedDate && !isNaN(expectedDate.getTime())) return new Date(expectedDate);
const lastEndStr = updatedResults.length > 0 ? updatedResults[updatedResults.length - 1]?.estimatedEnd : null;
const parsed = typeof lastEndStr === 'string' ? parseDate(lastEndStr) : null;
return parsed && !isNaN(parsed.getTime()) ? parsed : new Date();
})();
let cumulativeEndTime = new Date(fallbackEnd);
for (let i = updatedResults.length - 1; i >= 0; i--) {
const result = updatedResults[i];
const baseTimelineValue = (typeof result.timelineValue === 'number')
? result.timelineValue
: (typeof result.adjustedTimelineValue === 'number')
? result.adjustedTimelineValue
: 0;
const nodeKey = buildTimelineAdjustmentKey(result, i);
const adjustment = adjustments[nodeKey] || 0;
const adjustedTimelineValue = baseTimelineValue + adjustment;
const nodeWeekendDays = result.weekendDaysConfig || [];
const nodeCalculationMethod = result.calculationMethod || '外部';
const nodeExcludedDates = Array.isArray(result.excludedDates) ? result.excludedDates : [];
let nodeEndTime = new Date(cumulativeEndTime);
if (shouldForceEndTimeTo18(result) && nodeEndTime && !isNaN(nodeEndTime.getTime())) {
nodeEndTime = setTimeOfDay(nodeEndTime, 18, 0);
}
let nodeStartTime: Date;
if (adjustedTimelineValue !== 0) {
if (nodeCalculationMethod === '内部') {
nodeStartTime = addInternalBusinessTime(nodeEndTime, -adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates);
} else {
nodeStartTime = addBusinessDaysWithHolidays(nodeEndTime, -adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates);
}
} else {
nodeStartTime = new Date(nodeEndTime);
}
const skippedWeekends = calculateSkippedWeekends(nodeStartTime, nodeEndTime, nodeWeekendDays);
const estimatedStartStr = formatDate(nodeStartTime);
const estimatedEndStr = formatDate(nodeEndTime);
const actualDays = calculateActualDays(estimatedStartStr, estimatedEndStr);
const excludedDatesInRange = calculateExcludedDatesInRange(nodeStartTime, nodeEndTime, nodeExcludedDates);
updatedResults[i] = {
...result,
adjustedTimelineValue,
estimatedStart: estimatedStartStr,
estimatedEnd: estimatedEndStr,
adjustment,
calculationMethod: nodeCalculationMethod,
skippedWeekends,
actualDays,
actualExcludedDates: excludedDatesInRange.dates,
actualExcludedDatesCount: excludedDatesInRange.count,
};
if (adjustedTimelineValue !== 0) {
cumulativeEndTime = new Date(nodeStartTime);
}
}
return updatedResults;
};
const buildAutoAllocationDraft = (nodes: any[], extraDays: number, excluded?: {[key: number]: boolean}) => {
const roundedExtra = Math.max(0, Math.round(Number(extraDays) * 100) / 100);
if (!Array.isArray(nodes) || nodes.length === 0 || roundedExtra === 0) return {};
const isTurnover = (node: any) => node?.nodeName === '周转周期';
const toBase = (node: any) => {
const v = typeof node?.timelineValue === 'number'
? node.timelineValue
: (typeof node?.adjustedTimelineValue === 'number' ? node.adjustedTimelineValue : 0);
return Number.isFinite(v) ? v : 0;
};
const eligible = nodes
.map((n, i) => ({ i, base: toBase(n), turnover: isTurnover(n), excluded: !!excluded?.[i] }))
.filter(x => !x.turnover && !x.excluded);
if (eligible.length === 0) return {};
const positive = eligible.filter(x => x.base > 0);
const pool = positive.length > 0 ? positive : eligible;
const totalBase = pool.reduce((s, x) => s + (x.base > 0 ? x.base : 1), 0);
const draft: {[key: number]: number} = {};
let remaining = roundedExtra;
for (let k = 0; k < pool.length; k++) {
const idx = pool[k].i;
if (k === pool.length - 1) {
draft[idx] = Math.max(0, Math.round(remaining * 100) / 100);
break;
}
const weight = pool[k].base > 0 ? pool[k].base : 1;
const raw = (roundedExtra * weight) / totalBase;
const val = Math.max(0, Math.round(raw * 100) / 100);
draft[idx] = val;
remaining = Math.max(0, Math.round((remaining - val) * 100) / 100);
}
return draft;
};
useEffect(() => {
if (!allocationVisible) return;
if (allocationMode !== 'auto') return;
const nodes = allocationNodesSnapshot.length > 0 ? allocationNodesSnapshot : timelineResults;
const draft = buildAutoAllocationDraft(nodes, allocationExtraDays, allocationExcluded);
setAllocationDraft(draft);
}, [allocationVisible, allocationMode, allocationExtraDays, allocationNodesSnapshot, timelineResults, allocationExcluded]);
const applyAllocationDraft = async () => {
const sum = Object.values(allocationDraft || {}).reduce((s, v) => s + (Number(v) || 0), 0);
const roundedSum = Math.round(sum * 100) / 100;
const roundedTarget = Math.round((Number(allocationExtraDays) || 0) * 100) / 100;
const diff = Math.round((roundedTarget - roundedSum) * 100) / 100;
if (Math.abs(diff) > 0.01) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: `当前分配合计${roundedSum}天,与盈余${roundedTarget}天不一致` });
}
return;
}
const merged: Record<string, number> = { ...(timelineAdjustments || {}) };
const nodes = allocationNodesSnapshot.length > 0 ? allocationNodesSnapshot : timelineResults;
for (const [k, v] of Object.entries(allocationDraft || {})) {
const idx = parseInt(k, 10);
const val = Math.max(0, Math.round((Number(v) || 0) * 100) / 100);
const node = nodes?.[idx];
const nodeKey = buildTimelineAdjustmentKey(node, idx);
merged[nodeKey] = Math.round(((Number(merged[nodeKey]) || 0) + val) * 100) / 100;
}
setTimelineAdjustments(merged);
setAllocationVisible(false);
recalculateTimeline(merged, true);
setTimelineVisible(true);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.success, message: '已应用盈余分配' });
}
};
// 获取有效的最后完成日期(忽略 '时效值为0'),若不存在则返回最后一项的日期
const getLastValidCompletionDateFromResults = (results: any[]): Date | null => {
if (!Array.isArray(results) || results.length === 0) return null;
for (let i = results.length - 1; i >= 0; i--) {
const endStr = results[i]?.estimatedEnd;
if (endStr && typeof endStr === 'string' && endStr !== '时效值为0') {
const d = new Date(endStr);
if (!isNaN(d.getTime())) return d;
}
}
const fallbackStr = results[results.length - 1]?.estimatedEnd;
const fallback = fallbackStr ? new Date(fallbackStr) : null;
return fallback && !isNaN(fallback.getTime()) ? fallback : null;
};
const computeLastNodeEndDeltaDays = (adjustments: Record<string, number>): number => {
try {
if (timelineDirection === 'backward') return 0;
const baseline = getRecalculatedTimeline({}, { ignoreActualCompletionDates: true });
const current = getRecalculatedTimeline(adjustments);
const pickLastNodeEnd = (results: any[]): Date | null => {
if (!Array.isArray(results) || results.length === 0) return null;
const endStr = results[results.length - 1]?.estimatedEnd;
if (endStr && typeof endStr === 'string' && endStr !== '时效值为0') {
const d = new Date(endStr);
if (!isNaN(d.getTime())) return d;
}
return getLastValidCompletionDateFromResults(results);
};
const baselineLast = pickLastNodeEnd(baseline);
const currentLast = pickLastNodeEnd(current);
if (!baselineLast || !currentLast) return 0;
const dayMs = 1000 * 60 * 60 * 24;
return Math.ceil((currentLast.getTime() - baselineLast.getTime()) / dayMs);
} catch {
return 0;
}
};
const getAdjustedLastCompletionDate = (results: any[], completionAdjustmentOverride?: number): Date | null => {
const lastCompletionDate = getLastValidCompletionDateFromResults(results);
if (!lastCompletionDate) return null;
const adjusted = new Date(lastCompletionDate);
const adj = Number.isFinite(completionAdjustmentOverride as number)
? (completionAdjustmentOverride as number)
: completionDateAdjustment;
if (Number.isFinite(adj) && adj !== 0) {
adjusted.setDate(adjusted.getDate() + adj);
}
return adjusted;
};
const computeExpectedDateByBufferDays = (
results: any[],
bufferDays: number,
completionAdjustmentOverride?: number
): Date | null => {
if (timelineDirection === 'backward') return null;
if (!Number.isFinite(bufferDays)) return null;
const adjustedCompletionDate = getAdjustedLastCompletionDate(results, completionAdjustmentOverride);
if (!adjustedCompletionDate) return null;
const next = new Date(adjustedCompletionDate);
next.setDate(next.getDate() + Math.round(bufferDays));
return next;
};
const computeAutoBufferDaysUsingExpectedDate = (
results: any[],
expectedDateOverride?: Date | null,
completionAdjustmentOverride?: number
): number => {
try {
if (timelineDirection === 'backward') return 0;
const expected = expectedDateOverride ?? expectedDate;
if (!expected || isNaN(expected.getTime())) return 0;
const adjustedCompletionDate = getAdjustedLastCompletionDate(results, completionAdjustmentOverride);
if (!adjustedCompletionDate) return 0;
return differenceInCalendarDays(expected, adjustedCompletionDate);
} catch {
return 0;
}
};
const computeBufferDeficitDaysUsingEndDelta = (adjustments: Record<string, number>): number => {
const deltaDays = computeLastNodeEndDeltaDays(adjustments);
const autoBufferDays = computeAutoBufferDaysUsingExpectedDate(timelineResults, expectedDate, completionDateAdjustment);
return Math.max(0, deltaDays - autoBufferDays);
};
const computeExpectedDeliveryDateTsFromResults = (
results: any[],
adjustments: Record<string, number>,
expectedDateOverride?: Date | null,
completionAdjustmentOverride?: number
): number | null => {
if (timelineDirection === 'backward') return null;
const adjustedCompletionDate = getAdjustedLastCompletionDate(results, completionAdjustmentOverride);
if (!adjustedCompletionDate) return null;
const expected = expectedDateOverride ?? expectedDate;
if (expected && !isNaN(expected.getTime())) {
const ts = expected.getTime();
return Number.isFinite(ts) ? ts : null;
}
const ts = adjustedCompletionDate.getTime();
return Number.isFinite(ts) ? ts : null;
};
const handleExpectedDeliveryDateLockChange = (checked: boolean) => {
const nextLocked = !!checked;
setIsExpectedDeliveryDateLocked(nextLocked);
if (!nextLocked) {
setLockedExpectedDeliveryDateTs(null);
return;
}
if (!expectedDate || isNaN(expectedDate.getTime())) {
setIsExpectedDeliveryDateLocked(false);
setLockedExpectedDeliveryDateTs(null);
if (bitable.ui.showToast) {
bitable.ui.showToast({ toastType: ToastType.warning, message: '请先选择客户期望日期,再开启锁交付' });
}
return;
}
setLockedExpectedDeliveryDateTs(expectedDate.getTime());
};
const computeDynamicBufferDaysUsingEndDelta = (
adjustments: Record<string, number>,
expectedDateOverride?: Date | null,
resultsOverride?: any[],
completionAdjustmentOverride?: number
): number => {
const results = resultsOverride ?? timelineResults;
const expected = expectedDateOverride ?? expectedDate;
const completionAdj = Number.isFinite(completionAdjustmentOverride as number)
? (completionAdjustmentOverride as number)
: completionDateAdjustment;
return computeAutoBufferDaysUsingExpectedDate(results, expected, completionAdj);
};
// 重新计算时间线的函数
const recalculateTimeline = (adjustments: Record<string, number>, forceRecalculateAll: boolean = false) => {
if (timelineDirection === 'backward') {
const updated = getRecalculatedTimelineBackward(adjustments);
setTimelineResults(updated);
const firstStartStr = updated.length > 0 ? updated[0]?.estimatedStart : null;
const parsed = typeof firstStartStr === 'string' ? parseDate(firstStartStr) : null;
if (parsed && !isNaN(parsed.getTime())) {
setCalculatedRequiredStartTime(parsed);
} else {
setCalculatedRequiredStartTime(null);
}
return;
}
const updatedResults = [...timelineResults];
// 找到第一个被调整的节点索引
const adjustedIndices = updatedResults
.map((r, i) => {
const key = buildTimelineAdjustmentKey(r, i);
const v = adjustments[key] || 0;
return v !== 0 ? i : -1;
})
.filter(i => 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 {
// 使用预计完成时间
const prevEndParsed = typeof previousResult.estimatedEnd === 'string'
? parseDate(previousResult.estimatedEnd)
: previousResult.estimatedEnd as any as Date;
// 当无法解析前一节点的预计完成时,安全回退到全局起始时间(或当前时间),避免使用未初始化的累计时间
cumulativeStartTime = (prevEndParsed && !isNaN(prevEndParsed.getTime()))
? new Date(prevEndParsed)
: (startTime ? new Date(startTime) : new Date());
}
}
// 只重新计算从第一个调整节点开始的后续节点
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 nodeKey = buildTimelineAdjustmentKey(result, i);
const adjustment = adjustments[nodeKey] || 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: any) => item.type === 'text')
.map((item: any) => 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);
}
if (shouldForceEndTimeTo18(result) && nodeEndTime && !isNaN(nodeEndTime.getTime())) {
nodeEndTime = setTimeOfDay(nodeEndTime, 18, 0);
}
// 计算跳过的天数
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates);
const skippedWeekends = calculateSkippedWeekends(adjustedStartTime, nodeEndTime, nodeWeekendDays);
const estimatedStartStr = formatDate(adjustedStartTime);
let estimatedEndStr = adjustedTimelineValue !== 0 ? formatDate(nodeEndTime) : '时效值为0';
// 若当前节点存在实际完成时间,则用于展示与跨度计算,以实现与后续节点的实际连线
if (actualCompletionDates[i]) {
estimatedEndStr = formatDate(actualCompletionDates[i]!);
}
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);
const [hasAppliedSuggestedBuffer, setHasAppliedSuggestedBuffer] = useState(false);
const [lastSuggestedApplied, setLastSuggestedApplied] = useState<number | null>(null);
// 初始状态快照(仅捕获一次)
const initialSnapshotRef = useRef<any>(null);
const hasCapturedInitialSnapshotRef = useRef<boolean>(false);
// 当起始时间变更时,重新以最新起始时间为基准重算全流程
useEffect(() => {
if (timelineDirection !== 'forward') return;
if (timelineResults.length > 0 && !isRestoringSnapshot) {
recalculateTimeline(timelineAdjustments, true); // 强制重算所有节点
}
}, [startTime, isRestoringSnapshot, timelineDirection]);
useEffect(() => {
if (timelineDirection !== 'backward') return;
if (!expectedDate) return;
if (timelineResults.length > 0 && !isRestoringSnapshot) {
recalculateTimeline(timelineAdjustments, true);
}
}, [expectedDate, isRestoringSnapshot, timelineDirection]);
// 当实际完成日期变化时,以最新状态进行重算,避免首次选择不触发或使用旧值
useEffect(() => {
if (timelineResults.length > 0 && !isRestoringSnapshot) {
recalculateTimeline(timelineAdjustments, false);
}
}, [actualCompletionDates, isRestoringSnapshot]);
useEffect(() => {
if (!pendingRecalculateAfterExcludedDatesRef.current) return;
if (isRestoringSnapshot) return;
pendingRecalculateAfterExcludedDatesRef.current = false;
if (timelineResults.length > 0) {
recalculateTimeline(timelineAdjustments, true);
}
}, [timelineResults, isRestoringSnapshot]);
useEffect(() => {
if (!pendingRecalculateAfterCalculateRef.current) return;
if (isRestoringSnapshot) return;
pendingRecalculateAfterCalculateRef.current = false;
const adjustments = pendingRecalculateAfterCalculateAdjustmentsRef.current || timelineAdjustments;
pendingRecalculateAfterCalculateAdjustmentsRef.current = null;
if (timelineResults.length > 0) {
recalculateTimeline(adjustments, true);
}
}, [timelineResults, isRestoringSnapshot]);
// 捕获初始状态快照(在首次生成/还原出完整时间线后)
useEffect(() => {
if (!hasCapturedInitialSnapshotRef.current && timelineResults.length > 0) {
initialSnapshotRef.current = {
timelineDirection,
startTime,
expectedDate,
selectedLabels,
timelineResults,
timelineAdjustments,
baseBufferDays,
lockedExpectedDeliveryDateTs,
isExpectedDeliveryDateLocked,
actualCompletionDates,
completionDateAdjustment,
hasAppliedSuggestedBuffer,
lastSuggestedApplied,
deliveryMarginDeductions,
};
hasCapturedInitialSnapshotRef.current = true;
console.log('已捕获初始状态快照');
}
}, [timelineResults, startTime, expectedDate]);
// 重置调整的函数
const resetTimelineAdjustments = () => {
setTimelineAdjustments({});
setDeliveryMarginDeductions(0); // 同时重置交期余量扣减
setCompletionDateAdjustment(0); // 重置最后流程完成日期调整
setActualCompletionDates({}); // 重置实际完成日期
setBaseBufferDays(0); // 重置固定缓冲期为默认值
setIsExpectedDeliveryDateLocked(false);
try { lastBufferDeficitRef.current = 0; } catch {}
setHasAppliedSuggestedBuffer(false); // 重置建议缓冲期应用标志
setLastSuggestedApplied(null); // 清空上次建议值
recalculateTimeline({}, true); // 强制重算所有节点
};
// 一键还原到最初状态
const resetToInitialState = async () => {
try {
if (!hasCapturedInitialSnapshotRef.current || !initialSnapshotRef.current) {
// 若未捕获到初始快照,则退化为仅重置调整
resetTimelineAdjustments();
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: '未检测到初始快照,已重置调整项' });
}
return;
}
const s = initialSnapshotRef.current;
setIsRestoringSnapshot(true);
if (s.timelineDirection === 'forward' || s.timelineDirection === 'backward') {
setTimelineDirection(s.timelineDirection);
}
setStartTime(s.startTime || null);
setExpectedDate(s.expectedDate || null);
setSelectedLabels(s.selectedLabels || {});
const normalizedAdjustments = normalizeTimelineAdjustmentsFromSnapshot(s.timelineAdjustments || {}, s.timelineResults || []);
setTimelineAdjustments(normalizedAdjustments);
setBaseBufferDays(s.baseBufferDays ?? 0);
const shouldLock = s.isExpectedDeliveryDateLocked === true || (s.isExpectedDeliveryDateLocked === undefined && s.lockedExpectedDeliveryDateTs !== null && s.lockedExpectedDeliveryDateTs !== undefined);
if (shouldLock && typeof s.lockedExpectedDeliveryDateTs === 'number' && Number.isFinite(s.lockedExpectedDeliveryDateTs)) {
setLockedExpectedDeliveryDateTs(s.lockedExpectedDeliveryDateTs);
setIsExpectedDeliveryDateLocked(true);
} else {
setLockedExpectedDeliveryDateTs(null);
setIsExpectedDeliveryDateLocked(false);
}
setActualCompletionDates(s.actualCompletionDates || {});
setCompletionDateAdjustment(s.completionDateAdjustment || 0);
setHasAppliedSuggestedBuffer(!!s.hasAppliedSuggestedBuffer && s.hasAppliedSuggestedBuffer);
setLastSuggestedApplied(s.lastSuggestedApplied ?? null);
setDeliveryMarginDeductions(s.deliveryMarginDeductions || 0);
setTimelineResults(Array.isArray(s.timelineResults) ? s.timelineResults : []);
recalculateTimeline(normalizedAdjustments, true);
setIsRestoringSnapshot(false);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.success, message: '已恢复至最初状态' });
}
} catch (e) {
console.error('恢复初始状态失败:', e);
setIsRestoringSnapshot(false);
}
};
// 已移除未使用的 getTimelineLabelFieldId 辅助函数
// 写入货期记录表的函数
const writeToDeliveryRecordTable = async (
timelineResults: any[],
processRecordIds: string[],
timelineAdjustments: Record<string, number> = {},
overrides?: { foreignId?: string; style?: string; color?: string; expectedDate?: Date | null; startTime?: Date | null; selectedLabels?: {[key: string]: string | string[]}; baseBufferDays?: number }
) => {
let recordCells: any[] | undefined;
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, // 新增:起始时间字段
DELIVERY_FACTORY_DEPARTURE_DATE_FIELD_ID,
DELIVERY_SNAPSHOT_JSON_FIELD_ID,
DELIVERY_SNAPSHOT_JSON_2_FIELD_ID
];
console.log('需要检查的字段ID列表:', fieldsToCheck);
// 获取各个字段
console.log('正在获取各个字段对象...');
const [
foreignIdField,
labelsField,
styleField,
colorField,
text2Field,
createTimeField,
expectedDateField,
nodeDetailsField,
customerExpectedDateField,
adjustmentInfoField,
versionField,
startTimeField,
snapshotField,
snapshot2Field,
recordIdsTextField,
factoryDepartureDateField
] = await Promise.all([
deliveryRecordTable.getField(DELIVERY_FOREIGN_ID_FIELD_ID),
deliveryRecordTable.getField(DELIVERY_LABELS_FIELD_ID),
deliveryRecordTable.getField(DELIVERY_STYLE_FIELD_ID),
deliveryRecordTable.getField(DELIVERY_COLOR_FIELD_ID),
deliveryRecordTable.getField(DELIVERY_TEXT2_FIELD_ID),
deliveryRecordTable.getField(DELIVERY_CREATE_TIME_FIELD_ID),
deliveryRecordTable.getField(DELIVERY_EXPECTED_DATE_FIELD_ID),
deliveryRecordTable.getField(DELIVERY_NODE_DETAILS_FIELD_ID),
deliveryRecordTable.getField(DELIVERY_CUSTOMER_EXPECTED_DATE_FIELD_ID),
deliveryRecordTable.getField(DELIVERY_ADJUSTMENT_INFO_FIELD_ID),
deliveryRecordTable.getField(DELIVERY_VERSION_FIELD_ID),
deliveryRecordTable.getField(DELIVERY_START_TIME_FIELD_ID),
deliveryRecordTable.getField(DELIVERY_SNAPSHOT_JSON_FIELD_ID),
deliveryRecordTable.getField(DELIVERY_SNAPSHOT_JSON_2_FIELD_ID),
deliveryRecordTable.getField(DELIVERY_RECORD_IDS_FIELD_ID),
deliveryRecordTable.getField(DELIVERY_FACTORY_DEPARTURE_DATE_FIELD_ID)
]);
console.log('成功获取所有字段对象');
// 检查标签汇总字段的类型
const labelsFieldDebug = labelsField as any;
console.log('标签汇总字段信息:', {
id: labelsFieldDebug?.id,
name: labelsFieldDebug?.name,
type: labelsFieldDebug?.type,
property: labelsFieldDebug?.property
});
// 获取foreign_id调整模式严格使用快照数据生成模式优先使用选择记录
console.log('=== 开始获取foreign_id ===');
let foreignId = overrides?.foreignId ?? '';
// 使用全局状态
const currentSelectedRecords = selectedRecords;
const currentRecordDetails = recordDetails;
if (!foreignId && mode === 'adjust') {
// 调整模式严格使用快照回填的foreign_id即使为空也不回退
foreignId = currentForeignId ?? '';
console.log('调整模式严格使用快照恢复的foreign_id:', foreignId);
} else if (!foreignId && currentSelectedRecords.length > 0) {
// 生成模式:从选择记录获取
console.log('生成模式从选择记录获取foreign_id');
console.log('selectedRecords[0]:', currentSelectedRecords[0]);
// 生成模式:从数据库获取
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 (!foreignId && mode !== 'adjust' && currentRecordDetails.length > 0) {
const first = currentRecordDetails[0];
const val = first.fields['fldpvBfeC0'];
foreignId = extractText(val);
}
// 生成模式的最后回退:快照状态
if (!foreignId && mode !== 'adjust' && currentForeignId) {
foreignId = currentForeignId;
}
// 获取款式与颜色:调整模式优先使用快照数据,生成模式优先使用记录详情
let style = overrides?.style ?? '';
let color = overrides?.color ?? '';
if (!style && !color && mode === 'adjust') {
// 调整模式:严格使用快照回填的数据,即使为空也不回退
style = currentStyleText;
color = currentColorText;
console.log('调整模式:严格使用快照恢复的款式:', style, '颜色:', color);
} else {
// 生成模式:优先使用记录详情
if (!style && !color && currentRecordDetails.length > 0) {
const first = currentRecordDetails[0];
style = extractText(first.fields['fld6Uw95kt']) || currentStyleText || '';
color = extractText(first.fields['flde85ni4O']) || currentColorText || '';
} else {
// 回退:使用快照回填的状态
style = style || currentStyleText || '';
color = 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 {
// 生成模式文本2字段保持为空
text2 = '';
console.log('生成模式文本2字段保持为空');
}
// 获取标签汇总批量模式优先使用传入的labels
const selectedLabelValues = Object.values(overrides?.selectedLabels ?? selectedLabels).flat().filter(Boolean);
// 获取预计交付日期(交期余量的日期版本:根据客户期望日期自动计算缓冲期)
let expectedDeliveryDate = computeExpectedDeliveryDateTsFromResults(timelineResults, timelineAdjustments, overrides?.expectedDate ?? expectedDate, completionDateAdjustment);
if (isExpectedDeliveryDateLocked && lockedExpectedDeliveryDateTs !== null) {
expectedDeliveryDate = lockedExpectedDeliveryDateTs;
}
// 获取客户期望日期批量模式优先使用传入的expectedDate
let customerExpectedDate = null;
const expectedDateToUse = overrides?.expectedDate ?? expectedDate;
if (expectedDateToUse) {
customerExpectedDate = expectedDateToUse.getTime();
}
let factoryDepartureDate = null as number | null;
const reservationInbound = timelineResults.find(r => r?.nodeName === '预约入库');
const reservationEnd = reservationInbound?.estimatedEnd;
if (reservationEnd instanceof Date && !isNaN(reservationEnd.getTime())) {
factoryDepartureDate = reservationEnd.getTime();
} else if (typeof reservationEnd === 'number' && Number.isFinite(reservationEnd)) {
factoryDepartureDate = reservationEnd;
} else if (typeof reservationEnd === 'string' && reservationEnd.trim() !== '') {
const parsed = parseDate(reservationEnd);
if (parsed) {
factoryDepartureDate = parsed.getTime();
}
}
// 创建当前时间戳
const currentTime = new Date().getTime();
// 计算版本号(数字)并格式化货期调整信息
let versionNumber = 1;
if (mode === 'adjust' && currentVersionNumber !== null) {
versionNumber = currentVersionNumber + 1;
}
let adjustmentInfo = `版本V${versionNumber}`;
if (Object.keys(timelineAdjustments).length > 0) {
const nodeNameByKey = new Map<string, string>();
for (let i = 0; i < timelineResults.length; i++) {
const r = timelineResults[i];
const k = buildTimelineAdjustmentKey(r, i);
if (!nodeNameByKey.has(k)) nodeNameByKey.set(k, r?.nodeName || k);
}
const adjustmentTexts = Object.entries(timelineAdjustments).map(([nodeKey, adjustment]) => {
const nodeName = nodeNameByKey.get(nodeKey) || nodeKey;
return `${nodeName}: ${adjustment > 0 ? '+' : ''}${adjustment.toFixed(1)}`;
});
adjustmentInfo += `\n当前调整\n${adjustmentTexts.join('\n')}`;
}
// 在创建Cell之前进行数据校验移除冗余日志
// ===== 构建完整快照(保持与流程数据表写入时的一致内容)并写入到货期记录表 =====
const expectedDateTimestamp = expectedDateToUse ? expectedDateToUse.getTime() : null;
const expectedDateString = expectedDateToUse ? format(expectedDateToUse, DATE_FORMATS.STORAGE_FORMAT) : null;
const currentStartTime = overrides?.startTime ?? startTime;
const currentSelectedLabels = overrides?.selectedLabels ?? selectedLabels;
// 与快照字段保持相同的命名
const styleText = style || '';
const colorText = color || '';
const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments, expectedDateToUse, timelineResults, completionDateAdjustment);
// 检查是否达到最终限制
let hasReachedFinalLimit = false;
const currentExpectedDate = expectedDateToUse;
if (currentExpectedDate) {
const adjustedLastCompletion = getAdjustedLastCompletionDate(timelineResults, completionDateAdjustment);
if (adjustedLastCompletion) {
const daysToExpected = differenceInCalendarDays(currentExpectedDate, adjustedLastCompletion);
hasReachedFinalLimit = daysToExpected <= 0;
}
}
// 为快照提供基础缓冲与节点调整总量(用于兼容历史字段),尽管动态缓冲期已改为“自然日差”口径
const baseBuferDays = dynamicBufferDays;
const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0);
const globalSnapshot = {
version: versionNumber,
foreignId,
styleText,
colorText,
text2,
mode,
timelineDirection,
excludedDatesOverride,
excludedDatesByNodeOverride: excludedDatesByNodeOverrideRef.current || excludedDatesByNodeOverride,
lockedExpectedDeliveryDateTs,
isExpectedDeliveryDateLocked,
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
},
...(timelineDirection !== 'backward'
? {
bufferManagement: {
baseDays: baseBuferDays,
totalAdjustments,
dynamicBufferDays,
hasReachedFinalLimit,
hasAppliedSuggestedBuffer,
lastSuggestedApplied: lastSuggestedApplied ?? 0
},
}
: {}),
chainAdjustmentSystem: {
enabled: true,
lastCalculationTime: new Date().getTime(),
adjustmentHistory: timelineAdjustments
},
timelineCalculationState: {
calculationTimestamp: new Date().getTime(),
totalNodes: timelineResults.length,
hasValidResults: timelineResults.length > 0,
lastCalculationMode: mode,
timelineDirection,
excludedDatesOverride,
excludedDatesByNodeOverride: excludedDatesByNodeOverrideRef.current || excludedDatesByNodeOverride
},
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: typeof selectedResult.timelineValue === 'number' && 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);
const snapshotPart1MaxLen = 80000;
const snapshotJson1 = snapshotJson.length > snapshotPart1MaxLen ? snapshotJson.slice(0, snapshotPart1MaxLen) : snapshotJson;
const snapshotJson2 = snapshotJson.length > snapshotPart1MaxLen ? snapshotJson.slice(snapshotPart1MaxLen) : '';
// 使用createCell方法创建各个字段的Cell
const startTimestamp = currentStartTime ? currentStartTime.getTime() : currentTime;
const recordIdsText = (restoredRecordIdsText && restoredRecordIdsText.trim() !== '')
? restoredRecordIdsText.trim()
: '';
const [
foreignIdCell,
labelsCell,
styleCell,
colorCell,
text2Cell,
createTimeCell,
startTimeCell,
snapshotCell,
snapshot2Cell,
expectedDateCell,
customerExpectedDateCell,
factoryDepartureDateCell,
nodeDetailsCell,
adjustmentInfoCell,
versionCell,
recordIdsCell
] = await Promise.all([
foreignIdField.createCell(foreignId),
selectedLabelValues.length > 0 ? labelsField.createCell(selectedLabelValues) : Promise.resolve(null),
styleField.createCell(style),
colorField.createCell(color),
text2Field.createCell(text2),
createTimeField.createCell(currentTime),
startTimeField.createCell(startTimestamp),
snapshotField.createCell(snapshotJson1),
snapshotJson2 ? snapshot2Field.createCell(snapshotJson2) : Promise.resolve(null),
expectedDeliveryDate ? expectedDateField.createCell(expectedDeliveryDate) : Promise.resolve(null),
customerExpectedDate ? customerExpectedDateField.createCell(customerExpectedDate) : Promise.resolve(null),
factoryDepartureDate ? factoryDepartureDateField.createCell(factoryDepartureDate) : Promise.resolve(null),
processRecordIds.length > 0 ? nodeDetailsField.createCell({ recordIds: processRecordIds }) : Promise.resolve(null),
adjustmentInfo ? adjustmentInfoField.createCell(adjustmentInfo) : Promise.resolve(null),
versionField.createCell(versionNumber),
recordIdsTextField.createCell(recordIdsText)
]);
// 组合所有Cell到一个记录中
recordCells = [foreignIdCell, styleCell, colorCell, text2Cell, createTimeCell, startTimeCell, versionCell, snapshotCell];
if (snapshot2Cell) recordCells.push(snapshot2Cell);
recordCells.push(recordIdsCell);
// 只有当数据存在时才添加对应的Cell
if (labelsCell) recordCells.push(labelsCell);
if (expectedDateCell) recordCells.push(expectedDateCell);
if (customerExpectedDateCell) recordCells.push(customerExpectedDateCell);
if (factoryDepartureDateCell) recordCells.push(factoryDepartureDateCell);
if (nodeDetailsCell) recordCells.push(nodeDetailsCell);
if (adjustmentInfoCell) recordCells.push(adjustmentInfoCell);
// 添加记录到货期记录表
const addedRecord = await deliveryRecordTable.addRecord(recordCells);
return addedRecord;
} catch (error: any) {
console.error('写入货期记录表详细错误:', {
error: error,
message: error?.message,
stack: error?.stack,
recordCellsLength: Array.isArray(recordCells) ? recordCells.length : 'recordCells未定义'
});
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: ToastType.error,
message: '写入货期记录表失败: ' + (error as Error).message
});
}
throw error;
}
};
// 写入流程数据表的函数
const writeToProcessDataTable = async (timelineResults: any[], overrides?: { foreignId?: string; style?: string; color?: string }): Promise<string[]> => {
try {
console.log('=== 开始写入流程数据表 ===');
console.log('当前模式:', mode);
console.log('timelineResults数量:', timelineResults?.length || 0);
// 获取流程数据表和流程配置表
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,
processNameField,
processOrderField,
startDateField,
endDateField,
versionField,
timelinessField,
processStyleField,
processColorField,
processGroupField
] = await Promise.all([
processDataTable.getField(FOREIGN_ID_FIELD_ID),
processDataTable.getField(PROCESS_NAME_FIELD_ID),
processDataTable.getField(PROCESS_ORDER_FIELD_ID_DATA),
processDataTable.getField(ESTIMATED_START_DATE_FIELD_ID),
processDataTable.getField(ESTIMATED_END_DATE_FIELD_ID),
processDataTable.getField(PROCESS_VERSION_FIELD_ID),
processDataTable.getField(PROCESS_TIMELINESS_FIELD_ID),
processDataTable.getField(PROCESS_STYLE_FIELD_ID),
processDataTable.getField(PROCESS_COLOR_FIELD_ID),
processDataTable.getField(PROCESS_GROUP_FIELD_ID_DATA)
]);
console.log('成功获取所有字段');
// 获取foreign_id - 支持批量模式直接传递数据
console.log('=== 开始获取foreign_id ===');
let foreignId = overrides?.foreignId ?? null;
// 使用全局状态
const currentSelectedRecords = selectedRecords;
const currentRecordDetails = 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: ToastType.warning,
message: '未找到foreign_id字段无法写入流程数据表'
});
}
return [];
}
// 获取款式与颜色:与货期记录写入逻辑保持一致
let style = overrides?.style ?? '';
let color = overrides?.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);
}
}
}
}
// 构建页面快照JSON确保可一模一样还原
// 计算版本号:仅在调整模式下递增
let versionNumber = 1;
if (mode === 'adjust' && currentVersionNumber !== null) {
versionNumber = currentVersionNumber + 1;
}
// 使用createCell方法准备要写入的记录数据
const recordValueList: Array<{ fields: Record<string, any> }> = [];
const fallbackCellRows: any[] = [];
let selectOptions: Array<{ id: string; name: string }> = [];
try {
if ((processNameField as any)?.getOptions) {
selectOptions = await (processNameField as any).getOptions();
}
} catch {}
if (!selectOptions || selectOptions.length === 0) {
const propOptions = (processNameField as any)?.property?.options;
if (Array.isArray(propOptions)) {
selectOptions = propOptions;
}
}
const optionNameToId = new Map<string, string>();
for (const opt of (selectOptions || [])) {
if (opt && typeof (opt as any).name === 'string' && typeof (opt as any).id === 'string') {
optionNameToId.set((opt as any).name, (opt as any).id);
}
}
const _processGroupField = processGroupField;
for (let index = 0; index < timelineResults.length; index++) {
const result = timelineResults[index];
const hasValidTimelineValue = typeof result.timelineValue === 'number' && Number.isFinite(result.timelineValue) && result.timelineValue > 0;
if (!hasValidTimelineValue) {
console.log(`跳过节点 "${result.nodeName}" - 未匹配到时效值`);
continue;
}
// 检查是否有有效的预计完成时间(只检查结束时间)
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);
}
}
const nodeName = result.nodeName;
const optionId = typeof nodeName === 'string' ? optionNameToId.get(nodeName) : undefined;
const rawGroupName =
(typeof result.processGroupInstanceName === 'string' && result.processGroupInstanceName.trim() !== '')
? result.processGroupInstanceName
: (typeof result.processGroup === 'string'
? result.processGroup
: extractText(result.processGroup));
const groupName = (rawGroupName || '').trim();
if (optionId) {
const fields: Record<string, any> = {
[FOREIGN_ID_FIELD_ID]: foreignId,
[PROCESS_NAME_FIELD_ID]: { id: optionId, text: nodeName },
[PROCESS_ORDER_FIELD_ID_DATA]: result.processOrder,
[PROCESS_STYLE_FIELD_ID]: style,
[PROCESS_COLOR_FIELD_ID]: color,
[PROCESS_VERSION_FIELD_ID]: versionNumber,
[PROCESS_TIMELINESS_FIELD_ID]: result.timelineValue
};
if (startTimestamp) fields[ESTIMATED_START_DATE_FIELD_ID] = startTimestamp;
if (endTimestamp) fields[ESTIMATED_END_DATE_FIELD_ID] = endTimestamp;
if (groupName) {
(fields as any)[PROCESS_GROUP_FIELD_ID_DATA] = groupName;
}
recordValueList.push({ fields });
} else {
const foreignIdCell = await foreignIdField.createCell(foreignId);
const processNameCell = await processNameField.createCell(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);
const versionCell = await versionField.createCell(versionNumber);
const timelinessCell = await timelinessField.createCell(result.timelineValue);
const processGroupCell = groupName ? await _processGroupField.createCell(groupName) : null;
const recordCells = [
foreignIdCell,
processNameCell,
processOrderCell,
styleCell,
colorCell,
versionCell,
timelinessCell
];
if (startDateCell) recordCells.push(startDateCell);
if (endDateCell) recordCells.push(endDateCell);
if (processGroupCell) recordCells.push(processGroupCell);
fallbackCellRows.push(recordCells);
}
}
console.log('准备写入记录数:', recordValueList.length + fallbackCellRows.length);
// 在添加记录的部分收集记录ID
const addedRecordIds: string[] = [];
if (recordValueList.length > 0 || fallbackCellRows.length > 0) {
if (recordValueList.length > 0) {
try {
const addedRecords = await processDataTable.addRecords(recordValueList as any);
addedRecordIds.push(...(addedRecords as any));
} catch (error) {
console.error('批量写入(IRecordValue)失败,尝试逐条写入:', error);
for (const rv of recordValueList) {
try {
const addedRecord = await processDataTable.addRecord(rv as any);
addedRecordIds.push(addedRecord as any);
} catch (e) {
console.error('逐条写入(IRecordValue)失败:', e, rv);
}
}
}
}
if (fallbackCellRows.length > 0) {
try {
const addedRecords2 = await (processDataTable as any).addRecordsByCell(fallbackCellRows);
addedRecordIds.push(...(addedRecords2 as any));
} catch (error) {
console.error('回退批量写入(ICell[])失败,尝试逐条写入:', error);
for (const recordCells of fallbackCellRows) {
const addedRecord = await processDataTable.addRecord(recordCells);
addedRecordIds.push(addedRecord as any);
}
}
}
console.log(`成功写入 ${addedRecordIds.length} 条流程数据`);
// 显示成功提示
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: ToastType.success,
message: `成功写入 ${addedRecordIds.length} 条流程数据到流程数据表`
});
}
return addedRecordIds; // 返回记录ID列表
} else {
console.warn('没有有效的记录可以写入');
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: ToastType.warning,
message: '没有有效的流程数据可以写入 - 未匹配到时效值'
});
}
return [];
}
} catch (error) {
console.error('写入流程数据表失败:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: ToastType.error,
message: `写入流程数据表失败: ${(error as Error).message}`
});
}
return [];
}
};
// 保存时效数据的核心逻辑(从确定并保存按钮提取)
const saveTimelineData = async () => {
try {
// 使用全局状态
const currentTimelineResults = timelineResults;
const currentTimelineAdjustments = timelineAdjustments;
if (currentTimelineResults.length > 0) {
// 写入流程数据表
const processRecordIds = await writeToProcessDataTable(currentTimelineResults);
// 写入货期记录表
const deliveryRecord = await writeToDeliveryRecordTable(currentTimelineResults, processRecordIds, currentTimelineAdjustments);
const deliveryRecordId = typeof deliveryRecord === 'string'
? deliveryRecord
: (deliveryRecord && typeof deliveryRecord === 'object'
? ((deliveryRecord as any).id || (deliveryRecord as any).recordId || (deliveryRecord as any).record_id || '')
: '');
if (mode === 'adjust') {
setCurrentDeliveryRecordId(deliveryRecordId || null);
if (currentVersionNumber !== null) {
setCurrentVersionNumber(currentVersionNumber + 1);
}
}
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 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: ToastType.error,
message: '保存数据失败,请重试'
});
}
throw error; // 重新抛出错误
}
};
const copyToClipboard = async (text: string) => {
if (!text) return;
const fallbackCopy = async () => {
const doc: any = typeof document !== 'undefined' ? document : null;
const textarea = doc?.createElement?.('textarea');
if (!textarea) {
throw new Error('无法访问剪贴板');
}
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
textarea.style.left = '-9999px';
doc.body.appendChild(textarea);
textarea.focus();
textarea.select();
const ok = doc.execCommand?.('copy');
doc.body.removeChild(textarea);
if (!ok) {
throw new Error('复制失败');
}
};
try {
const nav: any = typeof navigator !== 'undefined' ? navigator : null;
if (nav?.clipboard?.writeText) {
try {
await nav.clipboard.writeText(text);
} catch {
await fallbackCopy();
}
} else {
await fallbackCopy();
}
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.success, message: '已复制' });
}
} catch (e: any) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.error, message: e?.message ? `复制失败:${e.message}` : '复制失败' });
}
}
};
const handleBatchProcess = async (range?: { start: number; end: number }) => {
try {
resetGlobalState({ resetMode: false });
setModeSelectionVisible(false);
setBatchLoading(true);
setBatchProcessedCount(0);
setBatchSuccessCount(0);
setBatchFailureCount(0);
setBatchProgressList([]);
batchAbortRef.current = false;
const batchTable = await bitable.base.getTable(BATCH_TABLE_ID);
const fieldMetaList = await batchTable.getFieldMetaList();
const nameToId = new Map<string, string>();
for (const meta of (fieldMetaList || [])) {
if (!meta) continue;
const nm = (meta as any).name;
const id = (meta as any).id;
if (typeof nm === 'string' && typeof id === 'string' && nm.trim() && id.trim()) {
nameToId.set(nm, id);
}
}
const rows = await fetchAllRecordsByPage(batchTable, undefined);
const rowsWithNo = rows.map((row: any, idx: number) => {
const f = row?.fields || {};
const no = parseBatchRowNumber(f[BATCH_ROW_NUMBER_FIELD_ID]);
return { row, idx, no };
});
const hasRowNo = rowsWithNo.some(r => typeof r.no === 'number');
const ordered = hasRowNo
? [...rowsWithNo].sort((a, b) => {
const na = typeof a.no === 'number' ? a.no : Infinity;
const nb = typeof b.no === 'number' ? b.no : Infinity;
if (na !== nb) return na - nb;
return a.idx - b.idx;
})
: rowsWithNo;
const allNos = hasRowNo ? ordered.map(r => (typeof r.no === 'number' ? r.no : null)).filter((v): v is number => v !== null) : [];
const minNo = allNos.length > 0 ? Math.min(...allNos) : 1;
const maxNo = allNos.length > 0 ? Math.max(...allNos) : ordered.length;
const requestedStart = range?.start && range.start > 0 ? range.start : (hasRowNo ? minNo : 1);
const requestedEnd = range?.end && range.end > 0 ? range.end : (hasRowNo ? maxNo : ordered.length);
const start = Math.min(requestedStart, requestedEnd);
const end = Math.max(requestedStart, requestedEnd);
const selected = hasRowNo
? ordered.filter(r => typeof r.no === 'number' && r.no >= Math.max(minNo, start) && r.no <= Math.min(maxNo, end))
: ordered.slice(Math.max(0, Math.min(start, ordered.length) - 1), Math.max(0, Math.min(end, ordered.length)));
setBatchProcessingTotal(selected.length);
let processed = 0;
for (let j = 0; j < selected.length; j++) {
if (batchAbortRef.current) {
break;
}
const { row, idx, no } = selected[j];
const displayIndex = typeof no === 'number' ? no : (idx + 1);
const f = row.fields || {};
const getText = (name: string) => extractText(f[nameToId.get(name) || '']);
const foreignId = getText('foreignId');
const styleText = getText('styleText');
const colorText = getText('colorText');
const rawStart = f[nameToId.get('startTimestamp') || ''];
const rawExpected = f[nameToId.get('expectedDateTimestamp') || ''];
const rawBufferDays = f[nameToId.get('缓冲期') || BATCH_BUFFER_FIELD_ID || ''];
let startDate: Date | null = null;
let expectedDateObj: Date | null = null;
if (typeof rawStart === 'number') startDate = new Date(rawStart);
else if (Array.isArray(rawStart) && rawStart.length > 0) {
const item = rawStart[0];
if (typeof item === 'number') startDate = new Date(item);
else startDate = parseDate(extractText(rawStart));
} else if (typeof rawStart === 'string') startDate = parseDate(rawStart);
else if (rawStart && typeof rawStart === 'object') {
const v = (rawStart as any).value || (rawStart as any).text || (rawStart as any).name || '';
if (typeof v === 'number') startDate = new Date(v); else startDate = parseDate(v);
}
if (typeof rawExpected === 'number') expectedDateObj = new Date(rawExpected);
else if (Array.isArray(rawExpected) && rawExpected.length > 0) {
const item = rawExpected[0];
if (typeof item === 'number') expectedDateObj = new Date(item);
else expectedDateObj = parseDate(extractText(rawExpected));
} else if (typeof rawExpected === 'string') expectedDateObj = parseDate(rawExpected);
else if (rawExpected && typeof rawExpected === 'object') {
const v = (rawExpected as any).value || (rawExpected as any).text || (rawExpected as any).name || '';
if (typeof v === 'number') expectedDateObj = new Date(v); else expectedDateObj = parseDate(v);
}
const parseBufferDays = (raw: any): number | null => {
if (raw == null) return null;
if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
if (Array.isArray(raw) && raw.length > 0) {
const item = raw[0];
if (typeof item === 'number' && Number.isFinite(item)) return item;
const text = extractText(item);
const n = Number(text);
return Number.isFinite(n) ? n : null;
}
if (typeof raw === 'string') {
const n = Number(raw);
return Number.isFinite(n) ? n : null;
}
if (raw && typeof raw === 'object') {
const v = (raw as any).value || (raw as any).text || (raw as any).name || '';
const n = Number(v);
return Number.isFinite(n) ? n : null;
}
return null;
};
const normalizeExcludedDates = (raw: any): string[] => {
const out: string[] = [];
const pushToken = (token: any) => {
if (token == null) return;
if (typeof token === 'number' && Number.isFinite(token)) {
const d = new Date(token);
if (!isNaN(d.getTime())) out.push(format(d, 'yyyy-MM-dd'));
return;
}
const text = (typeof token === 'string') ? token : extractText(token);
if (!text) return;
const parts = String(text).split(/[,;;、\n\r\t ]+/).map(s => s.trim()).filter(Boolean);
for (const p of parts) {
const head = p.slice(0, 10);
if (/^\d{4}-\d{2}-\d{2}$/.test(head)) {
out.push(head);
continue;
}
const parsed = parseDate(p);
if (parsed && !isNaN(parsed.getTime())) out.push(format(parsed, 'yyyy-MM-dd'));
}
};
const walk = (v: any) => {
if (Array.isArray(v)) {
v.forEach(walk);
return;
}
if (v && typeof v === 'object' && Array.isArray((v as any).value)) {
walk((v as any).value);
return;
}
pushToken(v);
};
walk(raw);
return Array.from(new Set(out)).filter(Boolean);
};
const batchExcludedDates = normalizeExcludedDates(f['fldQNxtHnd']);
const splitVals = (s: string) => (s || '').split(/[,,、]+/).map(v => v.trim()).filter(Boolean);
const normalizeToStringList = (raw: any): string[] => {
if (!raw) return [];
if (Array.isArray(raw)) {
return raw.flatMap((item: any) => normalizeToStringList(item));
}
if (typeof raw === 'string') return splitVals(raw);
if (typeof raw === 'number') return [String(raw)];
if (raw && typeof raw === 'object') return splitVals(extractText(raw));
return [];
};
const labels: { [key: string]: string | string[] } = {};
for (let i2 = 1; i2 <= 12; i2++) {
const key = `标签${i2}`;
const fieldId =
i2 === 11 ? (nameToId.get(key) || BATCH_LABEL11_FIELD_ID) :
i2 === 12 ? (nameToId.get(key) || BATCH_LABEL12_FIELD_ID) :
(nameToId.get(key) || '');
const raw = f[fieldId];
const list = normalizeToStringList(raw);
if (list.length > 0) {
if (i2 === 7 || i2 === 8 || i2 === 10) labels[key] = list;
else labels[key] = list.join('');
}
}
{
const requiredLabelKeys = Array.from({ length: 12 }, (_, k) => `标签${k + 1}`);
const missing = requiredLabelKeys.filter(k => {
const val = (labels as any)[k];
if (Array.isArray(val)) return val.length === 0;
return !(typeof val === 'string' && val.trim().length > 0);
});
if (missing.length > 0) {
setBatchProcessedCount(p => p + 1);
setBatchFailureCount(fCount => fCount + 1);
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: `标签不完整:${missing.join('、')}` }]);
continue;
}
}
setBatchCurrentRowInfo({ index: displayIndex, foreignId: foreignId || '', style: styleText || '', color: colorText || '' });
setCurrentForeignId(foreignId || '');
setCurrentStyleText(styleText || '');
setCurrentColorText(colorText || '');
setExpectedDate(expectedDateObj || null);
setStartTime(startDate || null);
setSelectedLabels(labels);
try {
const results = await handleCalculateTimeline(true, { selectedLabels: labels, expectedDate: expectedDateObj || null, startTime: startDate || null, excludedDates: batchExcludedDates }, false);
if (results && results.length > 0) {
let effectiveExpectedDate = expectedDateObj || null;
if (!effectiveExpectedDate) {
const bufferDays = parseBufferDays(rawBufferDays);
const fallbackDays = Number.isFinite(bufferDays as number) ? (bufferDays as number) : 14;
const autoExpected = computeExpectedDateByBufferDays(results, fallbackDays, completionDateAdjustment);
if (autoExpected) {
effectiveExpectedDate = autoExpected;
setExpectedDate(autoExpected);
}
}
const processRecordIds = await writeToProcessDataTable(results, { foreignId, style: styleText, color: colorText });
const deliveryRecordId = await writeToDeliveryRecordTable(
results,
processRecordIds,
{},
{
foreignId,
style: styleText,
color: colorText,
expectedDate: effectiveExpectedDate,
startTime: startDate || null,
selectedLabels: labels
}
);
try {
const candidateNames = ['状态','record_id','记录ID','货期记录ID','deliveryRecordId'];
let statusFieldId = '';
for (const nm of candidateNames) { const id = nameToId.get(nm) || ''; if (id) { statusFieldId = id; break; } }
if (!statusFieldId) statusFieldId = 'fldKTpPL9s';
const rowRecordId = (row.id || (row as any).recordId || (row as any)._id || (row as any).record_id);
const deliveryRecordIdStr = typeof deliveryRecordId === 'string'
? deliveryRecordId
: ((deliveryRecordId && ((deliveryRecordId as any).id || (deliveryRecordId as any).recordId)) ? (((deliveryRecordId as any).id || (deliveryRecordId as any).recordId) as string) : '');
if (statusFieldId && rowRecordId && deliveryRecordIdStr) {
const statusField = await batchTable.getField(statusFieldId);
try {
await (statusField as any).setValue(rowRecordId, deliveryRecordIdStr);
} catch (eSet) {
try {
const statusCell = await statusField.createCell(deliveryRecordIdStr);
try {
await (batchTable as any).updateRecord(rowRecordId, [statusCell]);
} catch (e1) {
const fn = (batchTable as any).setRecord || (batchTable as any).updateRecordById;
if (typeof fn === 'function') {
await fn.call(batchTable, rowRecordId, [statusCell]);
} else {
throw e1;
}
}
} catch (e2) {
throw e2;
}
}
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'success', message: `记录ID: ${deliveryRecordIdStr}` }]);
} else {
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: '未找到状态字段或记录ID为空' }]);
}
} catch (statusErr: any) {
console.warn('回写批量状态字段失败', statusErr);
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: `状态写入失败: ${statusErr?.message || '未知错误'}` }]);
}
processed++;
setBatchProcessedCount(p => p + 1);
setBatchSuccessCount(s => s + 1);
} else {
setBatchProcessedCount(p => p + 1);
setBatchFailureCount(fCount => fCount + 1);
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: '时效结果为空' }]);
}
} catch (rowErr: any) {
setBatchProcessedCount(p => p + 1);
setBatchFailureCount(fCount => fCount + 1);
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: rowErr?.message || '处理失败' }]);
}
}
if (bitable.ui.showToast) {
const aborted = batchAbortRef.current;
await bitable.ui.showToast({ toastType: ToastType.success, message: aborted ? `批量已中止,已处理 ${processed} 条记录` : `批量生成完成,共处理 ${processed} 条记录` });
}
} catch (error) {
console.error('批量处理失败:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.error, message: '批量处理失败,请检查数据表配置' });
}
} finally {
setBatchLoading(false);
}
};
// 为批量处理优化的时效计算函数
// 已移除批量时效计算函数 calculateTimelineForBatch
// 执行定价数据查询
const executeQuery = async (packId: string, packType: string) => {
if (queryLoading) return;
setQueryLoading(true);
try {
// 使用 apiService 中的函数
const data = await executePricingQuery(packId, packType, selectedLabels);
setQueryResults(data);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: ToastType.success,
message: '查询成功'
});
}
} catch (error: any) {
console.error('数据库查询出错:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: ToastType.error,
message: `数据库查询失败: ${error.message}`
});
}
} finally {
setQueryLoading(false);
}
};
// 执行二次工艺查询
const executeSecondaryProcessQueryLocal = async (packId: string, packType: string) => {
if (secondaryProcessLoading) return;
setSecondaryProcessLoading(true);
try {
const data = await executeSecondaryProcessQuery(packId, packType);
setSecondaryProcessResults(data);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: ToastType.success,
message: '二次工艺查询成功'
});
}
} catch (error: any) {
console.error('二次工艺查询出错:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: ToastType.error,
message: `二次工艺查询失败: ${error.message}`
});
}
} finally {
setSecondaryProcessLoading(false);
}
};
// 执行定价详情查询
const executePricingDetailsQueryLocal = async (packId: string) => {
if (pricingDetailsLoading) return;
setPricingDetailsLoading(true);
try {
const data = await executePricingDetailsQuery(packId);
setPricingDetailsResults(data);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: ToastType.success,
message: '定价详情查询成功'
});
}
} catch (error: any) {
console.error('定价详情查询出错:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 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: 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: 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: 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) {
if (!fieldMeta || typeof (fieldMeta as any).name !== 'string' || typeof (fieldMeta as any).id !== 'string') {
continue;
}
fieldNameToId[(fieldMeta as any).name] = (fieldMeta as any).id;
}
// 提取标签值的辅助函数
const extractFieldValue = (fieldName: string) => {
const fieldId = fieldNameToId[fieldName];
if (fieldId && firstRecord.fields[fieldId]) {
const fieldValue: any = firstRecord.fields[fieldId] as any;
// 优先处理数组格式(公式字段)
if (Array.isArray(fieldValue) && fieldValue.length > 0) {
const firstItem: any = fieldValue[0] as any;
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: any = firstRecord.fields[fieldId] as any;
// 优先处理数组格式(公式字段)
if (Array.isArray(fieldValue) && fieldValue.length > 0) {
const firstItem: any = fieldValue[0] as any;
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 extractFieldValuesById = (fieldId: string): string[] => {
const v: any = firstRecord.fields[fieldId] as any;
if (!v) return [];
if (Array.isArray(v)) {
return v
.flatMap((item: any) => {
const raw = typeof item === 'string' ? item : (item?.text || item?.name || '');
return raw.split(/[,,、]+/);
})
.map((s: string) => (s || '').trim())
.filter(Boolean) as string[];
}
if (typeof v === 'string') {
return v
.split(/[,,、]+/)
.map((s: string) => s.trim())
.filter(Boolean);
}
if (typeof v === 'object' && v !== null) {
const s = (v.text || v.name || '').trim();
return s
? s.split(/[,,、]+/).map((x: string) => x.trim()).filter(Boolean)
: [];
}
return [];
};
// 设置提取到的标签值
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;
const label8Vals = extractFieldValuesById('fldLeU2Qhq');
if (label8Vals.length > 0) {
const opts = labelOptions['标签8'] || [];
const matched = label8Vals.filter(val => opts.some(o => o.value === val || o.label === val));
if (matched.length > 0) {
newSelectedLabels['标签8'] = matched;
}
}
const label7Vals = extractFieldValuesById('fldt9v2oJM');
if (label7Vals.length > 0) {
const opts = labelOptions['标签7'] || [];
const matched = label7Vals.filter(val => opts.some(o => o.value === val || o.label === val));
if (matched.length > 0) {
newSelectedLabels['标签7'] = matched;
}
}
const label12Vals = extractFieldValuesById('fldWuCeQDS');
if (label12Vals.length > 0) {
const uniqVals = Array.from(new Set(label12Vals.map(v => (v || '').trim()).filter(Boolean)));
const opts = labelOptions['标签12'] || [];
const optSet = new Set<string>();
for (const o of opts) {
const v = typeof o?.value === 'string' ? o.value.trim() : '';
const l = typeof o?.label === 'string' ? o.label.trim() : '';
if (v) optSet.add(v);
if (l) optSet.add(l);
}
const matched = optSet.size > 0 ? uniqVals.filter(v => optSet.has(v)) : uniqVals;
if (matched.length > 0) {
newSelectedLabels['标签12'] = matched;
}
}
// 添加标签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: ToastType.success,
message: `已自动提取 ${Object.keys(newSelectedLabels).length} 个标签值`
});
}
}
} else {
setRecordDetails([]);
}
} catch (error) {
console.error('选择记录时出错:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 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' }}>
<div style={{ width: 280, cursor: 'pointer' }} onClick={() => chooseMode('generate')}>
<Card
style={{
width: '100%',
borderRadius: 12,
boxShadow: '0 8px 24px rgba(0,0,0,0.06)',
border: '1px solid #e5e7eb',
background: 'linear-gradient(180deg, #fff, #f9fbff)'
}}
>
<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>
</div>
<div style={{ width: 280, cursor: 'pointer' }} onClick={() => chooseMode('adjust')}>
<Card
style={{
width: '100%',
borderRadius: 12,
boxShadow: '0 8px 24px rgba(0,0,0,0.06)',
border: '1px solid #e5e7eb',
background: 'linear-gradient(180deg, #fff, #f9fbff)'
}}
>
<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>
</div>
<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)'
}}
>
<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={openBatchModal}></Button>
</div>
</Card>
</div>
</Modal>
<Modal
title="批量生成"
visible={batchModalVisible}
onCancel={async () => { batchAbortRef.current = true; setBatchModalVisible(false); }}
footer={null}
maskClosable={false}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Card style={{ padding: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<Text></Text>
<InputNumber min={1} value={batchStartRow} onChange={(v) => setBatchStartRow(typeof v === 'number' ? v : 1)} style={{ width: 120 }} disabled={batchLoading} />
<Text></Text>
<InputNumber min={1} value={batchEndRow} onChange={(v) => setBatchEndRow(typeof v === 'number' ? v : 1)} style={{ width: 120 }} disabled={batchLoading} />
<Text type='tertiary'>{batchTotalRows}</Text>
<Space>
<Button loading={batchLoading} type='primary' theme='solid' onClick={() => handleBatchProcess({ start: batchStartRow, end: batchEndRow })}></Button>
<Button type='danger' onClick={() => { batchAbortRef.current = true; }} disabled={!batchLoading}></Button>
<Button onClick={() => setBatchModalVisible(false)} disabled={batchLoading}></Button>
</Space>
</div>
</Card>
{(batchLoading || batchProcessedCount > 0) && (
<Card style={{ padding: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<div style={{ flex: 1, paddingRight: 12 }}>
<Progress percent={batchProcessingTotal > 0 ? Math.round((batchProcessedCount / batchProcessingTotal) * 100) : 0} showInfo />
</div>
<div className="progress-indicator">
<div className="progress-spinner" />
<Text>{batchProcessedCount}/{batchProcessingTotal}</Text>
<Text type='tertiary'> {batchSuccessCount} {batchFailureCount}</Text>
</div>
</div>
{batchCurrentRowInfo && (
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', marginBottom: 12 }}>
<Text>{batchCurrentRowInfo.index}</Text>
<Text>{batchCurrentRowInfo.foreignId || '-'}</Text>
<Text>{batchCurrentRowInfo.style || '-'}</Text>
<Text>{batchCurrentRowInfo.color || '-'}</Text>
</div>
)}
<List
dataSource={batchProgressList}
renderItem={item => (
<List.Item style={{ padding: '6px 0' }}>
<Space>
<Text> {item.index}</Text>
<Text> {item.foreignId || '-'}</Text>
<Text type={item.status === 'success' ? 'success' : 'danger'}>{item.status === 'success' ? '成功' : '失败'}</Text>
{item.message && <Text type='tertiary'>{item.message}</Text>}
</Space>
</List.Item>
)}
/>
</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 === '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) => {
const next = v as any;
setMode(next);
setIsExpectedDeliveryDateLocked(false);
setLockedExpectedDeliveryDateTs(null);
setExcludedDatesOverride([]);
setExcludedDatesOverrideText('');
}}
optionList={[
{ value: 'generate', label: '生成流程日期' },
{ value: 'adjust', label: '调整流程日期' }
]} />
{mode === 'adjust' && (
<Space spacing={12} align='center'>
<Button type="primary" onClick={async () => {
try {
// 新增:每次在调整模式读取记录前重置关键状态,避免跨单据串值
// 保留当前模式不变
resetGlobalState({ resetMode: false });
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: ToastType.warning, message: '请先在数据表中选中一条货期记录' });
}
return;
}
if (tableId === DELIVERY_RECORD_TABLE_ID) {
setSelectedDeliveryRecordId(recordId);
await loadProcessDataFromDeliveryRecord(recordId);
} else if (tableId === OMS_BOARD_TABLE_ID) {
const matchedDeliveryRecordId = await getDeliveryRecordIdFromOmsRecord(recordId);
if (!matchedDeliveryRecordId) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: 'OMS看板记录缺少货期记录IDfldjEIP9yC' });
}
return;
}
setSelectedDeliveryRecordId(matchedDeliveryRecordId);
await loadProcessDataFromDeliveryRecord(matchedDeliveryRecordId);
/*
// 原始逻辑从OMS看板用“货期计划 + 计划版本”匹配货期记录(已停用)
const omsTable = await bitable.base.getTable(OMS_BOARD_TABLE_ID);
const omsRecord = await omsTable.getRecordById(recordId);
const planTextRaw = omsRecord?.fields?.[OMS_PLAN_TEXT_FIELD_ID];
const planVersionRaw = omsRecord?.fields?.[OMS_PLAN_VERSION_FIELD_ID];
const planText = extractText(planTextRaw)?.trim();
let planVersion: number | null = null;
if (typeof planVersionRaw === 'number') {
planVersion = planVersionRaw;
} else if (typeof planVersionRaw === 'string') {
const m = planVersionRaw.match(/\d+/);
if (m) planVersion = parseInt(m[0], 10);
} else if (planVersionRaw && typeof planVersionRaw === 'object') {
const v = (planVersionRaw as any).value ?? (planVersionRaw as any).text;
if (typeof v === 'number') planVersion = v;
else if (typeof v === 'string') {
const m = v.match(/\d+/);
if (m) planVersion = parseInt(m[0], 10);
}
}
if (!planText || planVersion === null) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: 'OMS看板记录缺少货期计划或计划版本' });
}
return;
}
const matchedDeliveryRecordId = await findDeliveryRecordIdByPlan(planText, planVersion);
if (!matchedDeliveryRecordId) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: '未能在货期记录表中匹配到对应记录' });
}
return;
}
setSelectedDeliveryRecordId(matchedDeliveryRecordId);
await loadProcessDataFromDeliveryRecord(matchedDeliveryRecordId);
*/
} else {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: '请在货期记录或OMS看板表中选择记录' });
}
return;
}
} catch (e) {
console.error('读取当前选中记录失败:', e);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.error, message: '读取当前选中记录失败' });
}
}
}}></Button>
<Button
onClick={() => {
const base = Array.isArray(timelineResults) ? timelineResults : [];
const groups = Array.from(
new Set(
base
.map(r => (typeof r?.processGroup === 'string' ? r.processGroup : extractText(r?.processGroup)).trim())
.filter(Boolean)
)
);
if (groups.length <= 1) return;
const groupSet = new Set(groups);
const existing = Array.isArray(groupOrderConfig)
? groupOrderConfig.filter(inst => groupSet.has((inst?.groupName || '').trim()))
: [];
const draft = existing.length > 0 ? existing : deriveGroupOrderDraftByProcessOrder(base);
setGroupOrderDraft(draft);
pendingGroupConfigCalcRef.current = null;
setGroupConfigVisible(true);
}}
disabled={
(() => {
const base = Array.isArray(timelineResults) ? timelineResults : [];
const groups = Array.from(
new Set(
base
.map(r => (typeof r?.processGroup === 'string' ? r.processGroup : extractText(r?.processGroup)).trim())
.filter(Boolean)
)
);
return groups.length <= 1;
})()
}
>
/
</Button>
{currentDeliveryRecordId && (
<Space spacing={8} align="center">
<Text type="tertiary"></Text>
<Input
size="small"
style={{ width: 160 }}
value={currentStyleText || ''}
disabled
/>
<Text type="tertiary"></Text>
<Input
size="small"
style={{ width: 160 }}
value={currentColorText || ''}
disabled
/>
<Text type="tertiary">ID</Text>
<Input
size="small"
style={{ width: 260 }}
value={currentDeliveryRecordId}
disabled
/>
<Button size="small" onClick={() => copyToClipboard(currentDeliveryRecordId)}>
</Button>
</Space>
)}
</Space>
)}
{/* 批量处理功能 - 只在批量模式下显示 */}
</Space>
</div>
)}
<Modal
title="倒推盈余分配"
visible={allocationVisible}
maskClosable={false}
onCancel={() => {
setAllocationVisible(false);
setTimelineVisible(true);
}}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space>
<Button
onClick={() => {
const nodes = allocationNodesSnapshot.length > 0 ? allocationNodesSnapshot : timelineResults;
const draft = buildAutoAllocationDraft(nodes, allocationExtraDays, allocationExcluded);
setAllocationDraft(draft);
}}
>
</Button>
<Button
onClick={() => {
setAllocationDraft({});
}}
>
</Button>
</Space>
<Space>
<Button onClick={() => { setAllocationVisible(false); setTimelineVisible(true); }}></Button>
<Button type="primary" onClick={applyAllocationDraft}></Button>
</Space>
</div>
}
width={900}
>
{(() => {
const nodes = allocationNodesSnapshot.length > 0 ? allocationNodesSnapshot : timelineResults;
const sum = Object.values(allocationDraft || {}).reduce((s, v) => s + (Number(v) || 0), 0);
const roundedSum = Math.round(sum * 100) / 100;
const roundedTarget = Math.round((Number(allocationExtraDays) || 0) * 100) / 100;
const remainingRaw = Math.round((roundedTarget - roundedSum) * 100) / 100;
const remaining = Math.max(0, remainingRaw);
const overAllocated = Math.max(0, Math.round((0 - remainingRaw) * 100) / 100);
const statusColor = overAllocated > 0 ? '#dc2626' : (remaining === 0 ? '#16a34a' : '#d97706');
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Card>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 16, alignItems: 'center' }}>
<Text>{expectedDate ? formatDate(expectedDate, 'CHINESE_DATE') : '-'}</Text>
<Text>{startTime ? formatDate(startTime) : '-'}</Text>
<Text>{calculatedRequiredStartTime ? formatDate(calculatedRequiredStartTime) : '-'}</Text>
<Text strong style={{ color: statusColor }}>
{roundedTarget} {remaining}{overAllocated > 0 ? `,已超配 ${overAllocated}` : ''}
</Text>
</div>
<div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
<Text strong></Text>
<Select
style={{ width: 240 }}
value={allocationMode}
onChange={(v) => setAllocationMode(v as 'auto' | 'manual')}
optionList={[
{ value: 'auto', label: '系统自动分配(按时效比例)' },
{ value: 'manual', label: '业务自行分配' },
]}
/>
</div>
</Card>
<Table
pagination={false}
size="small"
columns={[
{
title: '节点',
dataIndex: 'nodeName',
key: 'nodeName',
width: 220,
render: (_: any, row: any) => {
const idx = row.index as number;
const nodeName = row.nodeName as string;
const isTurnoverNode = nodeName === '周转周期';
const excluded = !!allocationExcluded?.[idx] || isTurnoverNode;
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
<span style={{ opacity: excluded ? 0.5 : 1 }}>{nodeName}</span>
<Button
size="small"
type="tertiary"
disabled={isTurnoverNode}
onClick={() => {
const currentlyExcluded = excluded;
setAllocationExcluded(prev => {
const next = { ...(prev || {}) };
if (currentlyExcluded) {
delete next[idx];
} else {
next[idx] = true;
}
return next;
});
setAllocationDraft(prev => {
const next = { ...(prev || {}) };
if (!currentlyExcluded) {
next[idx] = 0;
}
return next;
});
}}
>
{excluded ? '+' : '-'}
</Button>
</span>
);
}
},
{ title: '基准时效(天)', dataIndex: 'base', key: 'base', width: 120, render: (v) => (Number(v) || 0).toFixed(2) },
{
title: allocationMode === 'manual' ? '分配盈余(天)' : '系统分配(天)',
dataIndex: 'allocated',
key: 'allocated',
width: 160,
render: (_: any, row: any) => {
const idx = row.index as number;
const isTurnoverNode = row.nodeName === '周转周期';
const isExcluded = !!allocationExcluded?.[idx];
const val = Number(allocationDraft[idx]) || 0;
if (allocationMode === 'manual') {
const otherSum = Math.round((roundedSum - val) * 100) / 100;
const maxAllowed = Math.max(0, Math.round((roundedTarget - otherSum) * 100) / 100);
const maxForControl = maxAllowed < val ? val : maxAllowed;
return (
<InputNumber
min={0}
max={maxForControl}
step={0.1}
value={val}
disabled={isTurnoverNode || isExcluded}
onChange={(v) => {
const raw = Math.round((Number(v) || 0) * 100) / 100;
const n = Math.max(0, Math.min(raw, maxAllowed));
setAllocationDraft(prev => ({ ...(prev || {}), [idx]: n }));
}}
style={{ width: 140 }}
/>
);
}
return <span>{val.toFixed(2)}</span>;
}
}
]}
dataSource={nodes.map((n, index) => {
const base = typeof n?.timelineValue === 'number'
? n.timelineValue
: (typeof n?.adjustedTimelineValue === 'number' ? n.adjustedTimelineValue : 0);
return {
key: index,
index,
nodeName: n?.nodeName || `节点${index + 1}`,
base: Number.isFinite(base) ? base : 0,
allocated: Number(allocationDraft[index]) || 0,
};
})}
/>
</div>
);
})()}
</Modal>
<Modal
title="流程组执行顺序"
visible={groupConfigVisible}
maskClosable={false}
onCancel={() => {
setGroupConfigVisible(false);
setGroupOrderDraft([]);
}}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text type="tertiary">
</Text>
<Space>
<Button
onClick={() => {
setGroupConfigVisible(false);
setGroupOrderDraft([]);
pendingGroupConfigCalcRef.current = null;
}}
>
</Button>
<Button
type="primary"
disabled={!Array.isArray(groupOrderDraft) || groupOrderDraft.length === 0}
onClick={() => {
const nextConfig = Array.isArray(groupOrderDraft) ? groupOrderDraft : [];
setGroupOrderConfig(nextConfig);
skipNextGroupConfigPopupRef.current = true;
setGroupConfigVisible(false);
if (!pendingGroupConfigCalcRef.current && mode === 'adjust') {
const next = applyGroupOrderConfigToTimelineResults(timelineResults, nextConfig);
pendingRecalculateAfterCalculateAdjustmentsRef.current = timelineAdjustments;
pendingRecalculateAfterCalculateRef.current = true;
setTimelineResults(next);
setTimelineVisible(true);
}
}}
>
</Button>
</Space>
</div>
}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{groupOrderDraft.length === 0 ? (
<Text type="tertiary"></Text>
) : (
<div>
{groupOrderDraft.map((inst, index) => (
<div
key={inst.id}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '8px 12px',
marginBottom: 8,
borderRadius: 8,
border: '1px solid #e5e7eb',
background: '#f9fafb',
cursor: 'move',
opacity: draggingGroupIndex === index ? 0.6 : 1,
}}
draggable
onDragStart={() => setDraggingGroupIndex(index)}
onDragOver={(e) => {
e.preventDefault();
if (draggingGroupIndex === null || draggingGroupIndex === index) return;
setGroupOrderDraft(prev => {
const next = [...prev];
const [removed] = next.splice(draggingGroupIndex, 1);
next.splice(index, 0, removed);
return next;
});
setDraggingGroupIndex(index);
}}
onDragEnd={() => setDraggingGroupIndex(null)}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Tag color="blue" style={{ minWidth: 32, textAlign: 'center' }}>
{index + 1}
</Tag>
<Text>{inst.displayName}</Text>
<Button
size="small"
type="tertiary"
onClick={() => {
setGroupOrderDraft(prev => {
const baseName = (inst.groupName || '').trim();
if (!baseName) return prev;
const currentCount = prev.filter(x => (x.groupName || '').trim() === baseName).length;
const nextIndex = currentCount + 1;
const displayName = nextIndex <= 1 ? baseName : `${baseName}${nextIndex}`;
const duplicated = createProcessGroupInstance(baseName, displayName);
const next = [...prev];
next.splice(index + 1, 0, duplicated);
return next;
});
}}
>
</Button>
</div>
<span style={{ cursor: 'grab', color: '#9ca3af', fontSize: 18 }}></span>
</div>
))}
</div>
)}
</div>
</Modal>
<Modal
title="跳过日期调整(按节点)"
visible={excludedDatesAdjustVisible}
maskClosable={false}
onCancel={closeExcludedDatesAdjustModal}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text type="tertiary">
{Array.isArray(timelineResults) ? timelineResults.length : 0}
{Object.values(excludedDatesByNodeDraft || {}).filter(v => Array.isArray(v) && v.length > 0).length}
</Text>
<Space>
<Button onClick={closeExcludedDatesAdjustModal}></Button>
<Button
type="primary"
onClick={() => {
const next = normalizeExcludedDatesByNodeMap(excludedDatesByNodeDraft || {});
excludedDatesByNodeOverrideRef.current = next;
setExcludedDatesByNodeOverride(next);
pendingRecalculateAfterExcludedDatesRef.current = true;
setTimelineResults(prev => {
const base = Array.isArray(prev) ? prev : [];
return base.map((r: any, idx: number) => {
const nodeKey = buildExcludedDatesNodeKey(
r?.nodeName,
r?.processOrder,
idx,
r?.processGroupInstanceId || r?.processGroupInstanceName
);
if (!Object.prototype.hasOwnProperty.call(next, nodeKey)) return r;
const list = Array.isArray(next[nodeKey]) ? next[nodeKey] : [];
return { ...r, excludedDates: list };
});
});
setExcludedDatesAdjustVisible(false);
}}
>
</Button>
</Space>
</div>
}
style={{ width: 980 }}
bodyStyle={{ maxHeight: '70vh', overflowY: 'auto' }}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{(Array.isArray(timelineResults) ? timelineResults : []).map((r: any, idx: number) => {
const nodeKey = buildExcludedDatesNodeKey(
r?.nodeName,
r?.processOrder,
idx,
r?.processGroupInstanceId || r?.processGroupInstanceName
);
const dates = Array.isArray(excludedDatesByNodeDraft?.[nodeKey]) ? excludedDatesByNodeDraft[nodeKey] : [];
const groups = groupDatesByMonth(dates);
const pickerValue = excludedDatesAddDraft?.[nodeKey] ?? null;
return (
<Card
key={nodeKey}
className="card-enhanced"
bodyStyle={{ padding: 12 }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
<Text strong style={{ fontSize: 14 }}>
{typeof r?.processOrder !== 'undefined' ? `#${r.processOrder} ` : ''}{r?.nodeName || nodeKey}{r?.processGroupInstanceName ? `${r.processGroupInstanceName}` : ''}
</Text>
<Space>
<DatePicker
style={{ width: 160 }}
value={pickerValue ?? undefined}
placeholder="选择日期"
format="yyyy-MM-dd"
onChange={(v) => {
let d: Date | null = null;
if (v instanceof Date) d = v;
else if (typeof v === 'string') {
const parsed = parseDate(v);
d = parsed && !isNaN(parsed.getTime()) ? parsed : null;
}
setExcludedDatesAddDraft(prev => ({ ...(prev || {}), [nodeKey]: d }));
}}
/>
<Button
onClick={() => {
const d = excludedDatesAddDraft?.[nodeKey] ?? null;
addExcludedDateToDraft(nodeKey, d);
setExcludedDatesAddDraft(prev => ({ ...(prev || {}), [nodeKey]: null }));
}}
>
</Button>
<Button type="tertiary" onClick={() => clearExcludedDatesDraftForNode(nodeKey)}></Button>
</Space>
</div>
<div style={{ marginTop: 10 }}>
{groups.length === 0 ? (
<Text type="tertiary"></Text>
) : (
groups.map(g => (
<div key={g.month} style={{ marginBottom: 10 }}>
<Text style={{ display: 'block', marginBottom: 6, color: '#595959' }}>{g.month}</Text>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{g.dates.map(d => (
<Tag
key={d}
closable
onClose={() => removeExcludedDateFromDraft(nodeKey, d)}
style={{ backgroundColor: '#fff7e6', borderColor: '#ffd591', color: '#ad4e00' }}
>
{d}
</Tag>
))}
</div>
</div>
))
)}
</div>
</Card>
);
})}
</div>
</Modal>
{/* 时效计算结果模态框 */}
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<span style={{ fontSize: 16, fontWeight: 600 }}></span>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, whiteSpace: 'nowrap', overflowX: 'auto', maxWidth: '70%' }}>
<Text type="tertiary"></Text>
<Input
size="small"
style={{ width: 160 }}
placeholder="请输入款号"
value={currentStyleText}
disabled={!styleColorEditable}
onChange={(val) => setCurrentStyleText(val)}
/>
<Text type="tertiary" style={{ marginLeft: 8 }}></Text>
<Input
size="small"
style={{ width: 160 }}
placeholder="请输入颜色"
value={currentColorText}
disabled={!styleColorEditable}
onChange={(val) => setCurrentColorText(val)}
/>
<Button
size="small"
type="tertiary"
onClick={() => setStyleColorEditable(prev => !prev)}
style={{ marginLeft: 8 }}
>
<span style={{ fontSize: 14, lineHeight: 1 }}></span>
</Button>
</div>
</div>
}
visible={timelineVisible}
onCancel={() => {
setTimelineVisible(false);
setTimelineAdjustments({}); // 关闭时重置调整
setDeliveryMarginDeductions(0); // 关闭时重置交期余量扣减
setCompletionDateAdjustment(0); // 关闭时重置最后流程完成日期调整
setStyleColorEditable(false); // 关闭弹窗后恢复为锁定状态
}}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space align="center">
<Text>{isExpectedDeliveryDateLocked ? '缓冲期(天)(不影响已锁交付)' : '缓冲期(天)'}</Text>
{(() => {
const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments, expectedDate, timelineResults, completionDateAdjustment);
const canEdit = !isExpectedDeliveryDateLocked && !!getAdjustedLastCompletionDate(timelineResults, completionDateAdjustment);
return (
<InputNumber
value={dynamicBufferDays}
disabled={!canEdit}
step={1}
onChange={(val) => {
if (!canEdit) return;
const n = Number(val);
if (!Number.isFinite(n)) return;
const adjustedCompletion = getAdjustedLastCompletionDate(timelineResults, completionDateAdjustment);
if (!adjustedCompletion) return;
const nextExpected = new Date(adjustedCompletion);
nextExpected.setDate(nextExpected.getDate() + n);
setExpectedDate(nextExpected);
}}
style={{
width: 90,
backgroundColor: dynamicBufferDays < 0 ? '#fff1f0' : undefined,
borderColor: dynamicBufferDays < 0 ? '#ff4d4f' : undefined,
color: dynamicBufferDays < 0 ? '#cf1322' : undefined
}}
/>
);
})()}
<Button onClick={resetToInitialState}>
</Button>
</Space>
<Button
type="primary"
onClick={async () => {
try {
await saveTimelineData();
} catch (error) {
// 错误已在saveTimelineData中处理
}
setTimelineVisible(false);
}}
>
</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 ?? undefined}
onChange={(date) => {
if (date instanceof Date) {
setStartTime(date);
} else if (typeof date === 'string') {
const parsed = new Date(date);
setStartTime(isNaN(parsed.getTime()) ? null : parsed);
} else {
setStartTime(null);
}
}}
type="dateTime"
format="yyyy-MM-dd HH:mm"
/>
</div>
{timelineResults.map((result, index) => {
const nodeKey = buildTimelineAdjustmentKey(result, index);
const adjustment = timelineAdjustments[nodeKey] || 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 k = buildTimelineAdjustmentKey(r, i);
const adj = timelineAdjustments[k] || 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 dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments, expectedDate, timelineResults, completionDateAdjustment);
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.processGroupInstanceName ? `${result.processGroupInstanceName}` : ''}
{result.isAccumulated && result.allMatchedRecords ? (
<span style={{
marginLeft: '8px',
fontSize: '12px',
color: '#000000',
fontWeight: 'normal'
}}>
( {result.allMatchedRecords.length} : {result.allMatchedRecords.map((record: any) => 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"
placeholder="选择日期时间"
style={{ width: '200px' }}
value={(actualCompletionDates[index] as Date | null) ?? undefined}
onChange={(date) => {
// 自动填充默认时分:仅在用户未指定具体时分时套用预计完成的时分
let nextDate: Date | null;
if (date instanceof Date) {
nextDate = new Date(date);
} else if (typeof date === 'string') {
const parsed = new Date(date);
nextDate = isNaN(parsed.getTime()) ? null : parsed;
} else {
nextDate = null;
}
try {
if (nextDate) {
const expectedEndStr = timelineResults?.[index]?.estimatedEnd;
if (expectedEndStr && typeof expectedEndStr === 'string' && !expectedEndStr.includes('未找到')) {
const expectedEnd = parseDate(expectedEndStr);
if (expectedEnd && !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 && nextDate) {
const startDate = parseDate(startStr);
if (startDate && !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(nextDate);
// 如果用户只调整了时分(同一天),不触发工作日反推,避免日期意外跳变
const expectedEndStrForAdjust = timelineResults?.[index]?.estimatedEnd;
const expectedEndForAdjust = expectedEndStrForAdjust ? parseDate(expectedEndStrForAdjust) : null;
if (expectedEndForAdjust && !isNaN(expectedEndForAdjust.getTime())) {
const sameDay = expectedEndForAdjust.getFullYear() === targetDate.getFullYear()
&& expectedEndForAdjust.getMonth() === targetDate.getMonth()
&& expectedEndForAdjust.getDate() === targetDate.getDate();
if (sameDay) {
// 仅时间微调:保持当前工作日调整量不变,并让预计完成对齐到实际完成时分
try {
const newEndStr = formatDate(targetDate);
const newSkippedWeekends = calculateSkippedWeekends(adjustedStart, targetDate, weekendDays);
const newActualDays = calculateActualDays(formatDate(adjustedStart), newEndStr);
setTimelineResults(prev => {
const updated = [...prev];
const prevItem = updated[index];
if (prevItem) {
updated[index] = {
...prevItem,
estimatedEnd: newEndStr,
skippedWeekends: newSkippedWeekends,
actualDays: newActualDays
};
}
return updated;
});
} catch {}
return;
}
}
// 使用二分搜索反推工作日数按0.5天粒度),使得正向计算的结束时间尽量贴近目标日期
const dayMs = 1000 * 60 * 60 * 24;
const approxNatural = (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 = approxNatural - 50;
let hi = 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[nodeKey] || 0;
const deltaToApply = desiredAdjustmentAbs - currentAdj;
if (deltaToApply !== 0) {
const updated = handleTimelineAdjustment(nodeKey, index, deltaToApply);
// 若该节点不允许调整交由实际完成日期的useEffect联动重算
if (!updated) {
return;
}
const deficit = computeBufferDeficitDaysUsingEndDelta(updated);
const prevDeficit = lastBufferDeficitRef.current;
lastBufferDeficitRef.current = deficit;
if (deficit > 0 && Math.ceil(deficit) > Math.ceil(prevDeficit)) {
Modal.warning({
title: '缓冲期不足',
content: `当前缓冲期无法覆盖本次超期,缺口 ${Math.ceil(deficit)}`,
});
}
}
}
}
} catch (e) {
console.warn('自动调整量计算失败:', e);
}
// 重算由依赖actualCompletionDates的useEffect触发避免使用旧状态
}}
/>
<Button
size="small"
disabled={isTurnoverNode}
onClick={() => {
try {
const baseTimelineValue = (typeof result.timelineValue === 'number')
? result.timelineValue
: (typeof result.adjustedTimelineValue === 'number' ? result.adjustedTimelineValue : 0);
const currentAdj = timelineAdjustments[nodeKey] || 0;
const targetAdj = -baseTimelineValue;
const deltaToApply = targetAdj - currentAdj;
if (deltaToApply !== 0) {
handleTimelineAdjustment(nodeKey, index, deltaToApply);
}
setActualCompletionDates(prev => {
const next = { ...(prev || {}) };
next[index] = null;
return next;
});
} catch (e) {
console.warn('节点置零失败:', e);
}
}}
style={{ height: '24px', padding: '0 8px' }}
title={isTurnoverNode ? '周转周期节点不支持置零' : '将该节点时效置为0天'}
>
</Button>
<Button
size="small"
disabled={isTurnoverNode || (timelineAdjustments[nodeKey] || 0) === 0}
onClick={() => {
try {
const currentAdj = timelineAdjustments[nodeKey] || 0;
if (currentAdj !== 0) {
handleTimelineAdjustment(nodeKey, index, -currentAdj);
}
} catch (e) {
console.warn('恢复节点失败:', e);
}
}}
style={{ height: '24px', padding: '0 8px' }}
title={isTurnoverNode ? '周转周期节点不支持恢复' : '恢复该节点的时效调整为0'}
>
</Button>
</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' }}>
<Button
size="small"
onClick={() => handleComplexAdjustment(nodeKey, index, -0.5)}
disabled={adjustedValue <= 0 || isCurrentTurnoverZero}
style={{ minWidth: '28px', height: '24px', fontSize: '13px' }}
title={isCurrentTurnoverZero ? '周转周期为零,无法调整' : ''}
>
-0.5
</Button>
<Button
size="small"
onClick={() => handleComplexAdjustment(nodeKey, 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>
<Button
size="small"
onClick={() => handleComplexAdjustment(nodeKey, index, 1)}
disabled={isCurrentTurnoverZero || (hasTurnoverNodeWithZero && !isTurnoverNode) || (isTurnoverNode && !isCurrentTurnoverZero)}
style={{ minWidth: '24px', height: '24px', fontSize: '13px' }}
title={
isCurrentTurnoverZero ? '周转周期为零,无法调整' :
(hasTurnoverNodeWithZero && !isTurnoverNode) ? '周转周期为零,其他节点无法增加' :
(isTurnoverNode && !isCurrentTurnoverZero) ? '周转周期节点会自动调整,无法手动修改' :
''
}
>
+1
</Button>
<Button
size="small"
onClick={() => handleComplexAdjustment(nodeKey, index, 0.5)}
disabled={isCurrentTurnoverZero || (hasTurnoverNodeWithZero && !isTurnoverNode) || (isTurnoverNode && !isCurrentTurnoverZero)}
style={{ minWidth: '28px', height: '24px', fontSize: '13px' }}
title={
isCurrentTurnoverZero ? '周转周期为零,无法调整' :
(hasTurnoverNodeWithZero && !isTurnoverNode) ? '周转周期为零,其他节点无法增加' :
(isTurnoverNode && !isCurrentTurnoverZero) ? '周转周期节点会自动调整,无法手动修改' :
''
}
>
+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: number) => ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][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>
{/* 汇总两行1最后完成+缓冲期=结束日期 2客户期望日期可更改 */}
<div style={{
marginBottom: '12px',
padding: '8px 12px',
backgroundColor: 'rgba(255,255,255,0.85)',
borderRadius: '8px',
border: '1px solid rgba(24, 144, 255, 0.2)'
}}>
{(() => {
// 计算有效的最后流程完成日期
let effectiveLastProcess = null as any;
let lastCompletionDate = null as Date | null;
for (let i = timelineResults.length - 1; i >= 0; i--) {
const process = timelineResults[i];
const processDate = typeof process.estimatedEnd === 'string'
? parseDate(process.estimatedEnd)
: (process.estimatedEnd as any as Date);
if (processDate && !isNaN(processDate.getTime()) && process.estimatedEnd !== '时效值为0') {
effectiveLastProcess = process;
lastCompletionDate = processDate;
break;
}
}
if (!effectiveLastProcess) {
effectiveLastProcess = timelineResults[timelineResults.length - 1];
const fallback = typeof effectiveLastProcess.estimatedEnd === 'string'
? parseDate(effectiveLastProcess.estimatedEnd)
: (effectiveLastProcess.estimatedEnd as any as Date);
lastCompletionDate = fallback || new Date();
}
// 缓冲期(动态)与调整后的最后完成日期
const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments, expectedDate, timelineResults, completionDateAdjustment);
const adjustedCompletionDate = new Date(lastCompletionDate!);
adjustedCompletionDate.setDate(adjustedCompletionDate.getDate() + completionDateAdjustment);
const deliveryDate = new Date(adjustedCompletionDate);
deliveryDate.setDate(deliveryDate.getDate() + dynamicBufferDays);
const lockedDeliveryDate = (isExpectedDeliveryDateLocked && lockedExpectedDeliveryDateTs !== null)
? new Date(lockedExpectedDeliveryDateTs)
: null;
return (
<>
{/* 第一行:最后流程完成日期 + 缓冲期 = 结束日期(优化为紧凑分段展示) */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
gap: 6,
flexWrap: 'nowrap',
whiteSpace: 'nowrap',
overflowX: 'auto'
}}>
<span style={{ fontSize: 13, color: '#666', fontWeight: 600 }}></span>
<span style={{
fontSize: 13,
color: '#333',
backgroundColor: 'rgba(0,0,0,0.04)',
border: '1px solid rgba(0,0,0,0.08)',
borderRadius: 6,
padding: '4px 8px'
}}>
{formatDate(adjustedCompletionDate)}{getDayOfWeek(adjustedCompletionDate)}
</span>
<span style={{ fontSize: 16, color: '#1890ff', fontWeight: 700 }}>+</span>
<span style={{ fontSize: 13, color: '#666', fontWeight: 600 }}></span>
<span style={{
fontSize: 13,
color: dynamicBufferDays < 0 ? '#cf1322' : '#1890ff',
backgroundColor: dynamicBufferDays < 0 ? '#fff1f0' : 'rgba(24, 144, 255, 0.08)',
border: dynamicBufferDays < 0 ? '1px solid #ff4d4f' : '1px solid rgba(24, 144, 255, 0.2)',
borderRadius: 16,
padding: '3px 8px',
fontWeight: 700,
whiteSpace: 'nowrap'
}}>
{dynamicBufferDays}
</span>
<span style={{ fontSize: 16, color: '#1890ff', fontWeight: 700 }}>=</span>
<span style={{ fontSize: 13, color: '#666', fontWeight: 600 }}>
{lockedDeliveryDate ? '结束日期(未锁)' : '结束日期'}
</span>
<span style={{
fontSize: 13,
color: '#52c41a',
backgroundColor: 'rgba(82, 196, 26, 0.08)',
border: '1px solid rgba(82, 196, 26, 0.24)',
borderRadius: 6,
padding: '3px 8px',
fontWeight: 700
}}>
{formatDate(deliveryDate)}{getDayOfWeek(deliveryDate)}
</span>
</div>
{lockedDeliveryDate && (
<div style={{ marginTop: 8, display: 'flex', alignItems: 'center', justifyContent: 'flex-start', gap: 8, flexWrap: 'nowrap', whiteSpace: 'nowrap', overflowX: 'auto' }}>
<Text style={{ fontSize: 13, color: '#333', fontWeight: 600, whiteSpace: 'nowrap' }}></Text>
<span style={{
fontSize: 13,
color: '#722ed1',
backgroundColor: 'rgba(114, 46, 209, 0.08)',
border: '1px solid rgba(114, 46, 209, 0.24)',
borderRadius: 6,
padding: '3px 8px',
fontWeight: 700,
whiteSpace: 'nowrap'
}}>
{formatDate(lockedDeliveryDate)}{getDayOfWeek(lockedDeliveryDate)}
</span>
</div>
)}
{/* 第二行:客户期望日期(可更改,优化展示为标签样式) */}
<div style={{ marginTop: 8, display: 'flex', alignItems: 'center', justifyContent: 'flex-start', gap: 8, flexWrap: 'nowrap', whiteSpace: 'nowrap', overflowX: 'auto' }}>
<Text style={{ fontSize: 13, color: '#333', fontWeight: 600, whiteSpace: 'nowrap' }}></Text>
<DatePicker
style={{ width: '200px' }}
placeholder="请选择客户期望日期"
value={expectedDate ?? undefined}
disabled={isExpectedDeliveryDateLocked}
onChange={(date) => {
if (date instanceof Date) {
setExpectedDate(date);
} else if (typeof date === 'string') {
const parsed = new Date(date);
setExpectedDate(isNaN(parsed.getTime()) ? null : parsed);
} else {
setExpectedDate(null);
}
}}
format="yyyy-MM-dd"
disabledDate={(date) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
if (!(date instanceof Date)) return false;
return date < today;
}}
/>
{expectedDate && (
<span style={{
fontSize: 13,
color: '#722ed1',
backgroundColor: 'rgba(114, 46, 209, 0.08)',
border: '1px solid rgba(114, 46, 209, 0.24)',
borderRadius: 6,
padding: '4px 8px',
fontWeight: 600,
whiteSpace: 'nowrap'
}}>
{formatDate(expectedDate)}{getDayOfWeek(expectedDate)}
</span>
)}
</div>
</>
);
})()}
</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>
<div style={{ marginTop: '8px', padding: '8px', backgroundColor: '#fffbe6', borderRadius: '4px' }}>
<Space align="center" spacing={8} style={{ width: '100%', justifyContent: 'space-between' }}>
<Text strong style={{ color: '#fa8c16' }}></Text>
<Space align="center" spacing={8}>
<Text type="tertiary">
{Object.values(excludedDatesByNodeOverride || {}).filter(v => Array.isArray(v) && v.length > 0).length}
</Text>
<Button size="small" onClick={openExcludedDatesAdjustModal}></Button>
</Space>
</Space>
</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' }}>
{(() => {
const nodeNameByKey = new Map<string, string>();
for (let i = 0; i < timelineResults.length; i++) {
const r = timelineResults[i];
const k = buildTimelineAdjustmentKey(r, i);
if (!nodeNameByKey.has(k)) nodeNameByKey.set(k, r?.nodeName || k);
}
return Object.entries(timelineAdjustments).map(([nodeKey, adjustment]) => {
const nodeName = nodeNameByKey.get(nodeKey) || nodeKey;
return (
<Text key={nodeKey} style={{ display: 'block', fontSize: '12px' }}>
{nodeName}: {adjustment > 0 ? '+' : ''}{adjustment.toFixed(1)}
</Text>
);
});
})()}
</div>
</div>
)}
</div>
</div>
)}
</div>
</Modal>
{/* 标签选择部分,生成/调整模式均可使用 */}
{(mode === 'generate' || labelAdjustmentFlow) && 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: 12 }, (_, i) => i + 1).map(num => {
const labelKey = `标签${num}`;
const options = labelOptions[labelKey] || [];
const isMultiSelect = labelKey === '标签7' || labelKey === '标签8' || labelKey === '标签10' || labelKey === '标签12';
const val = selectedLabels[labelKey];
const isMissing = Array.isArray(val) ? (val as string[]).length === 0 : !(typeof val === 'string' && (val as string).trim().length > 0);
const label7Val = selectedLabels['标签7'];
const label7Values = Array.isArray(label7Val)
? (label7Val as any[]).map(s => String(s ?? '').trim()).filter(Boolean)
: (typeof label7Val === 'string' ? [label7Val.trim()].filter(Boolean) : []);
const shouldRequireLabel12 = label7Values.length > 0 && !(label7Values.length === 1 && label7Values[0] === '无(二次工艺)');
const isRequired = labelKey !== '标签11' && (labelKey === '标签12' ? shouldRequireLabel12 : true);
return (
<div key={labelKey}>
<Text strong style={{ display: 'block', marginBottom: '8px' }}>{labelKey}{isRequired ? '必填' : '非必填'}</Text>
<Select
className="select-enhanced"
style={{ width: '100%', borderColor: isRequired && isMissing ? '#ff4d4f' : undefined }}
placeholder={`请选择${labelKey}${isRequired ? '(必填)' : ''}`}
value={selectedLabels[labelKey]}
onChange={(value) => {
const normalizeOne = (v: any) => {
if (typeof v === 'string') return v;
if (typeof v === 'number') return String(v);
if (v && typeof v === 'object') {
if (typeof v.value === 'string') return v.value;
if (typeof v.value === 'number') return String(v.value);
if (typeof v.text === 'string') return v.text;
if (typeof v.label === 'string') return v.label;
}
return '';
};
if (isMultiSelect) {
const arr = Array.isArray(value) ? value : (value == null ? [] : [value]);
const out = arr.map(normalizeOne).map(s => s.trim()).filter(Boolean);
handleLabelChange(labelKey, out);
return;
}
if (Array.isArray(value)) {
const first = value.length > 0 ? normalizeOne(value[0]).trim() : '';
handleLabelChange(labelKey, first);
return;
}
const single = normalizeOne(value).trim();
handleLabelChange(labelKey, single);
}}
multiple={isMultiSelect}
filter
>
{options.map((option, optionIndex) => (
<Select.Option key={optionIndex} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
{labelKey === '标签7' && (
<Text type="tertiary" style={{ marginTop: '6px', display: 'block' }}>
</Text>
)}
{labelKey === '标签12' && (
<Text type="tertiary" style={{ marginTop: '6px', display: 'block' }}>
</Text>
)}
{isRequired && isMissing && (
<Text type="danger" style={{ marginTop: '6px', display: 'block' }}></Text>
)}
</div>
);
})}
</div>
<div style={{ marginTop: '24px' }}>
<Text strong style={{ display: 'block', marginBottom: '8px' }}></Text>
<Select
style={{ width: '300px' }}
value={timelineDirection}
onChange={(value) => setTimelineDirection(value as 'forward' | 'backward')}
>
<Select.Option value="forward"></Select.Option>
<Select.Option value="backward"></Select.Option>
</Select>
</div>
<div style={{ marginTop: '24px' }}>
<Text strong style={{ display: 'block', marginBottom: '8px' }}></Text>
<DatePicker
className="input-enhanced"
style={{ width: '300px' }}
placeholder="请选择起始日期"
value={startTime ?? undefined}
onChange={(date) => {
if (date instanceof Date) {
setStartTime(date);
} else if (typeof date === 'string') {
const parsed = new Date(date);
setStartTime(isNaN(parsed.getTime()) ? null : parsed);
} else {
setStartTime(null);
}
}}
type="dateTime"
format="yyyy-MM-dd HH:mm"
/>
{timelineDirection === 'backward' && (
<Text type="secondary" style={{ marginLeft: '12px' }}>
</Text>
)}
</div>
{/* 客户期望日期选择 */}
<div style={{ marginTop: '24px' }}>
<Text strong style={{ display: 'block', marginBottom: '8px' }}></Text>
<DatePicker
className="input-enhanced"
style={{ width: '300px' }}
placeholder="请选择客户期望日期"
value={expectedDate ?? undefined}
onChange={(date) => {
if (date instanceof Date) {
setExpectedDate(date);
} else if (typeof date === 'string') {
const parsed = new Date(date);
setExpectedDate(isNaN(parsed.getTime()) ? null : parsed);
} else {
setExpectedDate(null);
}
}}
format="yyyy-MM-dd"
disabledDate={(date) => {
// 禁用今天之前的日期
const today = new Date();
today.setHours(0, 0, 0, 0);
if (!(date instanceof Date)) return false;
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 vertical spacing="loose" style={{ width: '100%' }}>
<Button
type='primary'
className="btn-gradient-calculate"
size="large"
onClick={() => labelAdjustmentFlow
? handleCalculateTimeline(true, { selectedLabels, expectedDate, startTime }, true)
: handleCalculateTimeline()}
loading={timelineLoading}
disabled={
timelineLoading
|| (timelineDirection === 'backward' && (!expectedDate || !startTime))
|| (() => {
const required = Array.from({ length: 11 }, (_, i) => `标签${i + 1}`);
const label7Val = (selectedLabels as any)['标签7'];
const label7Values = Array.isArray(label7Val)
? label7Val.map((s: any) => String(s ?? '').trim()).filter(Boolean)
: (typeof label7Val === 'string' ? [label7Val.trim()].filter(Boolean) : []);
const shouldRequireLabel12 = label7Values.length > 0 && !(label7Values.length === 1 && label7Values[0] === '无(二次工艺)');
if (shouldRequireLabel12) required.push('标签12');
return required.some(key => {
const val = (selectedLabels as any)[key];
if (Array.isArray(val)) return val.length === 0;
return !(typeof val === 'string' && val.trim().length > 0);
});
})()
}
style={{ minWidth: '160px' }}
>
{labelAdjustmentFlow ? '重新生成计划' : '计算预计时间'}
</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 === '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>
{/* 批量处理配置已移除 */}
</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>
);
}