diff --git a/src/App.tsx b/src/App.tsx index 339ca39..d14e700 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { bitable, FieldType } from '@lark-base-open/js-sdk'; import { Button, Typography, List, Card, Space, Divider, Spin, Table, Select, Modal, DatePicker, InputNumber } from '@douyinfe/semi-ui'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { addDays, format } from 'date-fns'; import { zhCN } from 'date-fns/locale'; import { executePricingQuery, executeSecondaryProcessQuery, executePricingDetailsQuery } from './services/apiService'; @@ -136,6 +136,12 @@ export default function App() { setRestoredRecordIds([]); setRestoredRecordIdsText(''); + // 重置初始快照捕获状态 + try { + hasCapturedInitialSnapshotRef.current = false; + initialSnapshotRef.current = null; + } catch {} + // 当前记录与批量信息 setCurrentBatchRecord(null); @@ -2371,8 +2377,8 @@ export default function App() { const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); const baseBuferDays = baseBufferDays; - // 智能缓冲期计算逻辑 - let dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments); + // 智能缓冲期计算逻辑(缓冲期扣减=最后完成时间变动的自然天数) + let dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments); // 只有在缓冲期为0时才检查最终限制:最后节点预计完成时间是否已达到客户期望日期 let hasReachedFinalLimit = false; @@ -2503,11 +2509,11 @@ export default function App() { return timelineResults; }; - // 获取重新计算后的时间线结果(不更新状态) - const getRecalculatedTimeline = (adjustments: {[key: number]: number}) => { + // 获取重新计算后的时间线结果(不更新状态,逻辑对齐页面的重算口径) + const getRecalculatedTimeline = (adjustments: { [key: number]: number }) => { const updatedResults = [...timelineResults]; let cumulativeStartTime = startTime ? new Date(startTime) : new Date(); // 从起始时间开始 - + for (let i = 0; i < updatedResults.length; i++) { const result = updatedResults[i]; const baseTimelineValue = (typeof result.timelineValue === 'number') @@ -2517,23 +2523,56 @@ export default function App() { : 0; const adjustment = adjustments[i] || 0; const adjustedTimelineValue = baseTimelineValue + adjustment; - + // 计算当前节点的开始时间 let nodeStartTime = new Date(cumulativeStartTime); - - // 获取节点的计算方式、周末天数和排除日期 - const nodeCalculationMethod = result.calculationMethod || 'external'; - const nodeWeekendDays = result.weekendDays || []; - const nodeExcludedDates = result.excludedDates || []; - - // 获取调整规则描述 - const ruleDescription = result.ruleDescription || ''; - + + // 应用起始日期调整规则(与页面重算逻辑一致) + if (result.startDateRule) { + let ruleJson = ''; + if (typeof result.startDateRule === 'string') { + ruleJson = result.startDateRule; + } else if (result.startDateRule && result.startDateRule.text) { + ruleJson = result.startDateRule.text; + } + if (ruleJson.trim()) { + nodeStartTime = adjustStartDateByRule(nodeStartTime, ruleJson); + } + } + + // 应用 JSON 日期调整规则(与页面重算逻辑一致) + let ruleDescription = result.ruleDescription || ''; + if (result.dateAdjustmentRule) { + let ruleText = ''; + if (typeof result.dateAdjustmentRule === 'string') { + ruleText = result.dateAdjustmentRule; + } else if (Array.isArray(result.dateAdjustmentRule)) { + ruleText = result.dateAdjustmentRule + .filter(item => item.type === 'text') + .map(item => item.text) + .join(''); + } else if (result.dateAdjustmentRule && result.dateAdjustmentRule.text) { + ruleText = result.dateAdjustmentRule.text; + } + if (ruleText && ruleText.trim() !== '') { + const adjustmentResult = adjustStartDateByJsonRule(nodeStartTime, ruleText); + nodeStartTime = adjustmentResult.adjustedDate; + if (adjustmentResult.description) { + ruleDescription = adjustmentResult.description; + } + } + } + + // 获取节点的计算方式、休息日与排除日期(与页面重算逻辑一致) + const nodeWeekendDays = result.weekendDaysConfig || []; + const nodeCalculationMethod = result.calculationMethod || '外部'; + const nodeExcludedDates = Array.isArray(result.excludedDates) ? result.excludedDates : []; + // 计算节点的结束时间(允许负时效值向前回退) - let nodeEndTime; + let nodeEndTime: Date; if (adjustedTimelineValue !== 0) { const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates); - if (nodeCalculationMethod === 'internal') { + if (nodeCalculationMethod === '内部') { nodeEndTime = addInternalBusinessTime(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates); } else { nodeEndTime = addBusinessDaysWithHolidays(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates); @@ -2541,43 +2580,90 @@ export default function App() { } else { nodeEndTime = new Date(nodeStartTime); } - - // 计算跳过的天数 + + // 计算跳过的天数及日期范围内的自定义跳过日期 const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates); const skippedWeekends = calculateSkippedWeekends(adjustedStartTime, nodeEndTime, nodeWeekendDays); const estimatedStartStr = formatDate(adjustedStartTime); const estimatedEndStr = adjustedTimelineValue !== 0 ? formatDate(nodeEndTime) : '时效值为0'; const actualDays = calculateActualDays(estimatedStartStr, estimatedEndStr); - - // 计算时间范围内实际跳过的自定义日期 const excludedDatesInRange = calculateExcludedDatesInRange(adjustedStartTime, nodeEndTime, nodeExcludedDates); - + // 更新结果 updatedResults[i] = { ...result, - adjustedTimelineValue: adjustedTimelineValue, + adjustedTimelineValue, estimatedStart: estimatedStartStr, estimatedEnd: estimatedEndStr, - adjustment: adjustment, - calculationMethod: nodeCalculationMethod, // 保持计算方式 - skippedWeekends: skippedWeekends, - actualDays: actualDays, - // 更新时间范围内实际跳过的日期 + adjustment, + calculationMethod: nodeCalculationMethod, + skippedWeekends, + actualDays, actualExcludedDates: excludedDatesInRange.dates, actualExcludedDatesCount: excludedDatesInRange.count, - adjustmentDescription: result.adjustmentDescription, // 保持调整规则描述 - ruleDescription: ruleDescription // 添加更新后的规则描述 + adjustmentDescription: result.adjustmentDescription, + ruleDescription }; - - // 更新累积时间:当前节点的完成时间成为下一个节点的开始时间 + + // 更新累积开始时间:使用当前节点的预计完成时间 if (adjustedTimelineValue !== 0) { cumulativeStartTime = new Date(nodeEndTime); } } - + return updatedResults; }; + // 获取有效的最后完成日期(忽略 '时效值为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 computeDynamicBufferDaysUsingEndDelta = (adjustments: { [key: number]: number }): number => { + try { + const baseline = getRecalculatedTimeline({}); // 原始计划(不含任何调整) + 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 Math.max(0, Math.min(baseBufferDays, baseBufferDays)); + } + + // 仅使用“预计完成日期”差,不叠加 completionDateAdjustment + const dayMs = 1000 * 60 * 60 * 24; + const deltaDays = Math.ceil((currentLast.getTime() - baselineLast.getTime()) / dayMs); + // 缓冲期剩余 = clamp(基础缓冲期 - 差值, 0, 基础缓冲期) + const remaining = baseBufferDays - deltaDays; + return Math.max(0, Math.min(baseBufferDays, remaining)); + } catch { + // 兜底:如出现异常,保持基础缓冲期 + return Math.max(0, Math.min(baseBufferDays, baseBufferDays)); + } + }; + // 重新计算时间线的函数 const recalculateTimeline = (adjustments: {[key: number]: number}, forceRecalculateAll: boolean = false) => { const updatedResults = [...timelineResults]; @@ -2750,6 +2836,10 @@ export default function App() { const [hasAppliedSuggestedBuffer, setHasAppliedSuggestedBuffer] = useState(false); + // 初始状态快照(仅捕获一次) + const initialSnapshotRef = useRef(null); + const hasCapturedInitialSnapshotRef = useRef(false); + // 当起始时间变更时,重新以最新起始时间为基准重算全流程 useEffect(() => { if (timelineResults.length > 0 && !isRestoringSnapshot) { @@ -2764,6 +2854,26 @@ export default function App() { } }, [actualCompletionDates, isRestoringSnapshot]); + // 捕获初始状态快照(在首次生成/还原出完整时间线后) + useEffect(() => { + if (!hasCapturedInitialSnapshotRef.current && timelineResults.length > 0) { + initialSnapshotRef.current = { + startTime, + expectedDate, + selectedLabels, + timelineResults, + timelineAdjustments, + baseBufferDays, + actualCompletionDates, + completionDateAdjustment, + hasAppliedSuggestedBuffer, + deliveryMarginDeductions, + }; + hasCapturedInitialSnapshotRef.current = true; + console.log('已捕获初始状态快照'); + } + }, [timelineResults, startTime, expectedDate]); + // 重置调整的函数 const resetTimelineAdjustments = () => { setTimelineAdjustments({}); @@ -2774,6 +2884,40 @@ export default function App() { setHasAppliedSuggestedBuffer(false); // 重置建议缓冲期应用标志 recalculateTimeline({}, true); // 强制重算所有节点 }; + + // 一键还原到最初状态 + const resetToInitialState = async () => { + try { + if (!hasCapturedInitialSnapshotRef.current || !initialSnapshotRef.current) { + // 若未捕获到初始快照,则退化为仅重置调整 + resetTimelineAdjustments(); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'warning', message: '未检测到初始快照,已重置调整项' }); + } + return; + } + const s = initialSnapshotRef.current; + setIsRestoringSnapshot(true); + setStartTime(s.startTime || null); + setExpectedDate(s.expectedDate || null); + setSelectedLabels(s.selectedLabels || {}); + setTimelineAdjustments(s.timelineAdjustments || {}); + setBaseBufferDays(s.baseBufferDays ?? 14); + setActualCompletionDates(s.actualCompletionDates || {}); + setCompletionDateAdjustment(s.completionDateAdjustment || 0); + setHasAppliedSuggestedBuffer(!!s.hasAppliedSuggestedBuffer && s.hasAppliedSuggestedBuffer); + setDeliveryMarginDeductions(s.deliveryMarginDeductions || 0); + setTimelineResults(Array.isArray(s.timelineResults) ? s.timelineResults : []); + recalculateTimeline(s.timelineAdjustments || {}, true); + setIsRestoringSnapshot(false); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'success', message: '已恢复至最初状态' }); + } + } catch (e) { + console.error('恢复初始状态失败:', e); + setIsRestoringSnapshot(false); + } + }; // 已移除未使用的 getTimelineLabelFieldId 辅助函数 @@ -2972,10 +3116,8 @@ export default function App() { try { const lastCompletionDate = new Date(effectiveLastProcess.estimatedEnd); - // 计算动态缓冲期:基础缓冲期 - 节点总调整量,最小为0天 - const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); - const baseBuferDays = baseBufferDays; - const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments); + // 计算动态缓冲期:按“最后完成时间自然日差”扣减基础缓冲期 + const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments); // 加上动态缓冲期 const deliveryDate = new Date(lastCompletionDate); @@ -3050,9 +3192,7 @@ export default function App() { const styleText = style || ''; const colorText = color || ''; - const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); - const baseBuferDays = baseBufferDays; - const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments); + const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments); // 检查是否达到最终限制 let hasReachedFinalLimit = false; @@ -3070,6 +3210,10 @@ export default function App() { } } + // 为快照提供基础缓冲与节点调整总量(用于兼容历史字段),尽管动态缓冲期已改为“自然日差”口径 + const baseBuferDays = baseBufferDays; + const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); + const globalSnapshot = { version: versionNumber, foreignId, @@ -5468,10 +5612,24 @@ export default function App() { {/* 建议缓冲期增量:根据实际完成偏差与交期余量自动反算 */} {(() => { try { - // 计算节点总调整量与动态缓冲期 - const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); + // 仅当用户本次会话发生改动时显示建议 + const initialSnap = initialSnapshotRef.current; + const hasAdjustmentChanges = Object.values(timelineAdjustments || {}).some(v => (Number(v) || 0) !== 0) || (Number(completionDateAdjustment) || 0) !== 0; + const toMillisMap = (obj: any) => { + const entries = Object.entries(obj || {}); + return Object.fromEntries(entries.map(([k, v]) => [k, v ? new Date(v as any).getTime() : null])); + }; + const currentActualMap = toMillisMap(actualCompletionDates); + const initialActualMap = toMillisMap(initialSnap?.actualCompletionDates || {}); + const hasActualChanges = JSON.stringify(currentActualMap) !== JSON.stringify(initialActualMap); + const hasUserSessionChanges = hasAdjustmentChanges || hasActualChanges; + if (!hasUserSessionChanges) { + return null; + } + + // 计算动态缓冲期(按“最后完成时间自然日差”扣减基础缓冲期) const baseBuferDays = baseBufferDays; - const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments); + const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments); // 获取有效的最后流程完成日期 let effectiveLastProcess: any = null; @@ -5562,7 +5720,7 @@ export default function App() { return null; } })()} - @@ -5649,10 +5807,10 @@ export default function App() { // 当前节点是否为零值的周转周期节点 const isCurrentTurnoverZero = isTurnoverNode && adjustedValue === 0; - // 计算动态缓冲期,用于限制节点增加操作 + // 计算动态缓冲期(按“最后完成时间变动天数”扣减缓冲期) const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); - const baseBuferDays = baseBufferDays; - const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments); + const baseBuferDays = baseBufferDays; + const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments); // 新的复合限制逻辑: // 1. 如果缓冲期 > 0,允许操作 @@ -5904,30 +6062,24 @@ export default function App() {
时效值调整:
- {/* 注释掉 -0.5 功能按钮 */} - {false && ( - - )} - {/* 注释掉 -1 功能按钮 */} - {false && ( - - )} + +
)}
- {/* 注释掉 +1 功能按钮 */} - {false && ( - - )} - {/* 注释掉 +0.5 功能按钮 */} - {false && ( - - )} + +
@@ -6121,12 +6267,8 @@ export default function App() { lastCompletionDate = new Date(effectiveLastProcess.estimatedEnd); } - // 计算所有节点的总调整量(用于动态缓冲期计算) - const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); - - // 计算动态缓冲期:基础14天 - 节点总调整量,最小为0天 - const baseBuferDays = baseBufferDays; - const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments); + // 计算动态缓冲期:按“最后完成时间自然日差”扣减基础缓冲期 + const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments); // 计算最后完成日期 + 动态缓冲期(考虑最后流程完成日期的调整) const adjustedCompletionDate = new Date(lastCompletionDate); @@ -6409,11 +6551,11 @@ export default function App() { lineHeight: '1.5' }}> {(() => { - // 计算动态缓冲期和总调整量 - const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0); + // 计算动态缓冲期(按“最后完成时间自然日差”扣减基础缓冲期) + const dynamicBufferDays = computeDynamicBufferDaysUsingEndDelta(timelineAdjustments); const baseBuferDays = baseBufferDays; - const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments); - + const deltaDays = baseBuferDays - dynamicBufferDays; // 自然日差(已按0..baseBuferDays夹取) + return ( 💡 说明: @@ -6427,11 +6569,11 @@ export default function App() { : '• 显示预计交付日期距离今天的天数' }
- • 缓冲期 = 基础{baseBuferDays}天 - 节点调整总量(最小0天),包含质检、包装、物流等后续环节 - {totalAdjustments > 0 && ( + • 缓冲期 = 基础{baseBuferDays}天 - 最后流程节点预计完成日期的自然日差(最小0天),包含质检、包装、物流等后续环节 + {deltaDays > 0 && ( <>
- • 当前节点总调整量:+{totalAdjustments}天,缓冲期相应减少 + • 当前自然日差:+{deltaDays}天,缓冲期相应减少 )}