4
This commit is contained in:
2025-11-13 15:59:08 +08:00
parent d10f10f325
commit 129ecfd92c

View File

@ -865,6 +865,11 @@ export default function App() {
// 记录ID文本字段货期记录表新增
const DELIVERY_RECORD_IDS_FIELD_ID = 'fldq3u7h7H';
// OMS看板表相关常量新增
const OMS_BOARD_TABLE_ID = 'tbl7j8bCpUbFmGuk'; // OMS看板表ID
const OMS_PLAN_TEXT_FIELD_ID = 'fldnGV2GLl'; // OMS看板货期计划文本结构
const OMS_PLAN_VERSION_FIELD_ID = 'fldwlIUf4z'; // OMS看板计划版本公式数字
// 已移除中国法定节假日相关常量和配置
// 这个变量声明也不需要了
@ -1192,27 +1197,29 @@ export default function App() {
// 内部工作时间计算函数
const addInternalBusinessTime = (startDate: Date, businessDays: number, weekendDays: number[] = [], excludedDates: string[] = []): Date => {
const result = new Date(startDate);
let remainingDays = businessDays;
if (!businessDays || businessDays === 0) return result;
const isNegative = businessDays < 0;
const absDays = Math.abs(businessDays);
// 处理整数天
const wholeDays = Math.floor(remainingDays);
let addedDays = 0;
while (addedDays < wholeDays) {
result.setDate(result.getDate() + 1);
// 处理整数天(仅计入工作日)
const wholeDays = Math.floor(absDays);
let processedDays = 0;
while (processedDays < wholeDays) {
result.setDate(result.getDate() + (isNegative ? -1 : 1));
if (isBusinessDay(result, weekendDays, excludedDates)) {
addedDays++;
processedDays++;
}
}
// 处理小数部分按9小时工作制
const fractionalDays = remainingDays - wholeDays;
const fractionalDays = absDays - wholeDays;
if (fractionalDays > 0) {
const workingHoursToAdd = fractionalDays * 9; // 内部按9小时工作制
const workingHours = fractionalDays * 9; // 内部按9小时工作制
let currentHour = result.getHours();
let currentMinute = result.getMinutes();
// 确保在工作时间内开始
if (!isNegative) {
// 正向:确保在工作时间内开始
if (currentHour < 9) {
currentHour = 9;
currentMinute = 0;
@ -1226,26 +1233,57 @@ export default function App() {
currentMinute = 0;
}
// 添加工作小时
const totalMinutes = currentHour * 60 + currentMinute + workingHoursToAdd * 60;
const totalMinutes = currentHour * 60 + currentMinute + workingHours * 60;
const finalHour = Math.floor(totalMinutes / 60);
const finalMinute = totalMinutes % 60;
// 如果超过18:00需要跨到下一个工作日
if (finalHour >= 18) {
const overflowHours = finalHour - 18;
const overflowMinutes = finalMinute;
// 跳到下一个工作日
result.setDate(result.getDate() + 1);
while (!isBusinessDay(result, weekendDays, excludedDates)) {
result.setDate(result.getDate() + 1);
}
result.setHours(9 + overflowHours, overflowMinutes, 0, 0);
} else {
result.setHours(finalHour, finalMinute, 0, 0);
}
} else {
// 负向:从当前时间向前回退工作小时,规范到工作时间窗口
if (currentHour > 18) {
// 当天超过18:00先归位到18:00
result.setHours(18, 0, 0, 0);
currentHour = 18;
currentMinute = 0;
} else if (currentHour < 9) {
// 早于9:00跳到前一个工作日的18:00
result.setDate(result.getDate() - 1);
while (!isBusinessDay(result, weekendDays, excludedDates)) {
result.setDate(result.getDate() - 1);
}
result.setHours(18, 0, 0, 0);
currentHour = 18;
currentMinute = 0;
}
const totalMinutes = currentHour * 60 + currentMinute - workingHours * 60;
if (totalMinutes >= 9 * 60) {
const finalHour = Math.floor(totalMinutes / 60);
const finalMinute = totalMinutes % 60;
result.setHours(finalHour, finalMinute, 0, 0);
} else {
// 需要跨到前一个工作日,计算欠缺分钟数
let deficit = 9 * 60 - totalMinutes; // 需要从前一工作日的18:00再退回的分钟数
// 跳到前一个工作日
result.setDate(result.getDate() - 1);
while (!isBusinessDay(result, weekendDays, excludedDates)) {
result.setDate(result.getDate() - 1);
}
// 从18:00开始退 deficit 分钟
result.setHours(18, 0, 0, 0);
result.setMinutes(result.getMinutes() - deficit);
}
}
}
return result;
@ -1254,24 +1292,25 @@ export default function App() {
// 添加工作日 - 使用表格配置的休息日与节点自定义跳过日期
const addBusinessDaysWithHolidays = (startDate: Date, businessDays: number, weekendDays: number[] = [], excludedDates: string[] = []): Date => {
const result = new Date(startDate);
let addedDays = 0;
if (!businessDays || businessDays === 0) return result;
const isNegative = businessDays < 0;
const absDays = Math.abs(businessDays);
let processedDays = 0;
// 处理小数天数:先添加整数天,再处理小数部分
const wholeDays = Math.floor(businessDays);
const fractionalDays = businessDays - wholeDays;
// 添加整数工作日
while (addedDays < wholeDays) {
result.setDate(result.getDate() + 1);
// 先处理整数工作日
const wholeDays = Math.floor(absDays);
while (processedDays < wholeDays) {
result.setDate(result.getDate() + (isNegative ? -1 : 1));
if (isBusinessDay(result, weekendDays, excludedDates)) {
addedDays++;
processedDays++;
}
}
// 处理小数部分转换为小时按24小时制
// 再处理小数部分按24小时制
const fractionalDays = absDays - wholeDays;
if (fractionalDays > 0) {
const hoursToAdd = fractionalDays * 24; // 1天=24小时
result.setHours(result.getHours() + hoursToAdd);
const hours = fractionalDays * 24;
result.setHours(result.getHours() + (isNegative ? -hours : hours));
}
return result;
@ -1398,6 +1437,53 @@ export default function App() {
return;
}
}
} else if (recordId && tableId === OMS_BOARD_TABLE_ID) {
// 从OMS看板匹配对应的货期记录后尝试获取其起始时间
try {
const omsTable = await bitable.base.getTable(OMS_BOARD_TABLE_ID);
const omsRecord = await omsTable.getRecordById(recordId);
const planTextRaw = omsRecord?.fields?.[OMS_PLAN_TEXT_FIELD_ID];
const planVersionRaw = omsRecord?.fields?.[OMS_PLAN_VERSION_FIELD_ID];
const planText = extractText(planTextRaw)?.trim();
let planVersion: number | null = null;
if (typeof planVersionRaw === 'number') {
planVersion = planVersionRaw;
} else if (typeof planVersionRaw === 'string') {
const m = planVersionRaw.match(/\d+/);
if (m) planVersion = parseInt(m[0], 10);
} else if (planVersionRaw && typeof planVersionRaw === 'object') {
const v = (planVersionRaw as any).value ?? (planVersionRaw as any).text;
if (typeof v === 'number') planVersion = v;
else if (typeof v === 'string') {
const m = v.match(/\d+/);
if (m) planVersion = parseInt(m[0], 10);
}
}
if (planText && planVersion !== null) {
const deliveryRecordId = await findDeliveryRecordIdByPlan(planText, planVersion);
if (deliveryRecordId) {
const deliveryTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID);
const deliveryRecord = await deliveryTable.getRecordById(deliveryRecordId);
const startTimeValue = deliveryRecord?.fields?.[DELIVERY_START_TIME_FIELD_ID];
if (startTimeValue) {
let extractedStartTime: Date | null = null;
if (typeof startTimeValue === 'number') {
extractedStartTime = new Date(startTimeValue);
} else if (Array.isArray(startTimeValue) && startTimeValue.length > 0) {
const timestamp = startTimeValue[0];
if (typeof timestamp === 'number') extractedStartTime = new Date(timestamp);
}
if (extractedStartTime && !isNaN(extractedStartTime.getTime())) {
setStartTime(extractedStartTime);
return;
}
}
}
}
} catch (e) {
console.warn('从OMS看板匹配起始时间失败使用当前时间:', e);
}
}
// 如果没有找到有效的起始时间,使用当前时间
@ -1409,6 +1495,42 @@ export default function App() {
}
};
// 根据OMS看板的“货期计划”和“计划版本”匹配货期记录ID
const findDeliveryRecordIdByPlan = async (planText: string, planVersion: number): Promise<string | null> => {
try {
const deliveryTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID);
// 拉取一定数量的记录进行匹配(如需可优化为分页/索引)
const recordsResult = await deliveryTable.getRecords({ pageSize: 5000 });
const records = recordsResult.records || [];
for (const rec of records) {
const fields = rec?.fields || {};
const recordIdsTextVal = fields[DELIVERY_RECORD_IDS_FIELD_ID];
const versionVal = fields[DELIVERY_VERSION_FIELD_ID];
const recordIdsText = extractText(recordIdsTextVal)?.trim();
let versionNum: number | null = null;
if (typeof versionVal === 'number') versionNum = versionVal;
else if (typeof versionVal === 'string') {
const m = versionVal.match(/\d+/);
if (m) versionNum = parseInt(m[0], 10);
} else if (versionVal && typeof versionVal === 'object') {
const v = (versionVal as any).value ?? (versionVal as any).text;
if (typeof v === 'number') versionNum = v;
else if (typeof v === 'string') {
const m = v.match(/\d+/);
if (m) versionNum = parseInt(m[0], 10);
}
}
if (recordIdsText && versionNum !== null && recordIdsText === planText && versionNum === planVersion) {
return rec.id || rec.recordId || null;
}
}
return null;
} catch (error) {
console.error('匹配货期记录失败:', error);
return null;
}
};
// 加载可用表列表
const loadAvailableTables = async () => {
try {
@ -2331,15 +2453,12 @@ export default function App() {
const currentAdjustment = newAdjustments[nodeIndex] || 0;
const newAdjustment = currentAdjustment + adjustment;
// 防止调整后的时效值小于0优先使用原始时效值其次使用上次重算后的值
// 允许调整后的时效值为负数,用于向前回退结束时间
const baseValue = (typeof timelineResults[nodeIndex]?.timelineValue === 'number')
? timelineResults[nodeIndex]!.timelineValue
: (typeof timelineResults[nodeIndex]?.adjustedTimelineValue === 'number')
? timelineResults[nodeIndex]!.adjustedTimelineValue
: 0;
if (baseValue + newAdjustment < 0) {
return null;
}
// 检查当前调整的节点是否为周转周期节点
const currentNodeName = timelineResults[nodeIndex]?.nodeName;
@ -2406,9 +2525,9 @@ export default function App() {
// 获取调整规则描述
const ruleDescription = result.ruleDescription || '';
// 计算节点的结束时间
// 计算节点的结束时间(允许负时效值向前回退)
let nodeEndTime;
if (adjustedTimelineValue > 0) {
if (adjustedTimelineValue !== 0) {
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates);
if (nodeCalculationMethod === 'internal') {
nodeEndTime = addInternalBusinessTime(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates);
@ -2420,10 +2539,10 @@ export default function App() {
}
// 计算跳过的天数
const adjustedStartTime = adjustedTimelineValue > 0 ? adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates) : nodeStartTime;
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates);
const skippedWeekends = calculateSkippedWeekends(adjustedStartTime, nodeEndTime, nodeWeekendDays);
const estimatedStartStr = formatDate(adjustedStartTime);
const estimatedEndStr = adjustedTimelineValue > 0 ? formatDate(nodeEndTime) : '时效值为0';
const estimatedEndStr = adjustedTimelineValue !== 0 ? formatDate(nodeEndTime) : '时效值为0';
const actualDays = calculateActualDays(estimatedStartStr, estimatedEndStr);
// 计算时间范围内实际跳过的自定义日期
@ -2447,7 +2566,7 @@ export default function App() {
};
// 更新累积时间:当前节点的完成时间成为下一个节点的开始时间
if (adjustedTimelineValue > 0) {
if (adjustedTimelineValue !== 0) {
cumulativeStartTime = new Date(nodeEndTime);
}
}
@ -2569,7 +2688,7 @@ export default function App() {
let nodeEndTime: Date;
const nodeExcludedDates = Array.isArray(result.excludedDates) ? result.excludedDates : [];
if (adjustedTimelineValue > 0) {
if (adjustedTimelineValue !== 0) {
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates);
if (nodeCalculationMethod === '内部') {
nodeEndTime = addInternalBusinessTime(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates);
@ -2581,10 +2700,10 @@ export default function App() {
}
// 计算跳过的天数
const adjustedStartTime = adjustedTimelineValue > 0 ? adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates) : nodeStartTime;
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates);
const skippedWeekends = calculateSkippedWeekends(adjustedStartTime, nodeEndTime, nodeWeekendDays);
const estimatedStartStr = formatDate(adjustedStartTime);
const estimatedEndStr = adjustedTimelineValue > 0 ? formatDate(nodeEndTime) : '时效值为0';
const estimatedEndStr = adjustedTimelineValue !== 0 ? formatDate(nodeEndTime) : '时效值为0';
const actualDays = calculateActualDays(estimatedStartStr, estimatedEndStr);
// 计算时间范围内实际跳过的自定义日期
@ -2608,7 +2727,7 @@ export default function App() {
};
// 更新累积时间:优先使用当前节点的实际完成时间,否则使用预计完成时间
if (adjustedTimelineValue > 0) {
if (adjustedTimelineValue !== 0) {
if (actualCompletionDates[i]) {
// 如果当前节点有实际完成时间,使用实际完成时间
cumulativeStartTime = new Date(actualCompletionDates[i]!);
@ -3029,12 +3148,12 @@ export default function App() {
startDateRule: selectedResult.startDateRule,
dateAdjustmentRule: selectedResult.dateAdjustmentRule,
nodeCalculationState: {
hasValidTimelineValue: selectedResult.timelineValue > 0,
hasValidTimelineValue: typeof selectedResult.timelineValue === 'number' && selectedResult.timelineValue !== 0,
hasValidStartTime: Boolean(nodeStartTs),
hasValidEndTime: Boolean(nodeEndTs),
calculationTimestamp: new Date().getTime(),
originalTimelineValue: selectedResult.timelineValue,
finalAdjustedValue: selectedResult.adjustedTimelineValue || selectedResult.timelineValue
finalAdjustedValue: (selectedResult.adjustedTimelineValue ?? selectedResult.timelineValue)
},
chainAdjustmentNode: {
nodeIndex: selectedIndex,
@ -4448,7 +4567,7 @@ export default function App() {
}
}
if (timelineValue && timelineValue > 0) {
if (timelineValue && timelineValue !== 0) {
// 根据计算方式调整开始时间
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, processNode.weekendDays, processNode.excludedDates || []);
@ -4492,7 +4611,7 @@ export default function App() {
});
// 更新累积时间
if (timelineValue && timelineValue > 0) {
if (timelineValue && timelineValue !== 0) {
cumulativeStartTime = new Date(nodeEndTime);
}
}
@ -5245,14 +5364,53 @@ export default function App() {
}
return;
}
if (tableId && tableId !== DELIVERY_RECORD_TABLE_ID) {
if (tableId === DELIVERY_RECORD_TABLE_ID) {
setSelectedDeliveryRecordId(recordId);
await loadProcessDataFromDeliveryRecord(recordId);
} else if (tableId === OMS_BOARD_TABLE_ID) {
// 支持在OMS看板选中记录后读取通过货期计划 + 计划版本匹配货期记录
const omsTable = await bitable.base.getTable(OMS_BOARD_TABLE_ID);
const omsRecord = await omsTable.getRecordById(recordId);
const planTextRaw = omsRecord?.fields?.[OMS_PLAN_TEXT_FIELD_ID];
const planVersionRaw = omsRecord?.fields?.[OMS_PLAN_VERSION_FIELD_ID];
const planText = extractText(planTextRaw)?.trim();
let planVersion: number | null = null;
if (typeof planVersionRaw === 'number') {
planVersion = planVersionRaw;
} else if (typeof planVersionRaw === 'string') {
const m = planVersionRaw.match(/\d+/);
if (m) planVersion = parseInt(m[0], 10);
} else if (planVersionRaw && typeof planVersionRaw === 'object') {
const v = (planVersionRaw as any).value ?? (planVersionRaw as any).text;
if (typeof v === 'number') planVersion = v;
else if (typeof v === 'string') {
const m = v.match(/\d+/);
if (m) planVersion = parseInt(m[0], 10);
}
}
if (!planText || planVersion === null) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 'warning', message: '请在货期记录表中选择记录' });
await bitable.ui.showToast({ toastType: 'warning', message: 'OMS看板记录缺少货期计划或计划版本' });
}
return;
}
const matchedDeliveryRecordId = await findDeliveryRecordIdByPlan(planText, planVersion);
if (!matchedDeliveryRecordId) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 'warning', message: '未能在货期记录表中匹配到对应记录' });
}
return;
}
setSelectedDeliveryRecordId(matchedDeliveryRecordId);
await loadProcessDataFromDeliveryRecord(matchedDeliveryRecordId);
} else {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 'warning', message: '请在货期记录或OMS看板表中选择记录' });
}
return;
}
setSelectedDeliveryRecordId(recordId);
await loadProcessDataFromDeliveryRecord(recordId);
} catch (e) {
console.error('读取当前选中记录失败:', e);
if (bitable.ui.showToast) {
@ -5579,16 +5737,16 @@ export default function App() {
// 使用二分搜索反推工作日数按0.5天粒度),使得正向计算的结束时间尽量贴近目标日期
const dayMs = 1000 * 60 * 60 * 24;
const approxNatural = Math.max(0, (targetDate.getTime() - adjustedStart.getTime()) / dayMs);
const approxNatural = (targetDate.getTime() - adjustedStart.getTime()) / dayMs;
const endFor = (bd: number): Date => {
if (bd <= 0) return new Date(adjustedStart);
if (bd === 0) return new Date(adjustedStart);
return calcMethod === '内部'
? addInternalBusinessTime(new Date(adjustedStart), bd, weekendDays, excludedDates)
: addBusinessDaysWithHolidays(new Date(adjustedStart), bd, weekendDays, excludedDates);
};
let lo = 0;
let hi = Math.max(1, approxNatural + 50);
let lo = approxNatural - 50;
let hi = approxNatural + 50;
for (let it = 0; it < 40; it++) {
const mid = (lo + hi) / 2;
const end = endFor(mid);