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(); 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 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(); for (const inst of order) { const bn = (inst?.groupName || '').trim(); if (bn) configuredBaseNames.add(bn); } const baseNameToNodes = new Map(); 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([]); const [recordDetails, setRecordDetails] = useState([]); const [loading, setLoading] = useState(false); const [queryResults, setQueryResults] = useState([]); const [queryLoading, setQueryLoading] = useState(false); const [secondaryProcessResults, setSecondaryProcessResults] = useState([]); const [secondaryProcessLoading, setSecondaryProcessLoading] = useState(false); const [pricingDetailsResults, setPricingDetailsResults] = useState([]); const [pricingDetailsLoading, setPricingDetailsLoading] = useState(false); // 标签相关状态 const [labelOptions, setLabelOptions] = useState<{[key: string]: any[]}>({}); const [labelAdjustmentFlow, setLabelAdjustmentFlow] = useState(false); const [selectedLabels, setSelectedLabels] = useState<{[key: string]: string | string[]}>({}); const [labelLoading, setLabelLoading] = useState(false); const [excludedDatesOverride, setExcludedDatesOverride] = useState([]); const [excludedDatesOverrideText, setExcludedDatesOverrideText] = useState(''); const [excludedDatesByNodeOverride, setExcludedDatesByNodeOverride] = useState>({}); const excludedDatesByNodeOverrideRef = useRef>({}); const pendingRecalculateAfterExcludedDatesRef = useRef(false); const pendingRecalculateAfterCalculateRef = useRef(false); const pendingRecalculateAfterCalculateAdjustmentsRef = useRef | null>(null); const [excludedDatesAdjustVisible, setExcludedDatesAdjustVisible] = useState(false); const [excludedDatesByNodeDraft, setExcludedDatesByNodeDraft] = useState>({}); const [excludedDatesAddDraft, setExcludedDatesAddDraft] = useState>({}); 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 [expectedDate, setExpectedDate] = useState(null); // 起始时间状态(从货期记录表获取,新记录则使用当前时间) const [startTime, setStartTime] = useState(null); const [timelineDirection, setTimelineDirection] = useState<'forward' | 'backward'>('forward'); const [calculatedRequiredStartTime, setCalculatedRequiredStartTime] = useState(null); const [allocationVisible, setAllocationVisible] = useState(false); const [allocationMode, setAllocationMode] = useState<'auto' | 'manual'>('auto'); const [allocationExtraDays, setAllocationExtraDays] = useState(0); const [allocationDraft, setAllocationDraft] = useState<{[key: number]: number}>({}); const [allocationNodesSnapshot, setAllocationNodesSnapshot] = useState([]); const [allocationExcluded, setAllocationExcluded] = useState<{[key: number]: boolean}>({}); const [groupOrderConfig, setGroupOrderConfig] = useState([]); const [groupConfigVisible, setGroupConfigVisible] = useState(false); const [groupOrderDraft, setGroupOrderDraft] = useState([]); const [draggingGroupIndex, setDraggingGroupIndex] = useState(null); // 预览相关状态(已移除未使用的 previewLoading 状态) // 时效计算相关状态 const [timelineVisible, setTimelineVisible] = useState(false); const [timelineLoading, setTimelineLoading] = useState(false); const [timelineResults, setTimelineResults] = useState([]); const [timelineAdjustments, setTimelineAdjustments] = useState>({}); // 交期余量扣减状态:记录从交期余量中扣减的天数 const [deliveryMarginDeductions, setDeliveryMarginDeductions] = useState(0); // 最后流程完成日期调整状态:记录最后流程完成日期增加的天数 const [completionDateAdjustment, setCompletionDateAdjustment] = useState(0); // 实际完成日期状态:记录每个节点的实际完成日期 const [actualCompletionDates, setActualCompletionDates] = useState<{[key: number]: Date | null}>({}); // 基础缓冲期天数(可配置),用于计算动态缓冲期,默认14天 const [baseBufferDays, setBaseBufferDays] = useState(14); const [lockedExpectedDeliveryDateTs, setLockedExpectedDeliveryDateTs] = useState(null); const [isExpectedDeliveryDateLocked, setIsExpectedDeliveryDateLocked] = useState(false); // 快照回填来源(foreign_id、款式、颜色、文本2) const [currentForeignId, setCurrentForeignId] = useState(null); const [currentStyleText, setCurrentStyleText] = useState(''); const [currentColorText, setCurrentColorText] = useState(''); // 标题中的款号/颜色编辑开关:默认锁定,点击笔按钮开放编辑 const [styleColorEditable, setStyleColorEditable] = useState(false); const [currentText2, setCurrentText2] = useState(''); const [currentVersionNumber, setCurrentVersionNumber] = useState(null); const [currentDeliveryRecordId, setCurrentDeliveryRecordId] = useState(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(1); const [batchEndRow, setBatchEndRow] = useState(1); const [batchTotalRows, setBatchTotalRows] = useState(0); const [batchLoading, setBatchLoading] = useState(false); const [batchProcessedCount, setBatchProcessedCount] = useState(0); const [batchProcessingTotal, setBatchProcessingTotal] = useState(0); const [batchSuccessCount, setBatchSuccessCount] = useState(0); const [batchFailureCount, setBatchFailureCount] = useState(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(false); const lastBufferDeficitRef = useRef(0); // 删除未使用的 deliveryRecords 状态 const [selectedDeliveryRecordId, setSelectedDeliveryRecordId] = useState(''); // 从货期记录读取到的record_ids(用于保存回写) const [restoredRecordIds, setRestoredRecordIds] = useState([]); // 原始文本格式的record_ids(不做JSON化写回) const [restoredRecordIdsText, setRestoredRecordIdsText] = useState(''); // 已移除:批量处理与表/视图选择相关状态 // 全局变量重置:在切换功能或切换版单/批量数据时,清空页面与计算相关状态 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(14); 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); setGroupOrderConfig([]); 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 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 (!req.viewId) delete req.viewId; if (token) req.pageToken = token; else delete req.pageToken; const res: any = await table.getRecordsByPage(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); 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(); 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 => { 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(); 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 = {}; 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 = {}; for (const [k, v] of entries) { out[k] = Math.round((v as number) * 100) / 100; } return out; }; const deriveTimelineAdjustmentsFromResults = (resultsFromSnapshot: any[]): Record => { const results = Array.isArray(resultsFromSnapshot) ? resultsFromSnapshot : []; const out: Record = {}; 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 => { const out: Record = {}; const prevArr = Array.isArray(prevResults) ? prevResults : []; const prevAdjustmentByName = new Map(); 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 => { const map: Record = {}; 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 => { const out: Record = {}; 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(); 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 = {}; 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 deliverySnapVal = deliveryFields?.[DELIVERY_SNAPSHOT_JSON_FIELD_ID]; let deliverySnapStr: string | null = null; if (typeof deliverySnapVal === 'string') { deliverySnapStr = deliverySnapVal; } else if (Array.isArray(deliverySnapVal)) { const texts = deliverySnapVal .filter((el: any) => el && el.type === 'text' && typeof el.text === 'string') .map((el: any) => el.text); deliverySnapStr = texts.length > 0 ? texts.join('') : null; } else if (deliverySnapVal && typeof deliverySnapVal === 'object') { if ((deliverySnapVal as any).text && typeof (deliverySnapVal as any).text === 'string') { deliverySnapStr = (deliverySnapVal as any).text; } else if ((deliverySnapVal as any).type === 'text' && typeof (deliverySnapVal as any).text === 'string') { deliverySnapStr = (deliverySnapVal as any).text; } } if (deliverySnapStr && deliverySnapStr.trim() !== '') { setIsRestoringSnapshot(true); const snapshot = JSON.parse(deliverySnapStr); 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_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 => { 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); const values = new Set(); 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(); 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 => { 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 => { 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 res: any = await deliveryTable.getRecordsByPage({ pageSize: 200, pageToken: token, filter: planFilter, sort, }); 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 res: any = await deliveryTable.getRecordsByPage({ pageSize: 200, pageToken: token, sort, }); 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: 10 }, (_, 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(); 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(/^标签([1-9]|10|12)$/); 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); 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 (allGroups.length > 1 && groupOrderConfig.length === 0 && showUI) { const initial = allGroups.map(g => createProcessGroupInstance(g)); 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(); for (const inst of groupOrderConfig) { const base = (inst?.groupName || '').trim(); if (base) configuredBaseNames.add(base); } const baseNameToNodes = new Map(); 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); // 优化2:预处理时效数据,建立节点名称到记录的映射 const timelineIndexByNode = new Map(); for (const timelineRecord of timelineRecords) { const timelineFields = timelineRecord.fields; const timelineNodeName = timelineFields[TIMELINE_NODE_FIELD_ID]; // 处理时效表中的节点名称 let timelineNodeNames: string[] = []; if (typeof timelineNodeName === 'string') { timelineNodeNames = timelineNodeName.split(',').map((name: 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: '当前起始日期晚于倒推要求起始日期,可能无法满足客户期望日期' }); } } } } const nextAdjustments = remapTimelineAdjustmentsToNewResults(timelineResults, results); setTimelineAdjustments(nextAdjustments); pendingRecalculateAfterCalculateAdjustmentsRef.current = nextAdjustments; pendingRecalculateAfterCalculateRef.current = true; setTimelineResults(results); if (!isBackward && mode === 'generate' && showUI && currentExpectedDate && results.length > 0) { let lastCompletionDate: Date | null = null; for (let i = results.length - 1; i >= 0; i--) { const end = results[i]?.estimatedEnd; if (!end || end === '时效值为0') continue; const parsed = typeof end === 'string' ? parseDate(end) : (end as any as Date); if (parsed && !isNaN(parsed.getTime())) { lastCompletionDate = parsed; break; } } if (lastCompletionDate) { const bufferDays = Math.max(0, Math.ceil(baseBufferDays)); const computedDeliveryDate = new Date(lastCompletionDate); computedDeliveryDate.setDate(computedDeliveryDate.getDate() + bufferDays); const slackDays = differenceInCalendarDays(currentExpectedDate, computedDeliveryDate); if (slackDays > 0) { const suggested = bufferDays + slackDays; await new Promise((resolve) => { Modal.confirm({ title: '建议增加缓冲期', content: `客户交期 ${formatDate(currentExpectedDate)}(${getDayOfWeek(currentExpectedDate)})晚于当前预计交付日期 ${formatDate(computedDeliveryDate)}(${getDayOfWeek(computedDeliveryDate)}),建议将缓冲期从 ${bufferDays} 天调整为 ${suggested} 天以对齐。`, okText: '增加缓冲期', cancelText: '不调整', onOk: () => { setBaseBufferDays(suggested); setHasAppliedSuggestedBuffer(true); setLastSuggestedApplied(suggested); resolve(); }, onCancel: () => resolve() }); }); } } } 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, 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) => { 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 = { ...(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): 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 computeBufferDeficitDaysUsingEndDelta = (adjustments: Record): number => { const deltaDays = computeLastNodeEndDeltaDays(adjustments); const base = Math.max(0, Math.ceil(baseBufferDays)); return Math.max(0, deltaDays - base); }; const computeExpectedDeliveryDateTsFromResults = ( results: any[], adjustments: Record, baseBufferDaysOverride?: number ): number | null => { if (timelineDirection === 'backward') return null; const lastCompletionDate = getLastValidCompletionDateFromResults(results); if (!lastCompletionDate) return null; const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(adjustments, baseBufferDaysOverride); const deliveryDate = new Date(lastCompletionDate); deliveryDate.setDate(deliveryDate.getDate() + dynamicBufferDays); const ts = deliveryDate.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, baseBufferDaysOverride?: number): number => { try { if (timelineDirection === 'backward') return 0; const deltaDays = computeLastNodeEndDeltaDays(adjustments); const base = Math.max(0, Math.ceil(baseBufferDaysOverride ?? baseBufferDays)); const remaining = base - deltaDays; return Math.max(0, Math.min(base, remaining)); } catch { const base = Math.max(0, Math.ceil(baseBufferDaysOverride ?? baseBufferDays)); return base; } }; // 重新计算时间线的函数 const recalculateTimeline = (adjustments: Record, 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(null); // 初始状态快照(仅捕获一次) const initialSnapshotRef = useRef(null); const hasCapturedInitialSnapshotRef = useRef(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(14); // 重置固定缓冲期为默认值 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 ?? 14); 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 = {}, 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 ]; console.log('需要检查的字段ID列表:', fieldsToCheck); // 获取各个字段 console.log('正在获取各个字段对象...'); const [ foreignIdField, labelsField, styleField, colorField, text2Field, createTimeField, expectedDateField, nodeDetailsField, customerExpectedDateField, adjustmentInfoField, versionField, startTimeField, snapshotField, 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_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); const baseBufferDaysToUse = Math.max(0, Math.ceil(overrides?.baseBufferDays ?? baseBufferDays)); // 获取预计交付日期(交期余量的日期版本:最后流程完成日期 + 基础缓冲期) let expectedDeliveryDate = computeExpectedDeliveryDateTsFromResults(timelineResults, timelineAdjustments, baseBufferDaysToUse); if (mode === 'adjust' && 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(); 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, baseBufferDaysToUse); // 检查是否达到最终限制 let hasReachedFinalLimit = false; const currentExpectedDate = expectedDate; if (dynamicBufferDays === 0 && currentExpectedDate && timelineResults.length > 0) { const lastNode = timelineResults[timelineResults.length - 1]; if (lastNode && lastNode.estimatedEnd && !lastNode.estimatedEnd.includes('未找到')) { try { const lastCompletionDate = new Date(lastNode.estimatedEnd); const daysToExpected = Math.ceil((currentExpectedDate.getTime() - lastCompletionDate.getTime()) / (1000 * 60 * 60 * 24)); hasReachedFinalLimit = daysToExpected <= 0; } catch (error) { console.warn('计算最终限制状态失败:', error); } } } // 为快照提供基础缓冲与节点调整总量(用于兼容历史字段),尽管动态缓冲期已改为“自然日差”口径 const baseBuferDays = baseBufferDaysToUse; 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); // 使用createCell方法创建各个字段的Cell const startTimestamp = currentStartTime ? currentStartTime.getTime() : currentTime; const recordIdsText = (restoredRecordIdsText && restoredRecordIdsText.trim() !== '') ? restoredRecordIdsText.trim() : ''; const [ foreignIdCell, labelsCell, styleCell, colorCell, text2Cell, createTimeCell, startTimeCell, snapshotCell, 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(snapshotJson), 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, 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 => { 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 }> = []; 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(); 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 = { [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(); 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); 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') || '']; 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 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 <= 10; i2++) { const key = `标签${i2}`; const raw = f[nameToId.get(key) || '']; 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: 10 }, (_, 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) { const baseForSuggestion = 14; const suggestedBaseBufferDays = (() => { if (!expectedDateObj || isNaN(expectedDateObj.getTime())) return baseForSuggestion; const lastCompletionDate = getLastValidCompletionDateFromResults(results); if (!lastCompletionDate) return baseForSuggestion; const computedDeliveryDate = new Date(lastCompletionDate); computedDeliveryDate.setDate(computedDeliveryDate.getDate() + baseForSuggestion); const slackDays = differenceInCalendarDays(expectedDateObj, computedDeliveryDate); return slackDays > 0 ? (baseForSuggestion + slackDays) : baseForSuggestion; })(); const processRecordIds = await writeToProcessDataTable(results, { foreignId, style: styleText, color: colorText }); const deliveryRecordId = await writeToDeliveryRecordTable( results, processRecordIds, {}, { foreignId, style: styleText, color: colorText, expectedDate: expectedDateObj || null, startTime: startDate || null, selectedLabels: labels, baseBufferDays: suggestedBaseBufferDays } ); 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(); 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 (
{/* 入口选择弹窗 */} setModeSelectionVisible(false)} maskClosable={false} >
chooseMode('generate')}> 生成流程日期 基于业务数据计算并生成节点时间线
chooseMode('adjust')}> 调整流程日期 读取货期记录,精确还原时间线
批量生成 从批量生成数据表读取并写入记录
{ batchAbortRef.current = true; setBatchModalVisible(false); }} footer={null} maskClosable={false} >
起始行 setBatchStartRow(typeof v === 'number' ? v : 1)} style={{ width: 120 }} disabled={batchLoading} /> 结束行 setBatchEndRow(typeof v === 'number' ? v : 1)} style={{ width: 120 }} disabled={batchLoading} /> 总行数:{batchTotalRows}
{(batchLoading || batchProcessedCount > 0) && (
0 ? Math.round((batchProcessedCount / batchProcessingTotal) * 100) : 0} showInfo />
进度:{batchProcessedCount}/{batchProcessingTotal} 成功 {batchSuccessCount},失败 {batchFailureCount}
{batchCurrentRowInfo && (
当前处理行:{batchCurrentRowInfo.index} 款号:{batchCurrentRowInfo.foreignId || '-'} 款式:{batchCurrentRowInfo.style || '-'} 颜色:{batchCurrentRowInfo.color || '-'}
)} ( 行 {item.index} 款号 {item.foreignId || '-'} {item.status === 'success' ? '成功' : '失败'} {item.message && {item.message}} )} /> )}
{mode === 'generate' && (
数据查询工具 基于业务数据计算并生成节点时间线
)} {/* 已移除:批量模式标题区块 */} {mode === 'adjust' && (
调整流程日期 读取货期记录,精确还原时间线
)} {/* 功能入口切换与调整入口 */} {mode !== null && (
颜色 最近保存记录ID )} )} {/* 批量处理功能 - 只在批量模式下显示 */}
)} { setAllocationVisible(false); setTimelineVisible(true); }} footer={
} 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 (
客户期望日期:{expectedDate ? formatDate(expectedDate, 'CHINESE_DATE') : '-'} 业务起始日期:{startTime ? formatDate(startTime) : '-'} 倒推要求起始日期:{calculatedRequiredStartTime ? formatDate(calculatedRequiredStartTime) : '-'} 盈余:{roundedTarget} 天(剩余 {remaining}{overAllocated > 0 ? `,已超配 ${overAllocated}` : ''})
分配方式 setCurrentStyleText(val)} /> 颜色 setCurrentColorText(val)} /> {mode === 'adjust' && ( <> 锁交付 )}
} visible={timelineVisible} onCancel={() => { setTimelineVisible(false); setTimelineAdjustments({}); // 关闭时重置调整 setDeliveryMarginDeductions(0); // 关闭时重置交期余量扣减 setCompletionDateAdjustment(0); // 关闭时重置最后流程完成日期调整 setStyleColorEditable(false); // 关闭弹窗后恢复为锁定状态 if (mode !== 'adjust') { setLockedExpectedDeliveryDateTs(null); setIsExpectedDeliveryDateLocked(false); } }} footer={
{mode === 'adjust' && isExpectedDeliveryDateLocked ? '基础缓冲期(天)(不影响已锁交付):' : '基础缓冲期(天):'} { const n = Number(val); setBaseBufferDays(Number.isFinite(n) && n >= 0 ? Math.ceil(n) : 0); }} style={{ width: 90 }} /> 剩余缓冲期(天):
} width={900} >
{timelineResults.length === 0 ? (
未找到匹配的时效数据
) : (
起始时间 { 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" />
{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); return (
{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> )}
实际完成: { // 自动填充默认时分:仅在用户未指定具体时分时套用预计完成的时分 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触发,避免使用旧状态 }} />
时效值调整:
0 ? '#52c41a' : '#ff4d4f', fontWeight: 'bold', fontSize: '13px' }}> {adjustedValue.toFixed(1)} 工作日 {adjustment !== 0 && (
原值: {baseValue.toFixed(1)}
调整: {adjustment > 0 ? '+' : ''}{adjustment.toFixed(1)}
)}
预计开始: {formatDate(result.estimatedStart)}
{getDayOfWeek(result.estimatedStart)}
预计完成: {result.estimatedEnd === '时效值为0' ? result.estimatedEnd : formatDate(result.estimatedEnd)}
{getDayOfWeek(result.estimatedEnd)}
实际跨度: {calculateActualDays(result.estimatedStart, result.estimatedEnd)} 自然日
含 {adjustedValue.toFixed(1)} 工作日
计算方式: {result.calculationMethod || '外部'} {result.ruleDescription && ( ({result.ruleDescription}) )} 0 ? `休息日配置:${result.weekendDaysConfig.map((day: 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')}` : ''}`} > ?
跳过日期:{(result.actualExcludedDatesCount && result.actualExcludedDatesCount > 0) ? `${result.actualExcludedDatesCount} 天` : '无'}
); })} {/* 计算公式展示卡片 */} {timelineResults.length > 0 && (
📅 交期余量计算 {/* 汇总两行:1)最后完成+缓冲期=结束日期 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); const adjustedCompletionDate = new Date(lastCompletionDate!); adjustedCompletionDate.setDate(adjustedCompletionDate.getDate() + completionDateAdjustment); const deliveryDate = new Date(adjustedCompletionDate); deliveryDate.setDate(deliveryDate.getDate() + dynamicBufferDays); const lockedDeliveryDate = (mode === 'adjust' && isExpectedDeliveryDateLocked && lockedExpectedDeliveryDateTs !== null) ? new Date(lockedExpectedDeliveryDateTs) : null; return ( <> {/* 第一行:最后流程完成日期 + 缓冲期 = 结束日期(优化为紧凑分段展示) */}
最后流程完成日期 {formatDate(adjustedCompletionDate)}({getDayOfWeek(adjustedCompletionDate)}) + 缓冲期 {dynamicBufferDays}天 = {lockedDeliveryDate ? '结束日期(未锁)' : '结束日期'} {formatDate(deliveryDate)}({getDayOfWeek(deliveryDate)})
{lockedDeliveryDate && (
结束日期(已锁定) {formatDate(lockedDeliveryDate)}({getDayOfWeek(lockedDeliveryDate)})
)} {/* 第二行:客户期望日期(可更改,优化展示为标签样式) */}
客户期望日期(可更改) { 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 && ( {formatDate(expectedDate)}({getDayOfWeek(expectedDate)}) )}
); })()}
)}
📊 计算说明: 共找到 {timelineResults.length} 个匹配的节点。时间按流程顺序计算,上一个节点的完成时间等于下一个节点的开始时间。每个节点使用其特定的休息日配置和计算方式进行工作日计算。
🗓️ 计算规则说明: 内部计算:按9小时工作制(9:00-18:00),超时自动顺延至下个工作日
外部计算:按24小时制计算,适用于外部供应商等不受工作时间限制的节点
• 根据每个节点的休息日配置自动跳过相应的休息日
• 可为每个节点配置“跳过日期”,这些日期将不参与工作日计算
• 时效值以"工作日"为单位计算,确保预期时间的准确性
• 使用 +1/-1 按钮调整整天,+0.5/-0.5 按钮调整半天,系统会自动重新计算所有后续节点
• 不同节点可配置不同的休息日和计算方式(如:内部节点按工作时间,外部节点按自然时间)
跳过日期 已配置 {Object.values(excludedDatesByNodeOverride || {}).filter(v => Array.isArray(v) && v.length > 0).length} 个节点
{Object.keys(timelineAdjustments).length > 0 && (
当前调整:
{(() => { const nodeNameByKey = new Map(); 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 ( {nodeName}: {adjustment > 0 ? '+' : ''}{adjustment.toFixed(1)} 天 ); }); })()}
)}
)}
{/* 标签选择部分,生成/调整模式均可使用 */} {(mode === 'generate' || labelAdjustmentFlow) && labelOptions && Object.keys(labelOptions).length > 0 && (
{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 (
{labelKey}({isRequired ? '必填' : '非必填'}) {labelKey === '标签7' && ( 此标签用作生产端工厂货期时效 )} {labelKey === '标签12' && ( 此标签用作前置流程节点,若选择版单不自动带出,建议提前询问一下设计跟单 )} {isRequired && isMissing && ( 该标签为必填 )}
); })}
计算方向
起始日期 { 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' && ( 倒推模式必填 )}
{/* 客户期望日期选择 */}
客户期望日期 { 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 && ( 已选择:{formatDate(expectedDate, 'CHINESE_DATE')} )}
{Object.keys(selectedLabels).length > 0 && (
已选择 {Object.keys(selectedLabels).length} 个标签
当前选择的标签:
{Object.entries(selectedLabels).map(([key, value]) => { const displayValue = Array.isArray(value) ? value.join(', ') : value; return ( {key}: {displayValue} ); })}
)}
)} {/* 批量处理配置已移除 */} {mode === 'generate' && (
版单数据 选择需要生成时效的版单记录 {selectedRecords.length > 0 && ( 已选择 {selectedRecords.length} 条记录 )}
{selectedRecords.length > 0 && ( )}
{/* 已选择记录的详细信息 */} {selectedRecords.length > 0 && recordDetails.length > 0 && (
主记录: {recordDetails[0].displayValue || recordDetails[0].id}
{recordDetails.length > 1 && (
+ 其他 {recordDetails.length - 1} 条
)}
)} {/* 加载状态 */} {loading && (
正在加载版单数据...
)} {/* 空状态提示 */} {selectedRecords.length === 0 && !loading && (
请选择版单记录开始操作
)}
{/* 批量处理配置已移除 */}
)} {mode === 'generate' && ( <> {/* 面料数据查询结果 */} {queryResults.length > 0 && ( <> 面料数据查询结果 ({queryResults.length} 条) ({ ...item, key: index }))} pagination={{ pageSize: 10 }} style={{ marginTop: '10px' }} /> )} {/* 二次工艺查询结果 */} {secondaryProcessResults.length > 0 && ( <> 二次工艺查询结果 ({secondaryProcessResults.length} 条)
({ ...item, key: index }))} pagination={{ pageSize: 10 }} style={{ marginTop: '10px' }} /> )} {/* 工艺价格查询结果 */} {pricingDetailsResults.length > 0 && ( <> 工艺价格查询结果 ({pricingDetailsResults.length} 条)
({ ...item, key: index }))} pagination={{ pageSize: 10 }} style={{ marginTop: '10px' }} /> )} )} ); }