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