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 [batchTotalRows, setBatchTotalRows] = useState<number>(0);
const [batchLoading, setBatchLoading] = useState(false);
const [batchMode, setBatchMode] = useState<'generate' | 'adjust'>('generate');
const [batchProcessedCount, setBatchProcessedCount] = useState<number>(0);
const [batchProcessingTotal, setBatchProcessingTotal] = 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_LABEL12_FIELD_ID = 'fldnRlMeaD';
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) => {
try {
@ -468,6 +472,115 @@ export default function App() {
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';
};
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) => {
return buildExcludedDatesNodeKey(
node?.nodeName,
@ -832,16 +960,35 @@ export default function App() {
const buildExcludedDatesByNodeFromTimeline = (results: any[]): 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++) {
const r = results[i];
const key = buildExcludedDatesNodeKey(
const keyWithInstance = buildExcludedDatesNodeKey(
r?.nodeName,
r?.processOrder,
i,
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 : [];
map[key] = normalizeExcludedDatesOverride(arr);
const normalized = normalizeExcludedDatesOverride(arr);
assignExcludedDates(keyWithInstance, normalized);
assignExcludedDates(keyWithoutInstance, normalized);
assignExcludedDates(keyByName, normalized);
}
return map;
};
@ -861,6 +1008,18 @@ export default function App() {
};
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[] = [
snapshot?.excludedDatesByNodeOverride,
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 = {};
setExcludedDatesByNodeOverride({});
};
@ -2775,7 +2924,8 @@ export default function App() {
selectedLabels?: {[key: string]: string | string[]},
expectedDate?: Date | null,
startTime?: Date | null,
excludedDates?: string[]
excludedDates?: string[],
prevResultsForAdjustmentRemap?: any[]
},
showUI: boolean = true // 新增参数控制是否显示UI
) => {
@ -2785,8 +2935,6 @@ export default function App() {
const currentSelectedLabels = overrideData?.selectedLabels || selectedLabels;
const currentExpectedDate = overrideData?.expectedDate || expectedDate;
const currentStartTime = overrideData?.startTime || startTime;
const currentExcludedDates = Array.isArray(overrideData?.excludedDates) ? overrideData!.excludedDates : [];
const globalExcludedDates = normalizeExcludedDatesOverride(currentExcludedDates);
const excludedByNode = excludedDatesByNodeOverrideRef.current || {};
const isBackward = timelineDirection === 'backward';
@ -3159,7 +3307,6 @@ export default function App() {
selectedLabels: currentSelectedLabels,
expectedDate: currentExpectedDate,
startTime: currentStartTime,
excludedDates: currentExcludedDates,
},
showUI,
};
@ -3269,20 +3416,20 @@ export default function App() {
for (let i = 0; i < nodesToProcess.length; i++) {
const processNode = nodesToProcess[i];
const nodeKey = buildExcludedDatesNodeKey(
processNode?.nodeName,
processNode?.processOrder,
i,
processNode?.processGroupInstanceId || processNode?.processGroupInstanceName
);
const overrideList = Object.prototype.hasOwnProperty.call(excludedByNode, nodeKey)
? excludedByNode[nodeKey]
: undefined;
const keyCandidates = getExcludedDatesNodeKeyCandidates(processNode, i);
let overrideList: string[] | undefined = undefined;
for (const candidateKey of keyCandidates) {
if (Object.prototype.hasOwnProperty.call(excludedByNode, candidateKey)) {
overrideList = excludedByNode[candidateKey];
break;
}
}
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([
...normalizeExcludedDatesOverride(selectedList),
...globalExcludedDates,
...oldExcludedDates,
...newRuleExcludedDates,
])).filter(Boolean);
let timelineValue = 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);
pendingRecalculateAfterCalculateAdjustmentsRef.current = nextAdjustments;
pendingRecalculateAfterCalculateRef.current = true;
@ -3797,10 +3947,17 @@ export default function App() {
// 获取重新计算后的时间线结果(不更新状态,逻辑对齐页面的重算口径)
const getRecalculatedTimeline = (
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];
let cumulativeStartTime = startTime ? new Date(startTime) : new Date(); // 从起始时间开始
const baseResults = Array.isArray(opts?.baseResultsOverride) ? opts!.baseResultsOverride! : timelineResults;
const updatedResults = [...baseResults];
const baseStartTime = opts?.startTimeOverride ?? startTime;
let cumulativeStartTime = baseStartTime ? new Date(baseStartTime) : new Date(); // 从起始时间开始
for (let i = 0; i < updatedResults.length; i++) {
const result = updatedResults[i];
@ -4530,7 +4687,7 @@ export default function App() {
timelineResults: any[],
processRecordIds: string[],
timelineAdjustments: Record<string, number> = {},
overrides?: { foreignId?: string; style?: string; color?: string; expectedDate?: Date | null; startTime?: Date | null; selectedLabels?: {[key: string]: string | string[]}; baseBufferDays?: number }
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;
try {
@ -4722,10 +4879,15 @@ export default function App() {
// 创建当前时间戳
const currentTime = new Date().getTime();
const effectiveMode = overrides?.forceMode ?? mode;
// 计算版本号(数字)并格式化货期调整信息
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;
}
@ -4779,7 +4941,7 @@ export default function App() {
styleText,
colorText,
text2,
mode,
mode: effectiveMode,
timelineDirection,
excludedDatesOverride,
excludedDatesByNodeOverride: excludedDatesByNodeOverrideRef.current || excludedDatesByNodeOverride,
@ -4912,9 +5074,11 @@ export default function App() {
// 使用createCell方法创建各个字段的Cell
const startTimestamp = currentStartTime ? currentStartTime.getTime() : currentTime;
const recordIdsText = (restoredRecordIdsText && restoredRecordIdsText.trim() !== '')
? restoredRecordIdsText.trim()
: '';
const recordIdsText = (overrides?.recordIdsTextOverride && overrides.recordIdsTextOverride.trim() !== '')
? overrides.recordIdsTextOverride.trim()
: ((restoredRecordIdsText && restoredRecordIdsText.trim() !== '')
? restoredRecordIdsText.trim()
: '');
const [
foreignIdCell,
@ -5470,30 +5634,13 @@ export default function App() {
const styleText = getText('styleText');
const colorText = getText('colorText');
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 || ''];
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);
}
let startDate: Date | null = parseBatchDateValue(rawStart);
let expectedDateObj: Date | null = parseBatchDateValue(rawExpected);
const parseBufferDays = (raw: any): number | null => {
if (raw == null) return null;
@ -5516,45 +5663,6 @@ export default function App() {
}
return null;
};
const normalizeExcludedDates = (raw: any): string[] => {
const out: string[] = [];
const pushToken = (token: any) => {
if (token == null) return;
if (typeof token === 'number' && Number.isFinite(token)) {
const d = new Date(token);
if (!isNaN(d.getTime())) out.push(format(d, 'yyyy-MM-dd'));
return;
}
const text = (typeof token === 'string') ? token : extractText(token);
if (!text) return;
const parts = String(text).split(/[,;;、\n\r\t ]+/).map(s => s.trim()).filter(Boolean);
for (const p of parts) {
const head = p.slice(0, 10);
if (/^\d{4}-\d{2}-\d{2}$/.test(head)) {
out.push(head);
continue;
}
const parsed = parseDate(p);
if (parsed && !isNaN(parsed.getTime())) out.push(format(parsed, 'yyyy-MM-dd'));
}
};
const walk = (v: any) => {
if (Array.isArray(v)) {
v.forEach(walk);
return;
}
if (v && typeof v === 'object' && Array.isArray((v as any).value)) {
walk((v as any).value);
return;
}
pushToken(v);
};
walk(raw);
return Array.from(new Set(out)).filter(Boolean);
};
const batchExcludedDates = normalizeExcludedDates(f['fldQNxtHnd']);
const splitVals = (s: string) => (s || '').split(/[,,、]+/).map(v => v.trim()).filter(Boolean);
const normalizeToStringList = (raw: any): string[] => {
if (!raw) return [];
@ -5580,32 +5688,96 @@ export default function App() {
else labels[key] = list.join('');
}
}
{
const requiredLabelKeys = Array.from({ length: 12 }, (_, k) => `标签${k + 1}`);
const missing = requiredLabelKeys.filter(k => {
const val = (labels as any)[k];
if (Array.isArray(val)) return val.length === 0;
return !(typeof val === 'string' && val.trim().length > 0);
});
if (missing.length > 0) {
setBatchProcessedCount(p => p + 1);
setBatchFailureCount(fCount => fCount + 1);
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: `标签不完整:${missing.join('、')}` }]);
continue;
}
}
const sourceDeliveryRecordId = extractText(
f[BATCH_SOURCE_DELIVERY_RECORD_ID_FIELD_ID] ??
f[nameToId.get('sourceDeliveryRecordId') || '']
).trim();
setBatchCurrentRowInfo({ index: displayIndex, foreignId: foreignId || '', style: styleText || '', color: colorText || '' });
setCurrentForeignId(foreignId || '');
setCurrentStyleText(styleText || '');
setCurrentColorText(colorText || '');
setExpectedDate(expectedDateObj || null);
setStartTime(startDate || null);
setSelectedLabels(labels);
try {
const results = await handleCalculateTimeline(true, { selectedLabels: labels, expectedDate: expectedDateObj || null, startTime: startDate || null, excludedDates: batchExcludedDates }, false);
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) {
let effectiveExpectedDate = expectedDateObj || null;
if (!effectiveExpectedDate) {
let resultsToWrite = results;
let effectiveExpectedDate = expectedDateToUse;
if (!effectiveExpectedDate && batchMode === 'generate') {
const bufferDays = parseBufferDays(rawBufferDays);
const fallbackDays = Number.isFinite(bufferDays as number) ? (bufferDays as number) : 14;
const autoExpected = computeExpectedDateByBufferDays(results, fallbackDays, completionDateAdjustment);
@ -5614,25 +5786,43 @@ export default function App() {
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(
results,
resultsToWrite,
processRecordIds,
{},
adjustmentsToUse,
{
foreignId,
style: styleText,
color: colorText,
foreignId: foreignIdToUse,
style: styleToUse,
color: colorToUse,
expectedDate: effectiveExpectedDate,
startTime: startDate || null,
selectedLabels: labels
startTime: startTimeToUse,
selectedLabels: labelsToUse,
recordIdsTextOverride: sourceRecordIdsText,
baseVersionNumber: baseVersionNumberToUse,
forceMode: batchMode === 'adjust' ? 'adjust' : 'generate'
}
);
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 candidateNames = ['resultDeliveryRecordId', '状态', 'record_id', '记录ID', '货期记录ID', 'deliveryRecordId'];
let statusFieldId = BATCH_RESULT_DELIVERY_RECORD_ID_FIELD_ID;
for (const nm of candidateNames) {
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 deliveryRecordIdStr = typeof deliveryRecordId === 'string'
? deliveryRecordId
@ -5656,15 +5846,15 @@ export default function App() {
}
} catch (e2) {
throw e2;
}
}
}
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'success', message: `记录ID: ${deliveryRecordIdStr}` }]);
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignIdToUse || '', status: 'success', message: `记录ID: ${deliveryRecordIdStr}` }]);
} 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) {
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++;
setBatchProcessedCount(p => p + 1);
@ -5682,7 +5872,8 @@ export default function App() {
}
if (bitable.ui.showToast) {
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) {
console.error('批量处理失败:', error);
@ -6329,8 +6520,8 @@ const omsVersionColumns = [
background: 'linear-gradient(180deg, #fff, #f9fbff)'
}}
>
<Title heading={3} style={{ marginBottom: 8 }}></Title>
<Text type='tertiary'></Text>
<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>
@ -6339,7 +6530,7 @@ const omsVersionColumns = [
</Modal>
<Modal
title="批量生成"
title={batchMode === 'adjust' ? '批量调整' : '批量生成'}
visible={batchModalVisible}
onCancel={async () => { batchAbortRef.current = true; setBatchModalVisible(false); }}
footer={null}
@ -6348,13 +6539,24 @@ const omsVersionColumns = [
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Card style={{ padding: 12 }}>
<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>
<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 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 onClick={() => setBatchModalVisible(false)} disabled={batchLoading}></Button>
</Space>