3
This commit is contained in:
2026-04-09 10:13:24 +08:00
parent 27790399f3
commit 55c27e77bf

View File

@ -224,6 +224,7 @@ export default function App() {
const [batchEndRow, setBatchEndRow] = useState<number>(1); const [batchEndRow, setBatchEndRow] = useState<number>(1);
const [batchTotalRows, setBatchTotalRows] = useState<number>(0); const [batchTotalRows, setBatchTotalRows] = useState<number>(0);
const [batchLoading, setBatchLoading] = useState(false); const [batchLoading, setBatchLoading] = useState(false);
const [batchMode, setBatchMode] = useState<'generate' | 'adjust'>('generate');
const [batchProcessedCount, setBatchProcessedCount] = useState<number>(0); const [batchProcessedCount, setBatchProcessedCount] = useState<number>(0);
const [batchProcessingTotal, setBatchProcessingTotal] = useState<number>(0); const [batchProcessingTotal, setBatchProcessingTotal] = useState<number>(0);
const [batchSuccessCount, setBatchSuccessCount] = useState<number>(0); const [batchSuccessCount, setBatchSuccessCount] = useState<number>(0);
@ -404,6 +405,9 @@ export default function App() {
const BATCH_LABEL11_FIELD_ID = 'fld4BZHtBV'; const BATCH_LABEL11_FIELD_ID = 'fld4BZHtBV';
const BATCH_LABEL12_FIELD_ID = 'fldnRlMeaD'; const BATCH_LABEL12_FIELD_ID = 'fldnRlMeaD';
const BATCH_BUFFER_FIELD_ID = 'fldLBXEAo0'; const BATCH_BUFFER_FIELD_ID = 'fldLBXEAo0';
const BATCH_SOURCE_DELIVERY_RECORD_ID_FIELD_ID = 'fld5pcGWeh';
const BATCH_RESULT_DELIVERY_RECORD_ID_FIELD_ID = 'fld9as1Y1c';
const BATCH_CUSTOMER_EXPECTED_DATE_FIELD_ID = 'fldqi7nUix';
const activateTableForPaging = async (table: any) => { const activateTableForPaging = async (table: any) => {
try { try {
@ -468,6 +472,115 @@ export default function App() {
return Number.isFinite(n) ? n : null; return Number.isFinite(n) ? n : null;
}; };
const parseBatchDateValue = (raw: any): Date | null => {
if (raw == null) return null;
if (raw instanceof Date && !isNaN(raw.getTime())) return raw;
if (typeof raw === 'number' && Number.isFinite(raw)) {
const d = new Date(raw);
return isNaN(d.getTime()) ? null : d;
}
if (Array.isArray(raw) && raw.length > 0) {
for (const item of raw) {
const parsed = parseBatchDateValue(item);
if (parsed) return parsed;
}
return null;
}
if (typeof raw === 'string') {
const t = raw.trim();
if (!t) return null;
const asNum = Number(t);
if (Number.isFinite(asNum) && /^\d{10,13}$/.test(t)) {
const d = new Date(asNum);
if (!isNaN(d.getTime())) return d;
}
const parsed = parseDate(t);
if (parsed && !isNaN(parsed.getTime())) return parsed;
const d = new Date(t);
return isNaN(d.getTime()) ? null : d;
}
if (typeof raw === 'object') {
const v = (raw as any).value ?? (raw as any).text ?? (raw as any).name;
return parseBatchDateValue(v);
}
return null;
};
const normalizeActualCompletionDatesMap = (raw: any): { [key: number]: Date | null } => {
const out: { [key: number]: Date | null } = {};
if (!raw || typeof raw !== 'object') return out;
for (const [k, v] of Object.entries(raw)) {
const idx = Number(k);
if (!Number.isFinite(idx)) continue;
const parsed = parseBatchDateValue(v);
out[idx] = parsed;
}
return out;
};
const getBatchSimulationStateFromSnapshot = async (sourceDeliveryRecordId: string) => {
const deliveryTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID);
const deliveryRecord: any = await deliveryTable.getRecordById(sourceDeliveryRecordId);
const fields = deliveryRecord?.fields || {};
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) => extractAllText(el)).join('');
if (typeof val === 'object') return (val as any).text || (val as any).name || (val as any).value?.toString?.() || '';
return '';
};
const snapshotPart1 = extractAllText(fields?.[DELIVERY_SNAPSHOT_JSON_FIELD_ID]);
const snapshotPart2 = extractAllText(fields?.[DELIVERY_SNAPSHOT_JSON_2_FIELD_ID]);
const snapshotStr = `${snapshotPart1 || ''}${snapshotPart2 || ''}`.trim();
if (!snapshotStr) {
throw new Error('源货期记录缺少快照');
}
const snapshot = JSON.parse(snapshotStr);
if (!Array.isArray(snapshot?.timelineResults) || snapshot.timelineResults.length === 0) {
throw new Error('源快照没有可用的timelineResults');
}
const timelineResultsFromSnapshot = snapshot.timelineResults as any[];
const labelsFromSnapshot = normalizeSelectedLabelsForRestore(snapshot?.selectedLabels || {});
const expectedFromSnapshot =
parseBatchDateValue(snapshot?.expectedDateTimestamp) ||
parseBatchDateValue(snapshot?.expectedDateString) ||
parseBatchDateValue(fields?.[DELIVERY_CUSTOMER_EXPECTED_DATE_FIELD_ID]);
const startFromSnapshot =
parseBatchDateValue(snapshot?.startTimestamp) ||
parseBatchDateValue(snapshot?.startString) ||
parseBatchDateValue(fields?.[DELIVERY_START_TIME_FIELD_ID]);
const snapshotAdjustments = snapshot?.timelineAdjustments
? normalizeTimelineAdjustmentsFromSnapshot(snapshot.timelineAdjustments, timelineResultsFromSnapshot)
: deriveTimelineAdjustmentsFromResults(timelineResultsFromSnapshot);
const excludedDatesByNodeFromResults = buildExcludedDatesByNodeFromTimeline(timelineResultsFromSnapshot);
const excludedDatesByNodeFromSnapshotMap = normalizeExcludedDatesByNodeMap(
snapshot?.excludedDatesByNodeOverride ||
snapshot?.timelineCalculationState?.excludedDatesByNodeOverride ||
{}
);
const sourceRecordIdsText = extractText(fields?.[DELIVERY_RECORD_IDS_FIELD_ID]).trim();
return {
snapshot,
timelineResults: timelineResultsFromSnapshot,
selectedLabels: labelsFromSnapshot,
expectedDate: expectedFromSnapshot,
startTime: startFromSnapshot,
timelineAdjustments: snapshotAdjustments,
excludedDatesOverride: normalizeExcludedDatesOverride(snapshot?.excludedDatesOverride || []),
excludedDatesByNodeOverride: Object.keys(excludedDatesByNodeFromResults).length > 0
? excludedDatesByNodeFromResults
: excludedDatesByNodeFromSnapshotMap,
foreignId: snapshot?.foreignId || '',
styleText: snapshot?.styleText || '',
colorText: snapshot?.colorText || '',
text2: snapshot?.text2 || '',
versionNumber: Number.isFinite(Number(snapshot?.version)) ? Number(snapshot.version) : null,
actualCompletionDates: normalizeActualCompletionDatesMap(snapshot?.actualCompletionDates),
sourceRecordIdsText
};
};
// 已移除:调整模式不再加载货期记录列表 // 已移除:调整模式不再加载货期记录列表
// 入口选择处理 // 入口选择处理
@ -648,6 +761,21 @@ export default function App() {
return '#unknown'; return '#unknown';
}; };
const getExcludedDatesNodeKeyCandidates = (node: any, indexFallback?: number): string[] => {
const nodeName = node?.nodeName;
const processOrder = node?.processOrder;
const instanceKey = node?.processGroupInstanceId || node?.processGroupInstanceName;
const candidates = [
buildExcludedDatesNodeKey(nodeName, processOrder, indexFallback, instanceKey),
buildExcludedDatesNodeKey(nodeName, processOrder, indexFallback),
buildExcludedDatesNodeKey(nodeName, undefined, indexFallback, instanceKey),
buildExcludedDatesNodeKey(nodeName, undefined, indexFallback),
]
.map(k => (k || '').trim())
.filter(k => k && k !== '#unknown');
return Array.from(new Set(candidates));
};
const buildTimelineAdjustmentKey = (node: any, indexFallback?: number) => { const buildTimelineAdjustmentKey = (node: any, indexFallback?: number) => {
return buildExcludedDatesNodeKey( return buildExcludedDatesNodeKey(
node?.nodeName, node?.nodeName,
@ -832,16 +960,35 @@ export default function App() {
const buildExcludedDatesByNodeFromTimeline = (results: any[]): Record<string, string[]> => { const buildExcludedDatesByNodeFromTimeline = (results: any[]): Record<string, string[]> => {
const map: Record<string, string[]> = {}; const map: Record<string, string[]> = {};
const assignExcludedDates = (key: string, val: string[]) => {
if (!key) return;
const existing = Array.isArray(map[key]) ? map[key] : [];
if (existing.length > 0 && val.length === 0) return;
map[key] = val;
};
for (let i = 0; i < results.length; i++) { for (let i = 0; i < results.length; i++) {
const r = results[i]; const r = results[i];
const key = buildExcludedDatesNodeKey( const keyWithInstance = buildExcludedDatesNodeKey(
r?.nodeName, r?.nodeName,
r?.processOrder, r?.processOrder,
i, i,
r?.processGroupInstanceId || r?.processGroupInstanceName r?.processGroupInstanceId || r?.processGroupInstanceName
); );
const keyWithoutInstance = buildExcludedDatesNodeKey(
r?.nodeName,
r?.processOrder,
i
);
const keyByName = buildExcludedDatesNodeKey(
r?.nodeName,
undefined,
i
);
const arr = Array.isArray(r?.excludedDates) ? r.excludedDates : []; const arr = Array.isArray(r?.excludedDates) ? r.excludedDates : [];
map[key] = normalizeExcludedDatesOverride(arr); const normalized = normalizeExcludedDatesOverride(arr);
assignExcludedDates(keyWithInstance, normalized);
assignExcludedDates(keyWithoutInstance, normalized);
assignExcludedDates(keyByName, normalized);
} }
return map; return map;
}; };
@ -861,6 +1008,18 @@ export default function App() {
}; };
const restoreExcludedDatesByNodeOverrideFromSnapshot = (snapshot: any, timelineResultsCandidate?: any[]) => { const restoreExcludedDatesByNodeOverrideFromSnapshot = (snapshot: any, timelineResultsCandidate?: any[]) => {
const results = Array.isArray(timelineResultsCandidate)
? timelineResultsCandidate
: (Array.isArray(snapshot?.timelineResults) ? snapshot.timelineResults : []);
if (results.length > 0) {
const derived = buildExcludedDatesByNodeFromTimeline(results);
if (Object.keys(derived).length > 0) {
excludedDatesByNodeOverrideRef.current = derived;
setExcludedDatesByNodeOverride(derived);
return;
}
}
const candidates: any[] = [ const candidates: any[] = [
snapshot?.excludedDatesByNodeOverride, snapshot?.excludedDatesByNodeOverride,
snapshot?.excludedDatesByNode, snapshot?.excludedDatesByNode,
@ -877,16 +1036,6 @@ export default function App() {
} }
} }
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 = {}; excludedDatesByNodeOverrideRef.current = {};
setExcludedDatesByNodeOverride({}); setExcludedDatesByNodeOverride({});
}; };
@ -2775,7 +2924,8 @@ export default function App() {
selectedLabels?: {[key: string]: string | string[]}, selectedLabels?: {[key: string]: string | string[]},
expectedDate?: Date | null, expectedDate?: Date | null,
startTime?: Date | null, startTime?: Date | null,
excludedDates?: string[] excludedDates?: string[],
prevResultsForAdjustmentRemap?: any[]
}, },
showUI: boolean = true // 新增参数控制是否显示UI showUI: boolean = true // 新增参数控制是否显示UI
) => { ) => {
@ -2785,8 +2935,6 @@ export default function App() {
const currentSelectedLabels = overrideData?.selectedLabels || selectedLabels; const currentSelectedLabels = overrideData?.selectedLabels || selectedLabels;
const currentExpectedDate = overrideData?.expectedDate || expectedDate; const currentExpectedDate = overrideData?.expectedDate || expectedDate;
const currentStartTime = overrideData?.startTime || startTime; const currentStartTime = overrideData?.startTime || startTime;
const currentExcludedDates = Array.isArray(overrideData?.excludedDates) ? overrideData!.excludedDates : [];
const globalExcludedDates = normalizeExcludedDatesOverride(currentExcludedDates);
const excludedByNode = excludedDatesByNodeOverrideRef.current || {}; const excludedByNode = excludedDatesByNodeOverrideRef.current || {};
const isBackward = timelineDirection === 'backward'; const isBackward = timelineDirection === 'backward';
@ -3159,7 +3307,6 @@ export default function App() {
selectedLabels: currentSelectedLabels, selectedLabels: currentSelectedLabels,
expectedDate: currentExpectedDate, expectedDate: currentExpectedDate,
startTime: currentStartTime, startTime: currentStartTime,
excludedDates: currentExcludedDates,
}, },
showUI, showUI,
}; };
@ -3269,20 +3416,20 @@ export default function App() {
for (let i = 0; i < nodesToProcess.length; i++) { for (let i = 0; i < nodesToProcess.length; i++) {
const processNode = nodesToProcess[i]; const processNode = nodesToProcess[i];
const nodeKey = buildExcludedDatesNodeKey( const keyCandidates = getExcludedDatesNodeKeyCandidates(processNode, i);
processNode?.nodeName, let overrideList: string[] | undefined = undefined;
processNode?.processOrder, for (const candidateKey of keyCandidates) {
i, if (Object.prototype.hasOwnProperty.call(excludedByNode, candidateKey)) {
processNode?.processGroupInstanceId || processNode?.processGroupInstanceName overrideList = excludedByNode[candidateKey];
); break;
const overrideList = Object.prototype.hasOwnProperty.call(excludedByNode, nodeKey) }
? excludedByNode[nodeKey] }
: undefined;
const baseList = Array.isArray(processNode?.excludedDates) ? processNode.excludedDates : []; const baseList = Array.isArray(processNode?.excludedDates) ? processNode.excludedDates : [];
const selectedList = Array.isArray(overrideList) ? overrideList : baseList; const oldExcludedDates = normalizeExcludedDatesOverride(Array.isArray(overrideList) ? overrideList : []);
const newRuleExcludedDates = normalizeExcludedDatesOverride(baseList);
const nodeExcludedDates = Array.from(new Set([ const nodeExcludedDates = Array.from(new Set([
...normalizeExcludedDatesOverride(selectedList), ...oldExcludedDates,
...globalExcludedDates, ...newRuleExcludedDates,
])).filter(Boolean); ])).filter(Boolean);
let timelineValue = null; let timelineValue = null;
let matchedTimelineRecord = null; let matchedTimelineRecord = null;
@ -3718,7 +3865,10 @@ export default function App() {
} }
} }
const nextAdjustments = remapTimelineAdjustmentsToNewResults(timelineResults, results); const prevResultsForRemap = Array.isArray(overrideData?.prevResultsForAdjustmentRemap)
? overrideData!.prevResultsForAdjustmentRemap!
: timelineResults;
const nextAdjustments = remapTimelineAdjustmentsToNewResults(prevResultsForRemap, results);
setTimelineAdjustments(nextAdjustments); setTimelineAdjustments(nextAdjustments);
pendingRecalculateAfterCalculateAdjustmentsRef.current = nextAdjustments; pendingRecalculateAfterCalculateAdjustmentsRef.current = nextAdjustments;
pendingRecalculateAfterCalculateRef.current = true; pendingRecalculateAfterCalculateRef.current = true;
@ -3797,10 +3947,17 @@ export default function App() {
// 获取重新计算后的时间线结果(不更新状态,逻辑对齐页面的重算口径) // 获取重新计算后的时间线结果(不更新状态,逻辑对齐页面的重算口径)
const getRecalculatedTimeline = ( const getRecalculatedTimeline = (
adjustments: Record<string, number>, adjustments: Record<string, number>,
opts?: { ignoreActualCompletionDates?: boolean; actualCompletionDatesOverride?: { [key: number]: Date | null } } opts?: {
ignoreActualCompletionDates?: boolean;
actualCompletionDatesOverride?: { [key: number]: Date | null };
baseResultsOverride?: any[];
startTimeOverride?: Date | null;
}
) => { ) => {
const updatedResults = [...timelineResults]; const baseResults = Array.isArray(opts?.baseResultsOverride) ? opts!.baseResultsOverride! : timelineResults;
let cumulativeStartTime = startTime ? new Date(startTime) : new Date(); // 从起始时间开始 const updatedResults = [...baseResults];
const baseStartTime = opts?.startTimeOverride ?? startTime;
let cumulativeStartTime = baseStartTime ? new Date(baseStartTime) : new Date(); // 从起始时间开始
for (let i = 0; i < updatedResults.length; i++) { for (let i = 0; i < updatedResults.length; i++) {
const result = updatedResults[i]; const result = updatedResults[i];
@ -4530,7 +4687,7 @@ export default function App() {
timelineResults: any[], timelineResults: any[],
processRecordIds: string[], processRecordIds: string[],
timelineAdjustments: Record<string, number> = {}, timelineAdjustments: Record<string, number> = {},
overrides?: { foreignId?: string; style?: string; color?: string; expectedDate?: Date | null; startTime?: Date | null; selectedLabels?: {[key: string]: string | string[]}; baseBufferDays?: number } overrides?: { foreignId?: string; style?: string; color?: string; expectedDate?: Date | null; startTime?: Date | null; selectedLabels?: {[key: string]: string | string[]}; baseBufferDays?: number; recordIdsTextOverride?: string; baseVersionNumber?: number | null; forceMode?: 'generate' | 'adjust' | null }
) => { ) => {
let recordCells: any[] | undefined; let recordCells: any[] | undefined;
try { try {
@ -4722,10 +4879,15 @@ export default function App() {
// 创建当前时间戳 // 创建当前时间戳
const currentTime = new Date().getTime(); const currentTime = new Date().getTime();
const effectiveMode = overrides?.forceMode ?? mode;
// 计算版本号(数字)并格式化货期调整信息 // 计算版本号(数字)并格式化货期调整信息
let versionNumber = 1; let versionNumber = 1;
if (mode === 'adjust' && currentVersionNumber !== null) { const overrideBaseVersion = Number(overrides?.baseVersionNumber);
const hasOverrideBaseVersion = Number.isFinite(overrideBaseVersion);
if (hasOverrideBaseVersion) {
versionNumber = overrideBaseVersion + 1;
} else if (effectiveMode === 'adjust' && currentVersionNumber !== null) {
versionNumber = currentVersionNumber + 1; versionNumber = currentVersionNumber + 1;
} }
@ -4779,7 +4941,7 @@ export default function App() {
styleText, styleText,
colorText, colorText,
text2, text2,
mode, mode: effectiveMode,
timelineDirection, timelineDirection,
excludedDatesOverride, excludedDatesOverride,
excludedDatesByNodeOverride: excludedDatesByNodeOverrideRef.current || excludedDatesByNodeOverride, excludedDatesByNodeOverride: excludedDatesByNodeOverrideRef.current || excludedDatesByNodeOverride,
@ -4912,9 +5074,11 @@ export default function App() {
// 使用createCell方法创建各个字段的Cell // 使用createCell方法创建各个字段的Cell
const startTimestamp = currentStartTime ? currentStartTime.getTime() : currentTime; const startTimestamp = currentStartTime ? currentStartTime.getTime() : currentTime;
const recordIdsText = (restoredRecordIdsText && restoredRecordIdsText.trim() !== '') const recordIdsText = (overrides?.recordIdsTextOverride && overrides.recordIdsTextOverride.trim() !== '')
? restoredRecordIdsText.trim() ? overrides.recordIdsTextOverride.trim()
: ''; : ((restoredRecordIdsText && restoredRecordIdsText.trim() !== '')
? restoredRecordIdsText.trim()
: '');
const [ const [
foreignIdCell, foreignIdCell,
@ -5470,30 +5634,13 @@ export default function App() {
const styleText = getText('styleText'); const styleText = getText('styleText');
const colorText = getText('colorText'); const colorText = getText('colorText');
const rawStart = f[nameToId.get('startTimestamp') || '']; const rawStart = f[nameToId.get('startTimestamp') || ''];
const rawExpected = f[nameToId.get('expectedDateTimestamp') || '']; const rawExpected =
f[BATCH_CUSTOMER_EXPECTED_DATE_FIELD_ID] ??
f[nameToId.get('customerExpectedDate') || ''] ??
f[nameToId.get('expectedDateTimestamp') || ''];
const rawBufferDays = f[nameToId.get('缓冲期') || BATCH_BUFFER_FIELD_ID || '']; const rawBufferDays = f[nameToId.get('缓冲期') || BATCH_BUFFER_FIELD_ID || ''];
let startDate: Date | null = null; let startDate: Date | null = parseBatchDateValue(rawStart);
let expectedDateObj: Date | null = null; let expectedDateObj: Date | null = parseBatchDateValue(rawExpected);
if (typeof rawStart === 'number') startDate = new Date(rawStart);
else if (Array.isArray(rawStart) && rawStart.length > 0) {
const item = rawStart[0];
if (typeof item === 'number') startDate = new Date(item);
else startDate = parseDate(extractText(rawStart));
} else if (typeof rawStart === 'string') startDate = parseDate(rawStart);
else if (rawStart && typeof rawStart === 'object') {
const v = (rawStart as any).value || (rawStart as any).text || (rawStart as any).name || '';
if (typeof v === 'number') startDate = new Date(v); else startDate = parseDate(v);
}
if (typeof rawExpected === 'number') expectedDateObj = new Date(rawExpected);
else if (Array.isArray(rawExpected) && rawExpected.length > 0) {
const item = rawExpected[0];
if (typeof item === 'number') expectedDateObj = new Date(item);
else expectedDateObj = parseDate(extractText(rawExpected));
} else if (typeof rawExpected === 'string') expectedDateObj = parseDate(rawExpected);
else if (rawExpected && typeof rawExpected === 'object') {
const v = (rawExpected as any).value || (rawExpected as any).text || (rawExpected as any).name || '';
if (typeof v === 'number') expectedDateObj = new Date(v); else expectedDateObj = parseDate(v);
}
const parseBufferDays = (raw: any): number | null => { const parseBufferDays = (raw: any): number | null => {
if (raw == null) return null; if (raw == null) return null;
@ -5516,45 +5663,6 @@ export default function App() {
} }
return null; return null;
}; };
const normalizeExcludedDates = (raw: any): string[] => {
const out: string[] = [];
const pushToken = (token: any) => {
if (token == null) return;
if (typeof token === 'number' && Number.isFinite(token)) {
const d = new Date(token);
if (!isNaN(d.getTime())) out.push(format(d, 'yyyy-MM-dd'));
return;
}
const text = (typeof token === 'string') ? token : extractText(token);
if (!text) return;
const parts = String(text).split(/[,;;、\n\r\t ]+/).map(s => s.trim()).filter(Boolean);
for (const p of parts) {
const head = p.slice(0, 10);
if (/^\d{4}-\d{2}-\d{2}$/.test(head)) {
out.push(head);
continue;
}
const parsed = parseDate(p);
if (parsed && !isNaN(parsed.getTime())) out.push(format(parsed, 'yyyy-MM-dd'));
}
};
const walk = (v: any) => {
if (Array.isArray(v)) {
v.forEach(walk);
return;
}
if (v && typeof v === 'object' && Array.isArray((v as any).value)) {
walk((v as any).value);
return;
}
pushToken(v);
};
walk(raw);
return Array.from(new Set(out)).filter(Boolean);
};
const batchExcludedDates = normalizeExcludedDates(f['fldQNxtHnd']);
const splitVals = (s: string) => (s || '').split(/[,,、]+/).map(v => v.trim()).filter(Boolean); const splitVals = (s: string) => (s || '').split(/[,,、]+/).map(v => v.trim()).filter(Boolean);
const normalizeToStringList = (raw: any): string[] => { const normalizeToStringList = (raw: any): string[] => {
if (!raw) return []; if (!raw) return [];
@ -5580,32 +5688,96 @@ export default function App() {
else labels[key] = list.join(''); else labels[key] = list.join('');
} }
} }
{ const sourceDeliveryRecordId = extractText(
const requiredLabelKeys = Array.from({ length: 12 }, (_, k) => `标签${k + 1}`); f[BATCH_SOURCE_DELIVERY_RECORD_ID_FIELD_ID] ??
const missing = requiredLabelKeys.filter(k => { f[nameToId.get('sourceDeliveryRecordId') || '']
const val = (labels as any)[k]; ).trim();
if (Array.isArray(val)) return val.length === 0;
return !(typeof val === 'string' && val.trim().length > 0);
});
if (missing.length > 0) {
setBatchProcessedCount(p => p + 1);
setBatchFailureCount(fCount => fCount + 1);
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: `标签不完整:${missing.join('、')}` }]);
continue;
}
}
setBatchCurrentRowInfo({ index: displayIndex, foreignId: foreignId || '', style: styleText || '', color: colorText || '' }); setBatchCurrentRowInfo({ index: displayIndex, foreignId: foreignId || '', style: styleText || '', color: colorText || '' });
setCurrentForeignId(foreignId || '');
setCurrentStyleText(styleText || '');
setCurrentColorText(colorText || '');
setExpectedDate(expectedDateObj || null);
setStartTime(startDate || null);
setSelectedLabels(labels);
try { try {
const results = await handleCalculateTimeline(true, { selectedLabels: labels, expectedDate: expectedDateObj || null, startTime: startDate || null, excludedDates: batchExcludedDates }, false); let labelsToUse: { [key: string]: string | string[] } = labels;
let expectedDateToUse: Date | null = expectedDateObj || null;
let startTimeToUse: Date | null = startDate || null;
let foreignIdToUse = foreignId || '';
let styleToUse = styleText || '';
let colorToUse = colorText || '';
let adjustmentsToUse: Record<string, number> = {};
let prevResultsForRemap: any[] | undefined = undefined;
let sourceRecordIdsText = '';
let actualCompletionDatesForWrite: { [key: number]: Date | null } = {};
let baseVersionNumberToUse: number | null = null;
if (batchMode === 'adjust') {
if (!sourceDeliveryRecordId) {
throw new Error('缺少sourceDeliveryRecordId');
}
const simulation = await getBatchSimulationStateFromSnapshot(sourceDeliveryRecordId);
setMode('adjust');
setTimelineResults(simulation.timelineResults);
setTimelineAdjustments(simulation.timelineAdjustments || {});
setSelectedLabels(simulation.selectedLabels || {});
setExpectedDate(simulation.expectedDate || null);
setStartTime(simulation.startTime || null);
setCurrentForeignId(simulation.foreignId || '');
setCurrentStyleText(simulation.styleText || '');
setCurrentColorText(simulation.colorText || '');
setCurrentText2(simulation.text2 || '');
setCurrentVersionNumber(simulation.versionNumber);
setActualCompletionDates(simulation.actualCompletionDates || {});
setExcludedDatesOverride(simulation.excludedDatesOverride || []);
excludedDatesByNodeOverrideRef.current = simulation.excludedDatesByNodeOverride || {};
setExcludedDatesByNodeOverride(simulation.excludedDatesByNodeOverride || {});
pendingRecalculateAfterCalculateRef.current = false;
pendingRecalculateAfterCalculateAdjustmentsRef.current = null;
labelsToUse = { ...(simulation.selectedLabels || {}) };
for (const [k, v] of Object.entries(labels || {})) {
if (Array.isArray(v) && v.length > 0) labelsToUse[k] = v;
if (typeof v === 'string' && v.trim() !== '') labelsToUse[k] = v.trim();
}
expectedDateToUse = expectedDateObj || simulation.expectedDate || null;
startTimeToUse = simulation.startTime || null;
foreignIdToUse = simulation.foreignId || foreignIdToUse;
styleToUse = simulation.styleText || styleToUse;
colorToUse = simulation.colorText || colorToUse;
adjustmentsToUse = simulation.timelineAdjustments || {};
prevResultsForRemap = simulation.timelineResults || [];
sourceRecordIdsText = simulation.sourceRecordIdsText || '';
actualCompletionDatesForWrite = simulation.actualCompletionDates || {};
baseVersionNumberToUse = Number.isFinite(Number(simulation.versionNumber))
? Number(simulation.versionNumber)
: null;
} else {
const requiredLabelKeys = Array.from({ length: 12 }, (_, k) => `标签${k + 1}`);
const missing = requiredLabelKeys.filter(k => {
const val = (labelsToUse as any)[k];
if (Array.isArray(val)) return val.length === 0;
return !(typeof val === 'string' && val.trim().length > 0);
});
if (missing.length > 0) {
throw new Error(`标签不完整:${missing.join('、')}`);
}
setCurrentForeignId(foreignIdToUse);
setCurrentStyleText(styleToUse);
setCurrentColorText(colorToUse);
setExpectedDate(expectedDateToUse);
setStartTime(startTimeToUse);
setSelectedLabels(labelsToUse);
}
const results = await handleCalculateTimeline(
true,
{
selectedLabels: labelsToUse,
expectedDate: expectedDateToUse,
startTime: startTimeToUse,
prevResultsForAdjustmentRemap: prevResultsForRemap
},
false
);
if (results && results.length > 0) { if (results && results.length > 0) {
let effectiveExpectedDate = expectedDateObj || null; let resultsToWrite = results;
if (!effectiveExpectedDate) { let effectiveExpectedDate = expectedDateToUse;
if (!effectiveExpectedDate && batchMode === 'generate') {
const bufferDays = parseBufferDays(rawBufferDays); const bufferDays = parseBufferDays(rawBufferDays);
const fallbackDays = Number.isFinite(bufferDays as number) ? (bufferDays as number) : 14; const fallbackDays = Number.isFinite(bufferDays as number) ? (bufferDays as number) : 14;
const autoExpected = computeExpectedDateByBufferDays(results, fallbackDays, completionDateAdjustment); const autoExpected = computeExpectedDateByBufferDays(results, fallbackDays, completionDateAdjustment);
@ -5614,25 +5786,43 @@ export default function App() {
setExpectedDate(autoExpected); setExpectedDate(autoExpected);
} }
} }
const processRecordIds = await writeToProcessDataTable(results, { foreignId, style: styleText, color: colorText }); if (batchMode === 'adjust' && Array.isArray(prevResultsForRemap) && prevResultsForRemap.length > 0) {
adjustmentsToUse = remapTimelineAdjustmentsToNewResults(prevResultsForRemap, results);
resultsToWrite = getRecalculatedTimeline(adjustmentsToUse, {
actualCompletionDatesOverride: actualCompletionDatesForWrite,
baseResultsOverride: results,
startTimeOverride: startTimeToUse
});
} else {
adjustmentsToUse = {};
}
const processRecordIds = await writeToProcessDataTable(resultsToWrite, { foreignId: foreignIdToUse, style: styleToUse, color: colorToUse });
const deliveryRecordId = await writeToDeliveryRecordTable( const deliveryRecordId = await writeToDeliveryRecordTable(
results, resultsToWrite,
processRecordIds, processRecordIds,
{}, adjustmentsToUse,
{ {
foreignId, foreignId: foreignIdToUse,
style: styleText, style: styleToUse,
color: colorText, color: colorToUse,
expectedDate: effectiveExpectedDate, expectedDate: effectiveExpectedDate,
startTime: startDate || null, startTime: startTimeToUse,
selectedLabels: labels selectedLabels: labelsToUse,
recordIdsTextOverride: sourceRecordIdsText,
baseVersionNumber: baseVersionNumberToUse,
forceMode: batchMode === 'adjust' ? 'adjust' : 'generate'
} }
); );
try { try {
const candidateNames = ['状态','record_id','记录ID','货期记录ID','deliveryRecordId']; const candidateNames = ['resultDeliveryRecordId', '状态', 'record_id', '记录ID', '货期记录ID', 'deliveryRecordId'];
let statusFieldId = ''; let statusFieldId = BATCH_RESULT_DELIVERY_RECORD_ID_FIELD_ID;
for (const nm of candidateNames) { const id = nameToId.get(nm) || ''; if (id) { statusFieldId = id; break; } } for (const nm of candidateNames) {
if (!statusFieldId) statusFieldId = 'fldKTpPL9s'; const id = nameToId.get(nm) || '';
if (id) {
statusFieldId = id;
break;
}
}
const rowRecordId = (row.id || (row as any).recordId || (row as any)._id || (row as any).record_id); const rowRecordId = (row.id || (row as any).recordId || (row as any)._id || (row as any).record_id);
const deliveryRecordIdStr = typeof deliveryRecordId === 'string' const deliveryRecordIdStr = typeof deliveryRecordId === 'string'
? deliveryRecordId ? deliveryRecordId
@ -5656,15 +5846,15 @@ export default function App() {
} }
} catch (e2) { } catch (e2) {
throw e2; throw e2;
}
} }
} setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignIdToUse || '', status: 'success', message: `记录ID: ${deliveryRecordIdStr}` }]);
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'success', message: `记录ID: ${deliveryRecordIdStr}` }]);
} else { } else {
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: '未找到状态字段或记录ID为空' }]); setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignIdToUse || '', status: 'failed', message: '未找到结果字段或记录ID为空' }]);
} }
} catch (statusErr: any) { } catch (statusErr: any) {
console.warn('回写批量状态字段失败', statusErr); console.warn('回写批量状态字段失败', statusErr);
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: `状态写入失败: ${statusErr?.message || '未知错误'}` }]); setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignIdToUse || '', status: 'failed', message: `结果回写失败: ${statusErr?.message || '未知错误'}` }]);
} }
processed++; processed++;
setBatchProcessedCount(p => p + 1); setBatchProcessedCount(p => p + 1);
@ -5682,7 +5872,8 @@ export default function App() {
} }
if (bitable.ui.showToast) { if (bitable.ui.showToast) {
const aborted = batchAbortRef.current; const aborted = batchAbortRef.current;
await bitable.ui.showToast({ toastType: ToastType.success, message: aborted ? `批量已中止,已处理 ${processed} 条记录` : `批量生成完成,共处理 ${processed} 条记录` }); const finishedText = batchMode === 'adjust' ? '批量调整完成' : '批量生成完成';
await bitable.ui.showToast({ toastType: ToastType.success, message: aborted ? `批量已中止,已处理 ${processed} 条记录` : `${finishedText},共处理 ${processed} 条记录` });
} }
} catch (error) { } catch (error) {
console.error('批量处理失败:', error); console.error('批量处理失败:', error);
@ -6329,8 +6520,8 @@ const omsVersionColumns = [
background: 'linear-gradient(180deg, #fff, #f9fbff)' background: 'linear-gradient(180deg, #fff, #f9fbff)'
}} }}
> >
<Title heading={3} style={{ marginBottom: 8 }}></Title> <Title heading={3} style={{ marginBottom: 8 }}>/</Title>
<Text type='tertiary'></Text> <Text type='tertiary'></Text>
<div style={{ marginTop: 16 }}> <div style={{ marginTop: 16 }}>
<Button type='primary' theme='solid' size='large' style={{ width: '100%' }} onClick={openBatchModal}></Button> <Button type='primary' theme='solid' size='large' style={{ width: '100%' }} onClick={openBatchModal}></Button>
</div> </div>
@ -6339,7 +6530,7 @@ const omsVersionColumns = [
</Modal> </Modal>
<Modal <Modal
title="批量生成" title={batchMode === 'adjust' ? '批量调整' : '批量生成'}
visible={batchModalVisible} visible={batchModalVisible}
onCancel={async () => { batchAbortRef.current = true; setBatchModalVisible(false); }} onCancel={async () => { batchAbortRef.current = true; setBatchModalVisible(false); }}
footer={null} footer={null}
@ -6348,13 +6539,24 @@ const omsVersionColumns = [
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Card style={{ padding: 12 }}> <Card style={{ padding: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<Text></Text>
<Select
value={batchMode}
onChange={(v) => setBatchMode(v as 'generate' | 'adjust')}
disabled={batchLoading}
style={{ width: 140 }}
optionList={[
{ label: '批量生成', value: 'generate' },
{ label: '批量调整', value: 'adjust' }
]}
/>
<Text></Text> <Text></Text>
<InputNumber min={1} value={batchStartRow} onChange={(v) => setBatchStartRow(typeof v === 'number' ? v : 1)} style={{ width: 120 }} disabled={batchLoading} /> <InputNumber min={1} value={batchStartRow} onChange={(v) => setBatchStartRow(typeof v === 'number' ? v : 1)} style={{ width: 120 }} disabled={batchLoading} />
<Text></Text> <Text></Text>
<InputNumber min={1} value={batchEndRow} onChange={(v) => setBatchEndRow(typeof v === 'number' ? v : 1)} style={{ width: 120 }} disabled={batchLoading} /> <InputNumber min={1} value={batchEndRow} onChange={(v) => setBatchEndRow(typeof v === 'number' ? v : 1)} style={{ width: 120 }} disabled={batchLoading} />
<Text type='tertiary'>{batchTotalRows}</Text> <Text type='tertiary'>{batchTotalRows}</Text>
<Space> <Space>
<Button loading={batchLoading} type='primary' theme='solid' onClick={() => handleBatchProcess({ start: batchStartRow, end: batchEndRow })}></Button> <Button loading={batchLoading} type='primary' theme='solid' onClick={() => handleBatchProcess({ start: batchStartRow, end: batchEndRow })}>{batchMode === 'adjust' ? '开始调整' : '开始生成'}</Button>
<Button type='danger' onClick={() => { batchAbortRef.current = true; }} disabled={!batchLoading}></Button> <Button type='danger' onClick={() => { batchAbortRef.current = true; }} disabled={!batchLoading}></Button>
<Button onClick={() => setBatchModalVisible(false)} disabled={batchLoading}></Button> <Button onClick={() => setBatchModalVisible(false)} disabled={batchLoading}></Button>
</Space> </Space>