10
This commit is contained in:
2025-12-18 19:03:33 +08:00
parent 2cbfab3da4
commit 2e00d56c3a

View File

@ -1,7 +1,7 @@
import { bitable, FieldType, ToastType } from '@lark-base-open/js-sdk';
import { Button, Typography, List, Card, Space, Divider, Spin, Table, Select, Modal, DatePicker, InputNumber, Input, Progress } from '@douyinfe/semi-ui';
import { useState, useEffect, useRef } from 'react';
import { addDays, format } from 'date-fns';
import { addDays, format, differenceInCalendarDays } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import { executePricingQuery, executeSecondaryProcessQuery, executePricingDetailsQuery } from './services/apiService';
@ -54,6 +54,14 @@ export default function App() {
const [expectedDate, setExpectedDate] = useState<Date | null>(null);
// 起始时间状态(从货期记录表获取,新记录则使用当前时间)
const [startTime, setStartTime] = useState<Date | null>(null);
const [timelineDirection, setTimelineDirection] = useState<'forward' | 'backward'>('forward');
const [calculatedRequiredStartTime, setCalculatedRequiredStartTime] = useState<Date | null>(null);
const [allocationVisible, setAllocationVisible] = useState(false);
const [allocationMode, setAllocationMode] = useState<'auto' | 'manual'>('auto');
const [allocationExtraDays, setAllocationExtraDays] = useState<number>(0);
const [allocationDraft, setAllocationDraft] = useState<{[key: number]: number}>({});
const [allocationNodesSnapshot, setAllocationNodesSnapshot] = useState<any[]>([]);
const [allocationExcluded, setAllocationExcluded] = useState<{[key: number]: boolean}>({});
// 预览相关状态(已移除未使用的 previewLoading 状态)
@ -121,6 +129,12 @@ export default function App() {
setSelectedLabels({});
setExpectedDate(null);
setStartTime(null);
setCalculatedRequiredStartTime(null);
setAllocationVisible(false);
setAllocationExtraDays(0);
setAllocationDraft({});
setAllocationExcluded({});
setAllocationNodesSnapshot([]);
setTimelineVisible(false);
setTimelineResults([]);
setTimelineAdjustments({});
@ -224,6 +238,7 @@ export default function App() {
// 新表ID批量生成表
const BATCH_TABLE_ID = 'tblXO7iSxBYxrqtY';
const BATCH_ROW_NUMBER_FIELD_ID = 'fldiqlTVsU';
const fetchAllRecordsByPage = async (table: any, params?: any) => {
let token: any = undefined;
@ -245,6 +260,14 @@ export default function App() {
return res?.total || 0;
};
const parseBatchRowNumber = (raw: any): number | null => {
const text = extractText(raw);
const n = typeof raw === 'number'
? raw
: (text && text.trim() !== '' ? Number(text) : NaN);
return Number.isFinite(n) ? n : null;
};
// 已移除:调整模式不再加载货期记录列表
// 入口选择处理
@ -259,10 +282,19 @@ export default function App() {
try {
const batchTable = await bitable.base.getTable(BATCH_TABLE_ID);
const total = await getRecordTotalByPage(batchTable);
setBatchTotalRows(total);
let totalByRowNumber = 0;
try {
const rows = await fetchAllRecordsByPage(batchTable);
for (const r of rows) {
const no = parseBatchRowNumber((r?.fields || {})[BATCH_ROW_NUMBER_FIELD_ID]);
if (no !== null) totalByRowNumber = Math.max(totalByRowNumber, no);
}
} catch {}
const totalRows = totalByRowNumber > 0 ? totalByRowNumber : total;
setBatchTotalRows(totalRows);
setBatchStartRow(1);
setBatchEndRow(total > 0 ? total : 1);
setBatchProcessingTotal(total);
setBatchEndRow(totalRows > 0 ? totalRows : 1);
setBatchProcessingTotal(totalRows);
} catch {
setBatchTotalRows(0);
setBatchStartRow(1);
@ -273,6 +305,8 @@ export default function App() {
};
const restoreBaseBufferDaysFromSnapshot = (snapshot: any) => {
const snapshotDirection = snapshot?.timelineDirection;
if (snapshotDirection === 'backward') return;
const candidates: any[] = [
snapshot?.bufferManagement?.baseDays,
snapshot?.baseBufferDays,
@ -291,6 +325,21 @@ export default function App() {
}
};
const restoreTimelineDirectionFromSnapshot = (snapshot: any) => {
const candidates: any[] = [
snapshot?.timelineDirection,
snapshot?.timelineCalculationState?.timelineDirection,
snapshot?.timelineCalculationState?.calculationDirection,
snapshot?.calculationDirection,
];
for (const c of candidates) {
if (c === 'forward' || c === 'backward') {
setTimelineDirection(c);
return;
}
}
};
// 根据货期记录ID读取节点详情并还原流程数据
const loadProcessDataFromDeliveryRecord = async (deliveryRecordId: string) => {
if (!deliveryRecordId) {
@ -355,6 +404,7 @@ export default function App() {
if (deliverySnapStr && deliverySnapStr.trim() !== '') {
setIsRestoringSnapshot(true);
const snapshot = JSON.parse(deliverySnapStr);
restoreTimelineDirectionFromSnapshot(snapshot);
restoreBaseBufferDaysFromSnapshot(snapshot);
// 恢复页面与全局状态
@ -494,6 +544,7 @@ export default function App() {
const snapshot = JSON.parse(snapStr);
if (Array.isArray(snapshot.timelineResults)) {
setIsRestoringSnapshot(true);
restoreTimelineDirectionFromSnapshot(snapshot);
restoreBaseBufferDaysFromSnapshot(snapshot);
if (snapshot.selectedLabels) setSelectedLabels(snapshot.selectedLabels);
@ -627,6 +678,7 @@ export default function App() {
if (snapStr && snapStr.trim() !== '') {
setIsRestoringSnapshot(true); // 开始快照还原
const snapshot = JSON.parse(snapStr);
restoreTimelineDirectionFromSnapshot(snapshot);
restoreBaseBufferDaysFromSnapshot(snapshot);
// 恢复页面状态
if (snapshot.selectedLabels) setSelectedLabels(snapshot.selectedLabels);
@ -967,6 +1019,7 @@ export default function App() {
nodeSnapshots.length === globalSnapshotData.totalNodes) {
// 重组完整的 timelineResults
restoreTimelineDirectionFromSnapshot(globalSnapshotData);
restoreBaseBufferDaysFromSnapshot(globalSnapshotData);
setTimelineResults(nodeSnapshots);
setTimelineVisible(true);
@ -1855,6 +1908,7 @@ export default function App() {
const currentSelectedLabels = overrideData?.selectedLabels || selectedLabels;
const currentExpectedDate = overrideData?.expectedDate || expectedDate;
const currentStartTime = overrideData?.startTime || startTime;
const isBackward = timelineDirection === 'backward';
console.log('=== handleCalculateTimeline - 使用的数据 ===');
console.log('currentSelectedRecords:', currentSelectedRecords);
@ -1877,6 +1931,28 @@ export default function App() {
}
}
if (isBackward && !currentExpectedDate) {
if (showUI && bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: '倒推模式需要先选择客户期望日期' });
}
return [];
}
if (isBackward) {
if (!currentStartTime || isNaN(currentStartTime.getTime())) {
if (showUI && bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: '倒推模式需要先选择起始日期' });
}
return [];
}
if (!currentExpectedDate || isNaN(currentExpectedDate.getTime())) {
if (showUI && bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: '倒推模式需要先选择客户期望日期' });
}
return [];
}
}
// 跳过验证(用于批量模式)
if (!skipValidation) {
// 检查是否选择了多条记录
@ -2220,10 +2296,13 @@ export default function App() {
const results: any[] = [];
// 3. 按顺序为每个匹配的流程节点查找时效数据并计算累积时间
let cumulativeStartTime = currentStartTime ? new Date(currentStartTime) : new Date(); // 累积开始时间
let cumulativeTime = isBackward
? new Date(currentExpectedDate as Date)
: (currentStartTime ? new Date(currentStartTime) : new Date());
const nodesToProcess = isBackward ? [...matchedProcessNodes].reverse() : matchedProcessNodes;
for (let i = 0; i < matchedProcessNodes.length; i++) {
const processNode = matchedProcessNodes[i];
for (let i = 0; i < nodesToProcess.length; i++) {
const processNode = nodesToProcess[i];
let timelineValue = null;
let matchedTimelineRecord = null;
@ -2447,9 +2526,29 @@ export default function App() {
};
};
let nodeStartTime = new Date(cumulativeStartTime);
// 获取当前节点的计算方式
let nodeCalculationMethod = '外部'; // 默认值
if (matchedCandidates.length > 0) {
nodeCalculationMethod = matchedCandidates[0].calculationMethod;
} else if (matchedTimelineRecord) {
const calculationMethodField = matchedTimelineRecord.fields[calculationMethodFieldId];
if (calculationMethodField) {
if (typeof calculationMethodField === 'string') {
nodeCalculationMethod = calculationMethodField;
} else if (calculationMethodField && typeof calculationMethodField === 'object' && calculationMethodField.text) {
nodeCalculationMethod = calculationMethodField.text;
}
}
}
let nodeStartTime: Date;
let nodeEndTime: Date;
let ruleDescription = '';
let timelineResult: { startDate: string; endDate: string };
if (!isBackward) {
nodeStartTime = new Date(cumulativeTime);
// 应用起始日期调整规则
if (processNode.startDateRule) {
let ruleJson = '';
if (typeof processNode.startDateRule === 'string') {
@ -2464,15 +2563,12 @@ export default function App() {
}
}
// 应用JSON格式日期调整规则
let ruleDescription = '';
if (processNode.dateAdjustmentRule) {
console.log('原始dateAdjustmentRule:', processNode.dateAdjustmentRule);
let ruleText = '';
if (typeof processNode.dateAdjustmentRule === 'string') {
ruleText = processNode.dateAdjustmentRule;
} else if (Array.isArray(processNode.dateAdjustmentRule)) {
// 处理富文本数组格式
ruleText = processNode.dateAdjustmentRule
.filter((item: any) => item.type === 'text')
.map((item: any) => item.text)
@ -2492,29 +2588,13 @@ export default function App() {
}
}
// 获取当前节点的计算方式
let nodeCalculationMethod = '外部'; // 默认值
if (matchedCandidates.length > 0) {
nodeCalculationMethod = matchedCandidates[0].calculationMethod;
} else if (matchedTimelineRecord) {
const calculationMethodField = matchedTimelineRecord.fields[calculationMethodFieldId];
if (calculationMethodField) {
if (typeof calculationMethodField === 'string') {
nodeCalculationMethod = calculationMethodField;
} else if (calculationMethodField && typeof calculationMethodField === 'object' && calculationMethodField.text) {
nodeCalculationMethod = calculationMethodField.text;
}
}
}
const timelineResult = timelineValue ?
calculateTimeline(nodeStartTime, timelineValue, nodeCalculationMethod) :
{
timelineResult = timelineValue
? calculateTimeline(nodeStartTime, timelineValue, nodeCalculationMethod)
: {
startDate: formatDate(nodeStartTime, 'STORAGE_FORMAT'),
endDate: '未找到时效数据'
};
let nodeEndTime: Date;
if (timelineValue) {
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, processNode.weekendDays, processNode.excludedDates || []);
if (nodeCalculationMethod === '内部') {
@ -2525,6 +2605,28 @@ export default function App() {
} else {
nodeEndTime = new Date(nodeStartTime);
}
} else {
nodeEndTime = new Date(cumulativeTime);
if (timelineValue) {
if (nodeCalculationMethod === '内部') {
nodeStartTime = addInternalBusinessTime(nodeEndTime, -timelineValue, processNode.weekendDays, processNode.excludedDates || []);
} else {
nodeStartTime = addBusinessDaysWithHolidays(nodeEndTime, -timelineValue, processNode.weekendDays, processNode.excludedDates || []);
}
} else {
nodeStartTime = new Date(nodeEndTime);
}
timelineResult = timelineValue
? {
startDate: formatDate(nodeStartTime, 'STORAGE_FORMAT'),
endDate: formatDate(nodeEndTime, 'STORAGE_FORMAT')
}
: {
startDate: formatDate(nodeStartTime, 'STORAGE_FORMAT'),
endDate: '未找到时效数据'
};
}
// 计算跳过的天数
const skippedWeekends = calculateSkippedWeekends(nodeStartTime, nodeEndTime, processNode.weekendDays);
@ -2533,7 +2635,7 @@ export default function App() {
// 计算时间范围内实际跳过的自定义日期
const excludedDatesInRange = calculateExcludedDatesInRange(nodeStartTime, nodeEndTime, processNode.excludedDates || []);
results.push({
(isBackward ? results.unshift.bind(results) : results.push.bind(results))({
processOrder: processNode.processOrder,
nodeName: processNode.nodeName,
matchedLabels: processNode.matchedLabels,
@ -2569,7 +2671,7 @@ export default function App() {
// 更新累积时间:当前节点的完成时间成为下一个节点的开始时间
if (timelineValue) {
cumulativeStartTime = new Date(nodeEndTime);
cumulativeTime = isBackward ? new Date(nodeStartTime) : new Date(nodeEndTime);
}
console.log(`节点 ${processNode.nodeName} (顺序: ${processNode.processOrder}):`, {
@ -2580,9 +2682,42 @@ export default function App() {
});
}
if (isBackward && results.length > 0) {
const parsed = typeof results[0]?.estimatedStart === 'string' ? parseDate(results[0].estimatedStart) : null;
if (parsed && !isNaN(parsed.getTime())) {
setCalculatedRequiredStartTime(parsed);
} else {
setCalculatedRequiredStartTime(null);
}
}
let delayShowTimelineModal = false;
if (isBackward) {
const required = typeof results[0]?.estimatedStart === 'string' ? parseDate(results[0].estimatedStart) : null;
setCalculatedRequiredStartTime(required && !isNaN(required.getTime()) ? required : null);
if (required && currentStartTime && !isNaN(currentStartTime.getTime())) {
const diffDays = differenceInCalendarDays(required, currentStartTime);
const extraDays = Math.max(0, diffDays);
if (extraDays > 0) {
setAllocationNodesSnapshot(results);
setAllocationExtraDays(extraDays);
setAllocationMode('auto');
setAllocationVisible(true);
delayShowTimelineModal = true;
} else if (required.getTime() < currentStartTime.getTime()) {
if (showUI && bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: '当前起始日期晚于倒推要求起始日期,可能无法满足客户期望日期' });
}
}
}
}
setTimelineResults(results);
if (showUI) {
if (showUI && !delayShowTimelineModal) {
setTimelineVisible(true);
} else if (showUI && delayShowTimelineModal) {
setTimelineVisible(false);
}
console.log('按流程顺序计算的时效结果:', results);
@ -2846,6 +2981,149 @@ export default function App() {
return updatedResults;
};
const getRecalculatedTimelineBackward = (adjustments: { [key: number]: number }) => {
const updatedResults = [...timelineResults];
const fallbackEnd = (() => {
if (expectedDate && !isNaN(expectedDate.getTime())) return new Date(expectedDate);
const lastEndStr = updatedResults.length > 0 ? updatedResults[updatedResults.length - 1]?.estimatedEnd : null;
const parsed = typeof lastEndStr === 'string' ? parseDate(lastEndStr) : null;
return parsed && !isNaN(parsed.getTime()) ? parsed : new Date();
})();
let cumulativeEndTime = new Date(fallbackEnd);
for (let i = updatedResults.length - 1; i >= 0; i--) {
const result = updatedResults[i];
const baseTimelineValue = (typeof result.timelineValue === 'number')
? result.timelineValue
: (typeof result.adjustedTimelineValue === 'number')
? result.adjustedTimelineValue
: 0;
const adjustment = adjustments[i] || 0;
const adjustedTimelineValue = baseTimelineValue + adjustment;
const nodeWeekendDays = result.weekendDaysConfig || [];
const nodeCalculationMethod = result.calculationMethod || '外部';
const nodeExcludedDates = Array.isArray(result.excludedDates) ? result.excludedDates : [];
const nodeEndTime = new Date(cumulativeEndTime);
let nodeStartTime: Date;
if (adjustedTimelineValue !== 0) {
if (nodeCalculationMethod === '内部') {
nodeStartTime = addInternalBusinessTime(nodeEndTime, -adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates);
} else {
nodeStartTime = addBusinessDaysWithHolidays(nodeEndTime, -adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates);
}
} else {
nodeStartTime = new Date(nodeEndTime);
}
const skippedWeekends = calculateSkippedWeekends(nodeStartTime, nodeEndTime, nodeWeekendDays);
const estimatedStartStr = formatDate(nodeStartTime);
const estimatedEndStr = formatDate(nodeEndTime);
const actualDays = calculateActualDays(estimatedStartStr, estimatedEndStr);
const excludedDatesInRange = calculateExcludedDatesInRange(nodeStartTime, nodeEndTime, nodeExcludedDates);
updatedResults[i] = {
...result,
adjustedTimelineValue,
estimatedStart: estimatedStartStr,
estimatedEnd: estimatedEndStr,
adjustment,
calculationMethod: nodeCalculationMethod,
skippedWeekends,
actualDays,
actualExcludedDates: excludedDatesInRange.dates,
actualExcludedDatesCount: excludedDatesInRange.count,
};
if (adjustedTimelineValue !== 0) {
cumulativeEndTime = new Date(nodeStartTime);
}
}
return updatedResults;
};
const buildAutoAllocationDraft = (nodes: any[], extraDays: number, excluded?: {[key: number]: boolean}) => {
const roundedExtra = Math.max(0, Math.round(Number(extraDays) * 100) / 100);
if (!Array.isArray(nodes) || nodes.length === 0 || roundedExtra === 0) return {};
const isTurnover = (node: any) => node?.nodeName === '周转周期';
const toBase = (node: any) => {
const v = typeof node?.timelineValue === 'number'
? node.timelineValue
: (typeof node?.adjustedTimelineValue === 'number' ? node.adjustedTimelineValue : 0);
return Number.isFinite(v) ? v : 0;
};
const eligible = nodes
.map((n, i) => ({ i, base: toBase(n), turnover: isTurnover(n), excluded: !!excluded?.[i] }))
.filter(x => !x.turnover && !x.excluded);
if (eligible.length === 0) return {};
const positive = eligible.filter(x => x.base > 0);
const pool = positive.length > 0 ? positive : eligible;
const totalBase = pool.reduce((s, x) => s + (x.base > 0 ? x.base : 1), 0);
const draft: {[key: number]: number} = {};
let remaining = roundedExtra;
for (let k = 0; k < pool.length; k++) {
const idx = pool[k].i;
if (k === pool.length - 1) {
draft[idx] = Math.max(0, Math.round(remaining * 100) / 100);
break;
}
const weight = pool[k].base > 0 ? pool[k].base : 1;
const raw = (roundedExtra * weight) / totalBase;
const val = Math.max(0, Math.round(raw * 100) / 100);
draft[idx] = val;
remaining = Math.max(0, Math.round((remaining - val) * 100) / 100);
}
return draft;
};
useEffect(() => {
if (!allocationVisible) return;
if (allocationMode !== 'auto') return;
const nodes = allocationNodesSnapshot.length > 0 ? allocationNodesSnapshot : timelineResults;
const draft = buildAutoAllocationDraft(nodes, allocationExtraDays, allocationExcluded);
setAllocationDraft(draft);
}, [allocationVisible, allocationMode, allocationExtraDays, allocationNodesSnapshot, timelineResults, allocationExcluded]);
const applyAllocationDraft = async () => {
const sum = Object.values(allocationDraft || {}).reduce((s, v) => s + (Number(v) || 0), 0);
const roundedSum = Math.round(sum * 100) / 100;
const roundedTarget = Math.round((Number(allocationExtraDays) || 0) * 100) / 100;
const diff = Math.round((roundedTarget - roundedSum) * 100) / 100;
if (Math.abs(diff) > 0.01) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.warning, message: `当前分配合计${roundedSum}天,与盈余${roundedTarget}天不一致` });
}
return;
}
const merged: {[key: number]: number} = { ...(timelineAdjustments || {}) };
for (const [k, v] of Object.entries(allocationDraft || {})) {
const idx = parseInt(k, 10);
const val = Math.max(0, Math.round((Number(v) || 0) * 100) / 100);
merged[idx] = Math.round(((Number(merged[idx]) || 0) + val) * 100) / 100;
}
setTimelineAdjustments(merged);
setAllocationVisible(false);
recalculateTimeline(merged, true);
setTimelineVisible(true);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: ToastType.success, message: '已应用盈余分配' });
}
};
// 获取有效的最后完成日期(忽略 '时效值为0'),若不存在则返回最后一项的日期
const getLastValidCompletionDateFromResults = (results: any[]): Date | null => {
if (!Array.isArray(results) || results.length === 0) return null;
@ -2864,6 +3142,7 @@ export default function App() {
// 依据“最后流程节点的预计完成日期差(自然日)”计算剩余缓冲期
const computeDynamicBufferDaysUsingEndDelta = (adjustments: { [key: number]: number }): number => {
try {
if (timelineDirection === 'backward') return 0;
const baseline = getRecalculatedTimeline({}); // 原始计划(不含任何调整)
const current = getRecalculatedTimeline(adjustments); // 当前计划(包含时效值调整)
@ -2898,6 +3177,19 @@ export default function App() {
// 重新计算时间线的函数
const recalculateTimeline = (adjustments: {[key: number]: number}, forceRecalculateAll: boolean = false) => {
if (timelineDirection === 'backward') {
const updated = getRecalculatedTimelineBackward(adjustments);
setTimelineResults(updated);
const firstStartStr = updated.length > 0 ? updated[0]?.estimatedStart : null;
const parsed = typeof firstStartStr === 'string' ? parseDate(firstStartStr) : null;
if (parsed && !isNaN(parsed.getTime())) {
setCalculatedRequiredStartTime(parsed);
} else {
setCalculatedRequiredStartTime(null);
}
return;
}
const updatedResults = [...timelineResults];
// 找到第一个被调整的节点索引
@ -3085,10 +3377,19 @@ export default function App() {
// 当起始时间变更时,重新以最新起始时间为基准重算全流程
useEffect(() => {
if (timelineDirection !== 'forward') return;
if (timelineResults.length > 0 && !isRestoringSnapshot) {
recalculateTimeline(timelineAdjustments, true); // 强制重算所有节点
}
}, [startTime, isRestoringSnapshot]);
}, [startTime, isRestoringSnapshot, timelineDirection]);
useEffect(() => {
if (timelineDirection !== 'backward') return;
if (!expectedDate) return;
if (timelineResults.length > 0 && !isRestoringSnapshot) {
recalculateTimeline(timelineAdjustments, true);
}
}, [expectedDate, isRestoringSnapshot, timelineDirection]);
// 当实际完成日期变化时,以最新状态进行重算,避免首次选择不触发或使用旧值
useEffect(() => {
@ -3101,6 +3402,7 @@ export default function App() {
useEffect(() => {
if (!hasCapturedInitialSnapshotRef.current && timelineResults.length > 0) {
initialSnapshotRef.current = {
timelineDirection,
startTime,
expectedDate,
selectedLabels,
@ -3143,6 +3445,9 @@ export default function App() {
}
const s = initialSnapshotRef.current;
setIsRestoringSnapshot(true);
if (s.timelineDirection === 'forward' || s.timelineDirection === 'backward') {
setTimelineDirection(s.timelineDirection);
}
setStartTime(s.startTime || null);
setExpectedDate(s.expectedDate || null);
setSelectedLabels(s.selectedLabels || {});
@ -3447,6 +3752,7 @@ export default function App() {
colorText,
text2,
mode,
timelineDirection,
selectedLabels: currentSelectedLabels,
expectedDateTimestamp,
expectedDateString,
@ -3465,6 +3771,8 @@ export default function App() {
}),
labelSelectionComplete: Object.keys(selectedLabels).length > 0
},
...(timelineDirection !== 'backward'
? {
bufferManagement: {
baseDays: baseBuferDays,
totalAdjustments,
@ -3473,6 +3781,8 @@ export default function App() {
hasAppliedSuggestedBuffer,
lastSuggestedApplied: lastSuggestedApplied ?? 0
},
}
: {}),
chainAdjustmentSystem: {
enabled: true,
lastCalculationTime: new Date().getTime(),
@ -3482,7 +3792,8 @@ export default function App() {
calculationTimestamp: new Date().getTime(),
totalNodes: timelineResults.length,
hasValidResults: timelineResults.length > 0,
lastCalculationMode: mode
lastCalculationMode: mode,
timelineDirection
},
totalNodes: timelineResults.length,
isGlobalSnapshot: true
@ -4060,18 +4371,42 @@ export default function App() {
}
}
const rows = await fetchAllRecordsByPage(batchTable);
const total = rows.length;
const startIndex1 = range?.start && range.start > 0 ? range.start : 1;
const endIndex1 = range?.end && range.end > 0 ? range.end : total;
const minIndex = Math.max(1, Math.min(startIndex1, total));
const maxIndex = Math.max(minIndex, Math.min(endIndex1, total));
setBatchProcessingTotal(maxIndex - minIndex + 1);
const rowsWithNo = rows.map((row: any, idx: number) => {
const f = row?.fields || {};
const no = parseBatchRowNumber(f[BATCH_ROW_NUMBER_FIELD_ID]);
return { row, idx, no };
});
const hasRowNo = rowsWithNo.some(r => typeof r.no === 'number');
const ordered = hasRowNo
? [...rowsWithNo].sort((a, b) => {
const na = typeof a.no === 'number' ? a.no : Infinity;
const nb = typeof b.no === 'number' ? b.no : Infinity;
if (na !== nb) return na - nb;
return a.idx - b.idx;
})
: rowsWithNo;
const allNos = hasRowNo ? ordered.map(r => (typeof r.no === 'number' ? r.no : null)).filter((v): v is number => v !== null) : [];
const minNo = allNos.length > 0 ? Math.min(...allNos) : 1;
const maxNo = allNos.length > 0 ? Math.max(...allNos) : ordered.length;
const requestedStart = range?.start && range.start > 0 ? range.start : (hasRowNo ? minNo : 1);
const requestedEnd = range?.end && range.end > 0 ? range.end : (hasRowNo ? maxNo : ordered.length);
const start = Math.min(requestedStart, requestedEnd);
const end = Math.max(requestedStart, requestedEnd);
const selected = hasRowNo
? ordered.filter(r => typeof r.no === 'number' && r.no >= Math.max(minNo, start) && r.no <= Math.min(maxNo, end))
: ordered.slice(Math.max(0, Math.min(start, ordered.length) - 1), Math.max(0, Math.min(end, ordered.length)));
setBatchProcessingTotal(selected.length);
let processed = 0;
for (let i = minIndex - 1; i < maxIndex; i++) {
for (let j = 0; j < selected.length; j++) {
if (batchAbortRef.current) {
break;
}
const row = rows[i];
const { row, idx, no } = selected[j];
const displayIndex = typeof no === 'number' ? no : (idx + 1);
const f = row.fields || {};
const getText = (name: string) => extractText(f[nameToId.get(name) || '']);
const foreignId = getText('foreignId');
@ -4132,11 +4467,11 @@ export default function App() {
if (missing.length > 0) {
setBatchProcessedCount(p => p + 1);
setBatchFailureCount(fCount => fCount + 1);
setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'failed', message: `标签不完整:${missing.join('、')}` }]);
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: `标签不完整:${missing.join('、')}` }]);
continue;
}
}
setBatchCurrentRowInfo({ index: i + 1, foreignId: foreignId || '', style: styleText || '', color: colorText || '' });
setBatchCurrentRowInfo({ index: displayIndex, foreignId: foreignId || '', style: styleText || '', color: colorText || '' });
setCurrentForeignId(foreignId || '');
setCurrentStyleText(styleText || '');
setCurrentColorText(colorText || '');
@ -4178,13 +4513,13 @@ export default function App() {
throw e2;
}
}
setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'success', message: `记录ID: ${deliveryRecordIdStr}` }]);
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'success', message: `记录ID: ${deliveryRecordIdStr}` }]);
} else {
setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'failed', message: '未找到状态字段或记录ID为空' }]);
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: '未找到状态字段或记录ID为空' }]);
}
} catch (statusErr: any) {
console.warn('回写批量状态字段失败', statusErr);
setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'failed', message: `状态写入失败: ${statusErr?.message || '未知错误'}` }]);
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: `状态写入失败: ${statusErr?.message || '未知错误'}` }]);
}
processed++;
setBatchProcessedCount(p => p + 1);
@ -4192,12 +4527,12 @@ export default function App() {
} else {
setBatchProcessedCount(p => p + 1);
setBatchFailureCount(fCount => fCount + 1);
setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'failed', message: '时效结果为空' }]);
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: '时效结果为空' }]);
}
} catch (rowErr: any) {
setBatchProcessedCount(p => p + 1);
setBatchFailureCount(fCount => fCount + 1);
setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'failed', message: rowErr?.message || '处理失败' }]);
setBatchProgressList(list => [...list, { index: displayIndex, foreignId: foreignId || '', status: 'failed', message: rowErr?.message || '处理失败' }]);
}
}
if (bitable.ui.showToast) {
@ -5000,6 +5335,179 @@ export default function App() {
</div>
)}
<Modal
title="倒推盈余分配"
visible={allocationVisible}
maskClosable={false}
onCancel={() => {
setAllocationVisible(false);
setTimelineVisible(true);
}}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space>
<Button
onClick={() => {
const nodes = allocationNodesSnapshot.length > 0 ? allocationNodesSnapshot : timelineResults;
const draft = buildAutoAllocationDraft(nodes, allocationExtraDays, allocationExcluded);
setAllocationDraft(draft);
}}
>
</Button>
<Button
onClick={() => {
setAllocationDraft({});
}}
>
</Button>
</Space>
<Space>
<Button onClick={() => { setAllocationVisible(false); setTimelineVisible(true); }}></Button>
<Button type="primary" onClick={applyAllocationDraft}></Button>
</Space>
</div>
}
width={900}
>
{(() => {
const nodes = allocationNodesSnapshot.length > 0 ? allocationNodesSnapshot : timelineResults;
const sum = Object.values(allocationDraft || {}).reduce((s, v) => s + (Number(v) || 0), 0);
const roundedSum = Math.round(sum * 100) / 100;
const roundedTarget = Math.round((Number(allocationExtraDays) || 0) * 100) / 100;
const remainingRaw = Math.round((roundedTarget - roundedSum) * 100) / 100;
const remaining = Math.max(0, remainingRaw);
const overAllocated = Math.max(0, Math.round((0 - remainingRaw) * 100) / 100);
const statusColor = overAllocated > 0 ? '#dc2626' : (remaining === 0 ? '#16a34a' : '#d97706');
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Card>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 16, alignItems: 'center' }}>
<Text>{expectedDate ? formatDate(expectedDate, 'CHINESE_DATE') : '-'}</Text>
<Text>{startTime ? formatDate(startTime) : '-'}</Text>
<Text>{calculatedRequiredStartTime ? formatDate(calculatedRequiredStartTime) : '-'}</Text>
<Text strong style={{ color: statusColor }}>
{roundedTarget} {remaining}{overAllocated > 0 ? `,已超配 ${overAllocated}` : ''}
</Text>
</div>
<div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
<Text strong></Text>
<Select
style={{ width: 240 }}
value={allocationMode}
onChange={(v) => setAllocationMode(v as 'auto' | 'manual')}
optionList={[
{ value: 'auto', label: '系统自动分配(按时效比例)' },
{ value: 'manual', label: '业务自行分配' },
]}
/>
</div>
</Card>
<Table
pagination={false}
size="small"
columns={[
{
title: '节点',
dataIndex: 'nodeName',
key: 'nodeName',
width: 220,
render: (_: any, row: any) => {
const idx = row.index as number;
const nodeName = row.nodeName as string;
const isTurnoverNode = nodeName === '周转周期';
const excluded = !!allocationExcluded?.[idx] || isTurnoverNode;
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
<span style={{ opacity: excluded ? 0.5 : 1 }}>{nodeName}</span>
<Button
size="small"
type="tertiary"
disabled={isTurnoverNode}
onClick={() => {
const currentlyExcluded = excluded;
setAllocationExcluded(prev => {
const next = { ...(prev || {}) };
if (currentlyExcluded) {
delete next[idx];
} else {
next[idx] = true;
}
return next;
});
setAllocationDraft(prev => {
const next = { ...(prev || {}) };
if (!currentlyExcluded) {
next[idx] = 0;
}
return next;
});
}}
>
{excluded ? '+' : '-'}
</Button>
</span>
);
}
},
{ title: '基准时效(天)', dataIndex: 'base', key: 'base', width: 120, render: (v) => (Number(v) || 0).toFixed(2) },
{
title: allocationMode === 'manual' ? '分配盈余(天)' : '系统分配(天)',
dataIndex: 'allocated',
key: 'allocated',
width: 160,
render: (_: any, row: any) => {
const idx = row.index as number;
const isTurnoverNode = row.nodeName === '周转周期';
const isExcluded = !!allocationExcluded?.[idx];
const val = Number(allocationDraft[idx]) || 0;
if (allocationMode === 'manual') {
const otherSum = Math.round((roundedSum - val) * 100) / 100;
const maxAllowed = Math.max(0, Math.round((roundedTarget - otherSum) * 100) / 100);
const maxForControl = maxAllowed < val ? val : maxAllowed;
return (
<InputNumber
min={0}
max={maxForControl}
step={0.1}
value={val}
disabled={isTurnoverNode || isExcluded}
onChange={(v) => {
const raw = Math.round((Number(v) || 0) * 100) / 100;
const n = Math.max(0, Math.min(raw, maxAllowed));
setAllocationDraft(prev => ({ ...(prev || {}), [idx]: n }));
}}
style={{ width: 140 }}
/>
);
}
return <span>{val.toFixed(2)}</span>;
}
}
]}
dataSource={nodes.map((n, index) => {
const base = typeof n?.timelineValue === 'number'
? n.timelineValue
: (typeof n?.adjustedTimelineValue === 'number' ? n.adjustedTimelineValue : 0);
return {
key: index,
index,
nodeName: n?.nodeName || `节点${index + 1}`,
base: Number.isFinite(base) ? base : 0,
allocated: Number(allocationDraft[index]) || 0,
};
})}
/>
</div>
);
})()}
</Modal>
{/* 时效计算结果模态框 */}
<Modal
title={
@ -5953,6 +6461,45 @@ export default function App() {
})}
</div>
<div style={{ marginTop: '24px' }}>
<Text strong style={{ display: 'block', marginBottom: '8px' }}></Text>
<Select
style={{ width: '300px' }}
value={timelineDirection}
onChange={(value) => setTimelineDirection(value as 'forward' | 'backward')}
>
<Select.Option value="forward"></Select.Option>
<Select.Option value="backward"></Select.Option>
</Select>
</div>
<div style={{ marginTop: '24px' }}>
<Text strong style={{ display: 'block', marginBottom: '8px' }}></Text>
<DatePicker
className="input-enhanced"
style={{ width: '300px' }}
placeholder="请选择起始日期"
value={startTime ?? undefined}
onChange={(date) => {
if (date instanceof Date) {
setStartTime(date);
} else if (typeof date === 'string') {
const parsed = new Date(date);
setStartTime(isNaN(parsed.getTime()) ? null : parsed);
} else {
setStartTime(null);
}
}}
type="dateTime"
format="yyyy-MM-dd HH:mm"
/>
{timelineDirection === 'backward' && (
<Text type="secondary" style={{ marginLeft: '12px' }}>
</Text>
)}
</div>
{/* 客户期望日期选择 */}
<div style={{ marginTop: '24px' }}>
<Text strong style={{ display: 'block', marginBottom: '8px' }}></Text>
@ -6004,11 +6551,15 @@ export default function App() {
? handleCalculateTimeline(true, { selectedLabels, expectedDate, startTime }, true)
: handleCalculateTimeline()}
loading={timelineLoading}
disabled={timelineLoading || Array.from({ length: 10 }, (_, i) => `标签${i + 1}`).some(key => {
disabled={
timelineLoading
|| (timelineDirection === 'backward' && (!expectedDate || !startTime))
|| Array.from({ length: 10 }, (_, i) => `标签${i + 1}`).some(key => {
const val = (selectedLabels as any)[key];
if (Array.isArray(val)) return val.length === 0;
return !(typeof val === 'string' && val.trim().length > 0);
})}
})
}
style={{ minWidth: '160px' }}
>
{labelAdjustmentFlow ? '重新生成计划' : '计算预计时间'}