10
10
This commit is contained in:
657
src/App.tsx
657
src/App.tsx
@ -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 ? '重新生成计划' : '计算预计时间'}
|
||||
|
||||
Reference in New Issue
Block a user