3
3
This commit is contained in:
490
src/App.tsx
490
src/App.tsx
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user