6920 lines
309 KiB
TypeScript
Raw Normal View History

2025-10-22 14:08:03 +08:00
import { bitable, FieldType } from '@lark-base-open/js-sdk';
2025-10-29 15:03:02 +08:00
import { Button, Typography, List, Card, Space, Divider, Spin, Table, Select, Modal, DatePicker, InputNumber } from '@douyinfe/semi-ui';
2025-10-22 14:08:03 +08:00
import { useState, useEffect } from 'react';
2025-10-24 09:27:39 +08:00
import { addDays, format } from 'date-fns';
2025-10-22 14:08:03 +08:00
import { zhCN } from 'date-fns/locale';
2025-10-22 16:19:28 +08:00
import { executePricingQuery, executeSecondaryProcessQuery, executePricingDetailsQuery } from './services/apiService';
2025-10-22 14:08:03 +08:00
const { Title, Text } = Typography;
// 统一的日期格式常量
const DATE_FORMATS = {
DISPLAY_WITH_TIME: 'yyyy-MM-dd HH:mm', // 显示格式2026-02-20 16:54
DISPLAY_DATE_ONLY: 'yyyy-MM-dd', // 日期格式2026-02-20
STORAGE_FORMAT: 'yyyy-MM-dd HH:mm:ss', // 存储格式2026-02-20 16:54:00
CHINESE_DATE: 'yyyy年MM月dd日', // 中文格式2026年02月20日
2025-11-06 16:19:08 +08:00
} as const;
// 统一的星期显示
const WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] as const;
// 统一的字段提取函数
const extractText = (val: any) => {
if (Array.isArray(val) && val.length > 0) {
const item = val[0];
return typeof item === 'string' ? item : (item?.text || item?.name || '');
} else if (typeof val === 'string') {
return val;
} else if (val && typeof val === 'object') {
return val.text || val.name || '';
}
return '';
};
export default function App() {
const [selectedRecords, setSelectedRecords] = useState<string[]>([]);
const [recordDetails, setRecordDetails] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [queryResults, setQueryResults] = useState<any[]>([]);
const [queryLoading, setQueryLoading] = useState(false);
const [secondaryProcessResults, setSecondaryProcessResults] = useState<any[]>([]);
const [secondaryProcessLoading, setSecondaryProcessLoading] = useState(false);
const [pricingDetailsResults, setPricingDetailsResults] = useState<any[]>([]);
const [pricingDetailsLoading, setPricingDetailsLoading] = useState(false);
// 标签相关状态
const [labelOptions, setLabelOptions] = useState<{[key: string]: any[]}>({});
const [selectedLabels, setSelectedLabels] = useState<{[key: string]: string | string[]}>({});
const [labelLoading, setLabelLoading] = useState(false);
// 客户期望日期状态
const [expectedDate, setExpectedDate] = useState<Date | null>(null);
// 起始时间状态(从货期记录表获取,新记录则使用当前时间)
const [startTime, setStartTime] = useState<Date | null>(null);
// 预览相关状态(已移除未使用的 previewLoading 状态)
// 时效计算相关状态
const [timelineVisible, setTimelineVisible] = useState(false);
const [timelineLoading, setTimelineLoading] = useState(false);
const [timelineResults, setTimelineResults] = useState<any[]>([]);
const [timelineAdjustments, setTimelineAdjustments] = useState<{[key: number]: number}>({});
// 交期余量扣减状态:记录从交期余量中扣减的天数
const [deliveryMarginDeductions, setDeliveryMarginDeductions] = useState<number>(0);
// 最后流程完成日期调整状态:记录最后流程完成日期增加的天数
const [completionDateAdjustment, setCompletionDateAdjustment] = useState<number>(0);
// 实际完成日期状态:记录每个节点的实际完成日期
const [actualCompletionDates, setActualCompletionDates] = useState<{[key: number]: Date | null}>({});
// 基础缓冲期天数可配置用于计算动态缓冲期默认14天
const [baseBufferDays, setBaseBufferDays] = useState<number>(14);
// 快照回填来源foreign_id、款式、颜色、文本2
const [currentForeignId, setCurrentForeignId] = useState<string | null>(null);
const [currentStyleText, setCurrentStyleText] = useState<string>('');
const [currentColorText, setCurrentColorText] = useState<string>('');
const [currentText2, setCurrentText2] = useState<string>('');
const [currentVersionNumber, setCurrentVersionNumber] = useState<number | null>(null);
// 功能入口模式与调整相关状态
const [mode, setMode] = useState<'generate' | 'adjust' | null>(null);
const [modeSelectionVisible, setModeSelectionVisible] = useState(true);
const [adjustLoading, setAdjustLoading] = useState(false);
// 删除未使用的 deliveryRecords 状态
const [selectedDeliveryRecordId, setSelectedDeliveryRecordId] = useState<string>('');
2025-11-12 11:39:56 +08:00
// 从货期记录读取到的record_ids用于保存回写
const [restoredRecordIds, setRestoredRecordIds] = useState<string[]>([]);
// 原始文本格式的record_ids不做JSON化写回
const [restoredRecordIdsText, setRestoredRecordIdsText] = useState<string>('');
2025-11-06 16:19:08 +08:00
// 批量处理相关状态
const [batchProcessing, setBatchProcessing] = useState(false);
const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0 });
const [batchRowCount, setBatchRowCount] = useState<number>(10); // 默认处理10行
// 批量处理当前记录信息(用于保存时传递正确的数据)
const [currentBatchRecord, setCurrentBatchRecord] = useState<{
selectedRecords: string[],
recordDetails: any[],
labels: Record<string, any>,
expectedDate: any,
startTime: any
} | null>(null);
// 批量处理表和视图选择状态
const [selectedBatchTableId, setSelectedBatchTableId] = useState<string>('');
const [selectedBatchViewId, setSelectedBatchViewId] = useState<string>('');
const [availableTables, setAvailableTables] = useState<Array<{id: string, name: string}>>([]);
const [availableViews, setAvailableViews] = useState<Array<{id: string, name: string}>>([]);
const [tablesLoading, setTablesLoading] = useState(false);
const [viewsLoading, setViewsLoading] = useState(false);
// 全局变量重置:在切换功能或切换版单/批量数据时,清空页面与计算相关状态
const resetGlobalState = (opts?: { resetMode?: boolean }) => {
// 运行时加载状态
setLoading(false);
setQueryLoading(false);
setSecondaryProcessLoading(false);
setPricingDetailsLoading(false);
setLabelLoading(false);
setAdjustLoading(false);
setTimelineLoading(false);
setBatchProcessing(false);
setBatchProgress({ current: 0, total: 0 });
// 页面与计算数据
setSelectedRecords([]);
setRecordDetails([]);
setSelectedLabels({});
setExpectedDate(null);
setStartTime(null);
setTimelineVisible(false);
setTimelineResults([]);
setTimelineAdjustments({});
setIsRestoringSnapshot(false);
2025-11-12 11:39:56 +08:00
setRestoredRecordIds([]);
setRestoredRecordIdsText('');
2025-11-06 16:19:08 +08:00
// 当前记录与批量信息
setCurrentBatchRecord(null);
// 当前回填状态
setCurrentForeignId(null);
setCurrentStyleText('');
setCurrentColorText('');
setCurrentText2('');
setCurrentVersionNumber(null);
// 可选:重置模式
if (opts?.resetMode) {
setMode(null);
setModeSelectionVisible(true);
}
};
// 指定的数据表ID和视图ID
const TABLE_ID = 'tblPIJ7unndydSMu';
const VIEW_ID = 'vewb28sjuX';
// 标签表ID
const LABEL_TABLE_ID = 'tblPnQscqwqopJ8V';
// 流程配置表ID
const PROCESS_CONFIG_TABLE_ID = 'tblMygOc6T9o4sYU';
const NODE_NAME_FIELD_ID = 'fld0g9L9Fw'; // 节点名称字段
const PROCESS_LABEL_FIELD_ID = 'fldrVTa23X'; // 流程配置表的标签字段
const PROCESS_ORDER_FIELD_ID = 'fldbfJQ4Zs'; // 流程顺序字段ID
const WEEKEND_DAYS_FIELD_ID = 'fld2BvjbIN'; // 休息日字段ID多选项0-6代表周一到周日
const START_DATE_RULE_FIELD_ID = 'fld0KsQ2j3'; // 起始日期调整规则字段ID
const DATE_ADJUSTMENT_RULE_FIELD_ID = 'fld0KsQ2j3'; // 日期调整规则字段ID
const EXCLUDED_DATES_FIELD_ID = 'fldGxzC5uG'; // 不参与计算日期多选格式yyyy-MM-dd
// 时效数据表相关常量
const TIMELINE_TABLE_ID = 'tblPnQscqwqopJ8V'; // 时效数据表ID
const TIMELINE_FIELD_ID = 'fldniJmZe3'; // 时效值字段ID
const TIMELINE_NODE_FIELD_ID = 'fldeIZzokl'; // 时效表中的节点名称字段ID
const CALCULATION_METHOD_FIELD_ID = 'fldxfLZNUu'; // 时效计算方式字段ID
// 新表ID批量生成表
const BATCH_TABLE_ID = 'tbl673YuipMIJnXL'; // 新创建的多维表格ID
// 已移除:调整模式不再加载货期记录列表
// 入口选择处理
const chooseMode = (m: 'generate' | 'adjust') => {
// 切换功能时重置全局变量但保留新的mode
resetGlobalState({ resetMode: false });
setMode(m);
setModeSelectionVisible(false);
};
// 根据货期记录ID读取节点详情并还原流程数据
const loadProcessDataFromDeliveryRecord = async (deliveryRecordId: string) => {
if (!deliveryRecordId) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 'warning', message: '请先选择一条货期记录' });
}
return;
}
setTimelineLoading(true);
try {
const deliveryTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID);
const deliveryRecord = await deliveryTable.getRecordById(deliveryRecordId);
const nodeDetailsVal = deliveryRecord?.fields?.[DELIVERY_NODE_DETAILS_FIELD_ID];
2025-11-12 11:39:56 +08:00
// 读取record_ids文本字段并保留原始文本用于原样写回
try {
const recordIdsTextVal = deliveryRecord?.fields?.[DELIVERY_RECORD_IDS_FIELD_ID];
const raw = extractText(recordIdsTextVal);
if (raw && raw.trim() !== '') {
setRestoredRecordIdsText(raw.trim());
// 若需要解析为数组供内部使用,可保留解析逻辑(不影响写回原始文本)
try {
const json = JSON.parse(raw);
if (Array.isArray(json)) {
const parsedFromText = json.filter((id: any) => typeof id === 'string' && id.trim() !== '');
if (parsedFromText.length > 0) setRestoredRecordIds(parsedFromText);
}
} catch {
const parsedFromText = raw.split(/[\,\s]+/).map(s => s.trim()).filter(Boolean);
if (parsedFromText.length > 0) setRestoredRecordIds(parsedFromText);
}
}
} catch (e) {
console.warn('解析record_ids文本字段失败忽略:', e);
}
2025-11-06 16:19:08 +08:00
// 优先使用货期记录表中的快照字段进行一键还原(新方案)
try {
const deliverySnapVal = deliveryRecord?.fields?.[DELIVERY_SNAPSHOT_JSON_FIELD_ID];
let deliverySnapStr: string | null = null;
if (typeof deliverySnapVal === 'string') {
deliverySnapStr = deliverySnapVal;
} else if (Array.isArray(deliverySnapVal)) {
const texts = deliverySnapVal
.filter((el: any) => el && el.type === 'text' && typeof el.text === 'string')
.map((el: any) => el.text);
deliverySnapStr = texts.length > 0 ? texts.join('') : null;
} else if (deliverySnapVal && typeof deliverySnapVal === 'object') {
if ((deliverySnapVal as any).text && typeof (deliverySnapVal as any).text === 'string') {
deliverySnapStr = (deliverySnapVal as any).text;
} else if ((deliverySnapVal as any).type === 'text' && typeof (deliverySnapVal as any).text === 'string') {
deliverySnapStr = (deliverySnapVal as any).text;
}
}
if (deliverySnapStr && deliverySnapStr.trim() !== '') {
setIsRestoringSnapshot(true);
const snapshot = JSON.parse(deliverySnapStr);
// 恢复页面与全局状态
if (snapshot.selectedLabels) setSelectedLabels(snapshot.selectedLabels);
if (!mode && snapshot.mode) setMode(snapshot.mode);
if (snapshot.foreignId) setCurrentForeignId(snapshot.foreignId);
if (snapshot.styleText) setCurrentStyleText(snapshot.styleText);
if (snapshot.colorText) setCurrentColorText(snapshot.colorText);
if (snapshot.text2) setCurrentText2(snapshot.text2);
if (snapshot.generationModeState) {
const genState = snapshot.generationModeState;
if (genState.currentForeignId) setCurrentForeignId(genState.currentForeignId);
if (genState.currentStyleText) setCurrentStyleText(genState.currentStyleText);
if (genState.currentColorText) setCurrentColorText(genState.currentColorText);
if (genState.currentText2) setCurrentText2(genState.currentText2);
if (genState.currentVersionNumber !== undefined) setCurrentVersionNumber(genState.currentVersionNumber);
if (genState.recordDetails && Array.isArray(genState.recordDetails)) {
setRecordDetails(genState.recordDetails);
}
}
if (snapshot.version !== undefined) {
let vNum: number | null = null;
if (typeof snapshot.version === 'number') {
vNum = snapshot.version;
} else if (typeof snapshot.version === 'string') {
const match = snapshot.version.match(/\d+/);
if (match) vNum = parseInt(match[0], 10);
}
if (vNum !== null && !isNaN(vNum)) setCurrentVersionNumber(vNum);
}
if (snapshot.timelineAdjustments) setTimelineAdjustments(snapshot.timelineAdjustments);
if (snapshot.expectedDateTimestamp) {
setExpectedDate(new Date(snapshot.expectedDateTimestamp));
} else if (snapshot.expectedDateString) {
setExpectedDate(new Date(snapshot.expectedDateString));
}
// 优先从快照恢复起始时间
let startTimeRestored = false;
if (snapshot.startTimestamp) {
setStartTime(new Date(snapshot.startTimestamp));
startTimeRestored = true;
} else if (snapshot.startString) {
const parsed = new Date(snapshot.startString);
if (!isNaN(parsed.getTime())) {
setStartTime(parsed);
startTimeRestored = true;
}
}
if (!startTimeRestored) {
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);
}
}
// 完整快照直接包含timelineResults优先使用
if (Array.isArray(snapshot.timelineResults)) {
setTimelineResults(snapshot.timelineResults);
setTimelineVisible(true);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 'success', message: '已按货期记录快照还原流程数据' });
}
setTimelineLoading(false);
setIsRestoringSnapshot(false);
return;
}
// 兼容完整快照标识但没有直接timelineResults的情况
if (snapshot.isCompleteSnapshot || snapshot.snapshotType === 'complete' || snapshot.isGlobalSnapshot) {
// 若没有timelineResults视为旧格式保持兼容不在此分支拼装后续走旧流程
} else {
// 非完整快照则进入旧流程(从节点记录扫描快照)
}
}
} catch (e) {
console.warn('从货期记录快照字段还原失败,回退到旧流程:', e);
}
let recordIds: string[] = [];
if (nodeDetailsVal && typeof nodeDetailsVal === 'object' && (nodeDetailsVal as any).recordIds) {
recordIds = (nodeDetailsVal as any).recordIds as string[];
} else if (Array.isArray(nodeDetailsVal)) {
recordIds = nodeDetailsVal.map((item: any) => item?.recordId || item?.id || item).filter(Boolean);
}
2025-11-12 11:39:56 +08:00
// 注意不将节点详情的recordIds写入restoredRecordIds避免读取为空时写入非空
2025-11-06 16:19:08 +08:00
if (!recordIds || recordIds.length === 0) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 'warning', message: '该货期记录未包含节点详情或为空' });
}
setTimelineLoading(false);
return;
}
const processTable = await bitable.base.getTable(PROCESS_DATA_TABLE_ID);
const records = await Promise.all(recordIds.map(id => processTable.getRecordById(id)));
// 优先使用文本2快照一模一样还原
try {
// 在所有记录中查找非空快照
let snapStr: string | null = null;
for (const rec of records) {
const snapVal = rec?.fields?.[PROCESS_SNAPSHOT_JSON_FIELD_ID];
let candidate: string | null = null;
if (typeof snapVal === 'string') {
candidate = snapVal;
} else if (Array.isArray(snapVal)) {
// 文本结构拼接所有text片段
const texts = snapVal
.filter((el: any) => el && el.type === 'text' && typeof el.text === 'string')
.map((el: any) => el.text);
candidate = texts.length > 0 ? texts.join('') : null;
} else if (snapVal && typeof snapVal === 'object') {
// 兼容 {text: '...'} 或 {type:'text', text:'...'}
if ((snapVal as any).text && typeof (snapVal as any).text === 'string') {
candidate = (snapVal as any).text;
} else if ((snapVal as any).type === 'text' && typeof (snapVal as any).text === 'string') {
candidate = (snapVal as any).text;
}
}
if (candidate && candidate.trim() !== '') {
snapStr = candidate;
break;
}
}
if (snapStr && snapStr.trim() !== '') {
setIsRestoringSnapshot(true); // 开始快照还原
const snapshot = JSON.parse(snapStr);
// 恢复页面状态
if (snapshot.selectedLabels) setSelectedLabels(snapshot.selectedLabels);
// 保留当前模式,不覆写为快照的模式(避免调整模式被还原为生成模式)
if (!mode && snapshot.mode) setMode(snapshot.mode);
// 快照回填的foreign_id/款式/颜色/版本
if (snapshot.foreignId) setCurrentForeignId(snapshot.foreignId);
if (snapshot.styleText) setCurrentStyleText(snapshot.styleText);
if (snapshot.colorText) setCurrentColorText(snapshot.colorText);
if (snapshot.text2) setCurrentText2(snapshot.text2);
// 恢复生成模式完整状态(如果存在)
if (snapshot.generationModeState) {
const genState = snapshot.generationModeState;
if (genState.currentForeignId) setCurrentForeignId(genState.currentForeignId);
if (genState.currentStyleText) setCurrentStyleText(genState.currentStyleText);
if (genState.currentColorText) setCurrentColorText(genState.currentColorText);
if (genState.currentText2) setCurrentText2(genState.currentText2);
if (genState.currentVersionNumber !== undefined) setCurrentVersionNumber(genState.currentVersionNumber);
if (genState.recordDetails && Array.isArray(genState.recordDetails)) {
// 恢复记录详情(如果需要的话)
console.log('恢复生成模式记录详情:', genState.recordDetails.length, '条记录');
}
console.log('恢复生成模式状态:', {
hasSelectedLabels: genState.hasSelectedLabels,
labelSelectionComplete: genState.labelSelectionComplete,
recordCount: genState.recordDetails?.length || 0
});
}
if (snapshot.version !== undefined) {
let vNum: number | null = null;
if (typeof snapshot.version === 'number') {
vNum = snapshot.version;
} else if (typeof snapshot.version === 'string') {
const match = snapshot.version.match(/\d+/);
if (match) {
vNum = parseInt(match[0], 10);
}
}
if (vNum !== null && !isNaN(vNum)) setCurrentVersionNumber(vNum);
}
if (snapshot.timelineAdjustments) setTimelineAdjustments(snapshot.timelineAdjustments);
if (snapshot.expectedDateTimestamp) {
setExpectedDate(new Date(snapshot.expectedDateTimestamp));
} else if (snapshot.expectedDateString) {
setExpectedDate(new Date(snapshot.expectedDateString));
}
// 优先从快照恢复起始时间,如果快照中没有则从当前货期记录中获取
let startTimeRestored = false;
if (snapshot.startTimestamp) {
setStartTime(new Date(snapshot.startTimestamp));
startTimeRestored = true;
} else if (snapshot.startString) {
const parsed = new Date(snapshot.startString);
if (!isNaN(parsed.getTime())) {
setStartTime(parsed);
startTimeRestored = true;
}
}
// 如果快照中没有起始时间信息,则从当前选中的货期记录中获取
if (!startTimeRestored) {
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);
}
}
}
if (Array.isArray(snapshot.timelineResults)) {
// 兼容旧版本的完整快照格式
setTimelineResults(snapshot.timelineResults);
setTimelineVisible(true);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 'success', message: '已按快照一模一样还原流程数据' });
}
setTimelineLoading(false);
setIsRestoringSnapshot(false); // 快照还原完成
return; // 快照还原完成,退出函数
} else if (snapshot.isCompleteSnapshot || snapshot.snapshotType === 'complete' ||
snapshot.isGlobalSnapshot || snapshot.isCombinedSnapshot) {
// 处理完整快照格式:每个节点都包含完整数据
console.log('检测到完整快照格式,直接使用快照数据');
// 如果是完整快照直接使用其中的timelineResults
if (snapshot.isCompleteSnapshot && snapshot.timelineResults) {
console.log('使用完整快照中的timelineResults数据');
setTimelineResults(snapshot.timelineResults);
setTimelineVisible(true);
// 恢复智能缓冲期状态
if (snapshot.bufferManagement) {
console.log('恢复智能缓冲期状态:', snapshot.bufferManagement);
}
// 恢复连锁调整系统状态
if (snapshot.chainAdjustmentSystem) {
console.log('恢复连锁调整系统状态:', snapshot.chainAdjustmentSystem);
}
// 恢复生成模式状态
if (snapshot.generationModeState) {
console.log('恢复生成模式状态:', snapshot.generationModeState);
const genState = snapshot.generationModeState;
// 恢复foreign_id状态
if (genState.currentForeignId) {
setCurrentForeignId(genState.currentForeignId);
console.log('恢复foreign_id状态:', genState.currentForeignId);
}
// 恢复款式和颜色状态
if (genState.currentStyleText) {
setCurrentStyleText(genState.currentStyleText);
console.log('恢复款式状态:', genState.currentStyleText);
}
if (genState.currentColorText) {
setCurrentColorText(genState.currentColorText);
console.log('恢复颜色状态:', genState.currentColorText);
}
// 恢复text2状态
if (genState.currentText2) {
setCurrentText2(genState.currentText2);
console.log('恢复text2状态:', genState.currentText2);
}
// 恢复版本号状态
if (genState.currentVersionNumber !== undefined) {
setCurrentVersionNumber(genState.currentVersionNumber);
console.log('恢复版本号状态:', genState.currentVersionNumber);
}
// 恢复记录详情状态
if (genState.recordDetails && Array.isArray(genState.recordDetails)) {
setRecordDetails(genState.recordDetails);
console.log('恢复记录详情状态,记录数量:', genState.recordDetails.length);
}
}
// 恢复时间线计算状态
if (snapshot.timelineCalculationState) {
console.log('恢复时间线计算状态:', snapshot.timelineCalculationState);
}
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'success',
message: '已按完整快照还原流程数据'
});
}
setTimelineLoading(false);
setIsRestoringSnapshot(false);
return;
}
// 兼容旧版本分散快照格式的处理逻辑
// 新版本的分散快照格式:需要从所有节点收集数据
console.log('检测到新版本快照格式,开始收集所有节点的快照数据');
try {
// 收集所有节点的快照数据
const nodeSnapshots: any[] = [];
let globalSnapshotData = snapshot.isCompleteSnapshot ? snapshot : null;
// 遍历所有记录,收集快照数据
for (const record of records) {
const fields = record?.fields || {};
const snapshotField = fields[PROCESS_SNAPSHOT_JSON_FIELD_ID];
if (snapshotField) {
let nodeSnapStr = '';
// 解析快照字段(支持多种格式)
if (typeof snapshotField === 'string') {
nodeSnapStr = snapshotField;
} else if (Array.isArray(snapshotField)) {
const texts = snapshotField
.filter((el: any) => el && el.type === 'text' && typeof el.text === 'string')
.map((el: any) => el.text);
nodeSnapStr = texts.length > 0 ? texts.join('') : '';
} else if (snapshotField && typeof snapshotField === 'object') {
if ((snapshotField as any).text && typeof (snapshotField as any).text === 'string') {
nodeSnapStr = (snapshotField as any).text;
}
}
if (nodeSnapStr && nodeSnapStr.trim() !== '') {
try {
const nodeSnapshot = JSON.parse(nodeSnapStr);
// 批量模式现在使用扁平化结构,直接从快照中提取全局数据
if (nodeSnapshot.isCompleteSnapshot && !globalSnapshotData) {
globalSnapshotData = {
version: nodeSnapshot.version,
foreignId: nodeSnapshot.foreignId,
styleText: nodeSnapshot.styleText,
colorText: nodeSnapshot.colorText,
text2: nodeSnapshot.text2,
mode: nodeSnapshot.mode,
selectedLabels: nodeSnapshot.selectedLabels,
expectedDateTimestamp: nodeSnapshot.expectedDateTimestamp,
expectedDateString: nodeSnapshot.expectedDateString,
startTimestamp: nodeSnapshot.startTimestamp,
startString: nodeSnapshot.startString,
timelineAdjustments: nodeSnapshot.timelineAdjustments,
// 恢复智能缓冲期管理状态
bufferManagement: nodeSnapshot.bufferManagement,
// 恢复连锁调整系统状态
chainAdjustmentSystem: nodeSnapshot.chainAdjustmentSystem,
// 恢复生成模式状态
generationModeState: nodeSnapshot.generationModeState,
// 恢复时间线计算状态
timelineCalculationState: nodeSnapshot.timelineCalculationState,
totalNodes: nodeSnapshot.totalNodes
};
}
// 扁平化结构中,每个快照都包含完整的节点数据
if (nodeSnapshot.isCompleteSnapshot) {
// 确保adjustedTimelineValue有正确的默认值
const adjustedTimelineValue = nodeSnapshot.adjustedTimelineValue !== undefined ?
nodeSnapshot.adjustedTimelineValue : nodeSnapshot.timelineValue;
// 处理日期格式,优先使用时间戳格式
let estimatedStart = nodeSnapshot.estimatedStart;
let estimatedEnd = nodeSnapshot.estimatedEnd;
// 如果有时间戳格式的日期,使用时间戳重新格式化
if (nodeSnapshot.estimatedStartTimestamp) {
try {
estimatedStart = formatDate(new Date(nodeSnapshot.estimatedStartTimestamp));
} catch (error) {
console.warn('时间戳格式开始时间转换失败:', error);
}
}
if (nodeSnapshot.estimatedEndTimestamp) {
try {
estimatedEnd = formatDate(new Date(nodeSnapshot.estimatedEndTimestamp));
} catch (error) {
console.warn('时间戳格式结束时间转换失败:', error);
}
}
// 调试日志
console.log(`节点 ${nodeSnapshot.nodeName} 快照还原:`, {
timelineValue: nodeSnapshot.timelineValue,
adjustedTimelineValue: adjustedTimelineValue,
originalAdjustedTimelineValue: nodeSnapshot.adjustedTimelineValue,
estimatedStart: estimatedStart,
estimatedEnd: estimatedEnd,
hasTimestamps: {
start: Boolean(nodeSnapshot.estimatedStartTimestamp),
end: Boolean(nodeSnapshot.estimatedEndTimestamp)
},
nodeCalculationState: nodeSnapshot.nodeCalculationState
});
nodeSnapshots.push({
processOrder: nodeSnapshot.processOrder,
nodeName: nodeSnapshot.nodeName || nodeSnapshot.currentNodeName,
matchedLabels: nodeSnapshot.matchedLabels,
timelineValue: nodeSnapshot.timelineValue,
estimatedStart: estimatedStart,
estimatedEnd: estimatedEnd,
timelineRecordId: nodeSnapshot.timelineRecordId,
allMatchedRecords: nodeSnapshot.allMatchedRecords,
isAccumulated: nodeSnapshot.isAccumulated,
weekendDaysConfig: nodeSnapshot.weekendDaysConfig,
excludedDates: nodeSnapshot.excludedDates,
actualExcludedDates: nodeSnapshot.actualExcludedDates,
actualExcludedDatesCount: nodeSnapshot.actualExcludedDatesCount,
calculationMethod: nodeSnapshot.calculationMethod,
ruleDescription: nodeSnapshot.ruleDescription,
skippedWeekends: nodeSnapshot.skippedWeekends,
actualDays: nodeSnapshot.actualDays,
adjustedTimelineValue: adjustedTimelineValue,
adjustment: nodeSnapshot.adjustment || 0,
adjustmentDescription: nodeSnapshot.adjustmentDescription || '',
startDateRule: nodeSnapshot.startDateRule,
dateAdjustmentRule: nodeSnapshot.dateAdjustmentRule,
// 恢复节点计算状态
nodeCalculationState: nodeSnapshot.nodeCalculationState,
// 恢复连锁调整节点状态
chainAdjustmentNode: nodeSnapshot.chainAdjustmentNode
});
}
} catch (parseError) {
console.warn('解析节点快照失败:', parseError);
}
}
}
}
// 按流程顺序排序节点快照
nodeSnapshots.sort((a, b) => (a.processOrder || 0) - (b.processOrder || 0));
console.log('收集到的节点快照数量:', nodeSnapshots.length);
console.log('全局快照数据:', globalSnapshotData);
// 验证数据完整性
if (globalSnapshotData && globalSnapshotData.totalNodes &&
nodeSnapshots.length === globalSnapshotData.totalNodes) {
// 重组完整的 timelineResults
setTimelineResults(nodeSnapshots);
setTimelineVisible(true);
// 恢复智能缓冲期状态(如果存在)
if (globalSnapshotData.bufferManagement) {
console.log('恢复智能缓冲期状态:', globalSnapshotData.bufferManagement);
// 这里可以添加额外的状态恢复逻辑,如果需要的话
}
// 恢复连锁调整系统状态(如果存在)
if (globalSnapshotData.chainAdjustmentSystem) {
console.log('恢复连锁调整系统状态:', globalSnapshotData.chainAdjustmentSystem);
// 这里可以添加额外的状态恢复逻辑,如果需要的话
}
// 恢复生成模式状态(如果存在)
if (globalSnapshotData.generationModeState) {
console.log('恢复生成模式状态:', globalSnapshotData.generationModeState);
const genState = globalSnapshotData.generationModeState;
// 确保生成模式的关键状态被正确恢复
if (genState.currentForeignId && !currentForeignId) {
setCurrentForeignId(genState.currentForeignId);
}
if (genState.currentStyleText && !currentStyleText) {
setCurrentStyleText(genState.currentStyleText);
}
if (genState.currentColorText && !currentColorText) {
setCurrentColorText(genState.currentColorText);
}
if (genState.currentText2 && !currentText2) {
setCurrentText2(genState.currentText2);
}
if (genState.currentVersionNumber !== undefined && !currentVersionNumber) {
setCurrentVersionNumber(genState.currentVersionNumber);
}
}
// 恢复时间线计算状态(如果存在)
if (globalSnapshotData.timelineCalculationState) {
console.log('恢复时间线计算状态:', globalSnapshotData.timelineCalculationState);
}
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'success',
message: `已从 ${nodeSnapshots.length} 个节点快照还原完整流程数据(包含智能缓冲期和连锁调整状态)`
});
}
setTimelineLoading(false);
setIsRestoringSnapshot(false); // 快照还原完成
return; // 快照还原完成,退出函数
} else {
console.warn('快照数据不完整,降级为基于字段的还原');
console.log('期望节点数:', globalSnapshotData?.totalNodes, '实际节点数:', nodeSnapshots.length);
}
} catch (collectError) {
console.warn('收集节点快照数据失败,降级为基于字段的还原:', collectError);
}
}
}
} catch (snapError) {
console.warn('解析快照失败,降级为基于字段的还原:', snapError);
setIsRestoringSnapshot(false); // 快照还原失败,重置标志
}
// 如果到达这里,说明没有成功的快照还原,重置标志
setIsRestoringSnapshot(false);
const results = records.map((rec: any) => {
const fields = rec?.fields || {};
const processOrder = fields[PROCESS_ORDER_FIELD_ID_DATA];
const nodeName = fields[PROCESS_NAME_FIELD_ID];
const startTs = fields[ESTIMATED_START_DATE_FIELD_ID];
const endTs = fields[ESTIMATED_END_DATE_FIELD_ID];
const startDate = typeof startTs === 'number' ? new Date(startTs) : (startTs ? new Date(startTs) : null);
const endDate = typeof endTs === 'number' ? new Date(endTs) : (endTs ? new Date(endTs) : null);
return {
processOrder: typeof processOrder === 'number' ? processOrder : parseInt(processOrder) || undefined,
nodeName: typeof nodeName === 'string' ? nodeName : (nodeName?.text || ''),
estimatedStart: startDate ? format(startDate, DATE_FORMATS.STORAGE_FORMAT) : '未找到时效数据',
estimatedEnd: endDate ? format(endDate, DATE_FORMATS.STORAGE_FORMAT) : '未找到时效数据',
timelineRecordId: rec?.id || rec?.recordId || null,
timelineValue: undefined,
calculationMethod: undefined,
weekendDaysConfig: [],
matchedLabels: [],
skippedWeekends: 0,
actualDays: undefined,
startDateRule: undefined,
dateAdjustmentRule: undefined,
ruleDescription: undefined,
adjustmentDescription: undefined
};
});
// 如果没有快照恢复起始时间,则从当前货期记录中获取起始时间
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);
}
}
const sorted = results.sort((a, b) => (a.processOrder || 0) - (b.processOrder || 0));
setTimelineResults(sorted);
setTimelineVisible(true);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 'success', message: '已从货期记录还原流程数据' });
}
} catch (error) {
console.error('从货期记录还原流程数据失败:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 'error', message: `还原失败: ${(error as Error).message}` });
}
} finally {
setTimelineLoading(false);
}
};
// 流程数据表相关常量
const PROCESS_DATA_TABLE_ID = 'tblsJzCxXClj5oK5'; // 流程数据表ID
const FOREIGN_ID_FIELD_ID = 'fld3oxJmpr'; // foreign_id字段
const PROCESS_NAME_FIELD_ID = 'fldR79qEG3'; // 流程名称字段
const PROCESS_ORDER_FIELD_ID_DATA = 'fldmND6vjT'; // 流程顺序字段
const ESTIMATED_START_DATE_FIELD_ID = 'fldlzvHjYP'; // 预计开始日期字段
const ESTIMATED_END_DATE_FIELD_ID = 'fldaPtY7Jk'; // 预计完成日期字段
const PROCESS_SNAPSHOT_JSON_FIELD_ID = 'fldSHTxfnC'; // 文本2用于保存计算页面快照(JSON)
const PROCESS_VERSION_FIELD_ID = 'fldwk5X7Yw'; // 版本字段
const PROCESS_TIMELINESS_FIELD_ID = 'fldEYCXnWt'; // 时效字段(天)
2025-11-11 17:39:18 +08:00
const PROCESS_STYLE_FIELD_ID = 'fld8xVqHJW'; // 款式字段(流程数据表)
const PROCESS_COLOR_FIELD_ID = 'fld3F1zGYe'; // 颜色字段(流程数据表)
2025-11-06 16:19:08 +08:00
// 货期记录表相关常量
const DELIVERY_RECORD_TABLE_ID = 'tblwiA49gksQrnfg'; // 货期记录表ID
const DELIVERY_FOREIGN_ID_FIELD_ID = 'fld0gAIcHS'; // foreign_id字段需要替换为实际字段ID
const DELIVERY_LABELS_FIELD_ID = 'fldp0cDP2T'; // 标签汇总字段需要替换为实际字段ID
const DELIVERY_STYLE_FIELD_ID = 'fldJRFxwB1'; // 款式字段需要替换为实际字段ID
const DELIVERY_COLOR_FIELD_ID = 'fldhA1uBMy'; // 颜色字段需要替换为实际字段ID
const DELIVERY_CREATE_TIME_FIELD_ID = 'fldP4w79LQ'; // 生成时间字段需要替换为实际字段ID
const DELIVERY_EXPECTED_DATE_FIELD_ID = 'fldrjlzsxn'; // 预计交付日期字段需要替换为实际字段ID
const DELIVERY_NODE_DETAILS_FIELD_ID = 'fldu1KL9yC'; // 节点详情字段需要替换为实际字段ID
const DELIVERY_CUSTOMER_EXPECTED_DATE_FIELD_ID = 'fldYNluU8D'; // 客户期望日期字段
const DELIVERY_ADJUSTMENT_INFO_FIELD_ID = 'fldNc6nNsz'; // 货期调整信息字段需要替换为实际字段ID
const DELIVERY_VERSION_FIELD_ID = 'fld5OmvZrn'; // 版本字段(新增)
const DELIVERY_SNAPSHOT_JSON_FIELD_ID = 'fldEYIvHeP'; // 货期记录表:完整快照(JSON)
// 起始时间字段(货期记录表新增)
const DELIVERY_START_TIME_FIELD_ID = 'fld727qCAv';
// 文本2字段货期记录表新增
const DELIVERY_TEXT2_FIELD_ID = 'fldG6LZnmU';
2025-11-12 11:39:56 +08:00
// 记录ID文本字段货期记录表新增
const DELIVERY_RECORD_IDS_FIELD_ID = 'fldq3u7h7H';
2025-11-06 16:19:08 +08:00
2025-11-13 15:59:08 +08:00
// 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看板计划版本公式数字
2025-11-06 16:19:08 +08:00
// 已移除中国法定节假日相关常量和配置
// 这个变量声明也不需要了
// const nodeNameToOptionId = new Map();
// 统一的日期格式化函数
const formatDate = (date: Date | string, formatType: keyof typeof DATE_FORMATS = 'DISPLAY_WITH_TIME'): string => {
try {
if (!date) return '';
const dateObj = typeof date === 'string' ? parseDate(date) : date;
if (!dateObj || isNaN(dateObj.getTime())) {
return '';
}
return format(dateObj, DATE_FORMATS[formatType], { locale: zhCN });
} catch (error) {
console.error('日期格式化失败:', error, { date, formatType });
return '';
}
};
// 统一的日期解析函数
const parseDate = (dateStr: string): Date | null => {
try {
if (!dateStr || dateStr.includes('未找到') || dateStr.includes('时效值为0')) {
return null;
}
// 如果是时间戳格式,直接转换
if (/^\d{13}$/.test(dateStr)) {
const date = new Date(parseInt(dateStr));
if (!isNaN(date.getTime())) {
return date;
}
}
// 移除所有星期信息(支持"星期X"和"周X"格式)
let cleanStr = dateStr
.replace(/\s*星期[一二三四五六日天]\s*/g, ' ')
.replace(/\s*周[一二三四五六日天]\s*/g, ' ')
.replace(/\s+/g, ' ')
.trim();
// 处理可能的格式问题:如果日期和时间连在一起了,添加空格
cleanStr = cleanStr.replace(/(\d{4}-\d{2}-\d{2})(\d{2}:\d{2})/, '$1 $2');
// 尝试标准解析
let date = new Date(cleanStr);
if (!isNaN(date.getTime())) {
return date;
}
// 手动解析 "YYYY-MM-DD HH:mm" 或 "YYYY-MM-DD HH:mm:ss" 格式
const match = cleanStr.match(/(\d{4})-(\d{2})-(\d{2})(?:\s+(\d{2}):(\d{2})(?::(\d{2}))?)?/);
if (match) {
const [, year, month, day, hour = '0', minute = '0', second = '0'] = match;
return new Date(
parseInt(year),
parseInt(month) - 1,
parseInt(day),
parseInt(hour),
parseInt(minute),
parseInt(second)
);
}
throw new Error(`无法解析日期格式: ${cleanStr}`);
} catch (error) {
console.error('日期解析失败:', { dateStr, error: error.message });
return null;
}
};
// 获取星期几(统一格式)
const getDayOfWeek = (dateStr: string | Date): string => {
try {
const date = typeof dateStr === 'string' ? parseDate(dateStr) : dateStr;
if (!date || isNaN(date.getTime())) {
return '';
}
return WEEKDAYS[date.getDay()];
} catch (error) {
console.error('获取星期失败:', error, { dateStr });
return '';
}
};
// 重构计算实际跨度天数函数
const calculateActualDays = (startDateStr: string | Date, endDateStr: string | Date): number => {
try {
const startDate = typeof startDateStr === 'string' ? parseDate(startDateStr) : startDateStr;
const endDate = typeof endDateStr === 'string' ? parseDate(endDateStr) : endDateStr;
if (!startDate || !endDate || isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
console.log('日期解析失败:', { startDateStr, endDateStr, startDate, endDate });
return 0;
}
// 计算日期差异(只考虑日期部分)
const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
const diffTime = endDateOnly.getTime() - startDateOnly.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
console.log('自然日计算:', {
startDateStr: formatDate(startDate),
endDateStr: formatDate(endDate),
diffDays
});
return Math.max(0, diffDays);
} catch (error) {
console.error('计算自然日出错:', error, { startDateStr, endDateStr });
return 0;
}
};
// 计算跳过的周末天数 - 支持空的休息日配置
const calculateSkippedWeekends = (startDate: Date | string, endDate: Date | string, weekendDays: number[] = []): number => {
if (weekendDays.length === 0) return 0; // 如果没有配置休息日,则不跳过任何天数
try {
const start = typeof startDate === 'string' ? new Date(startDate) : startDate;
const end = typeof endDate === 'string' ? new Date(endDate) : endDate;
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
return 0;
}
let count = 0;
const current = new Date(start);
while (current <= end) {
if (weekendDays.includes(current.getDay())) {
count++;
}
current.setDate(current.getDate() + 1);
}
return count;
} catch (error) {
return 0;
}
};
// 计算时间范围内实际跳过的自定义日期
const calculateExcludedDatesInRange = (startDate: Date | string, endDate: Date | string, excludedDates: string[] = []): { count: number, dates: string[] } => {
if (!excludedDates || excludedDates.length === 0) {
return { count: 0, dates: [] };
}
try {
const start = typeof startDate === 'string' ? parseDate(startDate) : startDate;
const end = typeof endDate === 'string' ? parseDate(endDate) : endDate;
if (!start || !end || isNaN(start.getTime()) || isNaN(end.getTime())) {
return { count: 0, dates: [] };
}
const actualExcludedDates: string[] = [];
for (const excludedDateStr of excludedDates) {
const excludedDate = parseDate(excludedDateStr);
if (excludedDate && excludedDate >= start && excludedDate <= end) {
actualExcludedDates.push(excludedDateStr);
}
}
return { count: actualExcludedDates.length, dates: actualExcludedDates };
} catch (error) {
console.error('计算跳过日期失败:', error);
return { count: 0, dates: [] };
}
};
// 已移除法定节假日跳过统计函数
// 已移除中国节假日判断函数
// 判断是否为自定义周末 - 支持空的休息日配置
const isCustomWeekend = (date: Date, weekendDays: number[] = []): boolean => {
if (weekendDays.length === 0) return false; // 如果没有配置休息日,则不认为是周末
return weekendDays.includes(date.getDay());
};
// 判断是否为工作日 - 排除表格休息日、以及节点自定义不参与计算日期
const isBusinessDay = (date: Date, weekendDays: number[] = [], excludedDates: string[] = []): boolean => {
try {
const dateStr = format(date, 'yyyy-MM-dd');
const isExcluded = Array.isArray(excludedDates) && excludedDates.includes(dateStr);
return !isCustomWeekend(date, weekendDays) && !isExcluded;
} catch {
return !isCustomWeekend(date, weekendDays);
}
};
// 日期调整函数
const adjustStartDateByRule = (date: Date, ruleJson: string): Date => {
if (!ruleJson || ruleJson.trim() === '') {
return date;
}
try {
const config = JSON.parse(ruleJson);
const adjustedDate = new Date(date);
const currentDayOfWeek = adjustedDate.getDay(); // 0=周日, 1=周一, ..., 6=周六
// 转换为1-7格式1=周一, 7=周日)
const dayOfWeek = currentDayOfWeek === 0 ? 7 : currentDayOfWeek;
if (config.rules && Array.isArray(config.rules)) {
for (const rule of config.rules) {
if (rule.condition === 'dayOfWeek' && rule.value === dayOfWeek) {
if (rule.action === 'delayToNextWeek') {
// 计算下周目标日期
const targetDay = rule.targetDay || 1; // 默认周一
const daysToAdd = 7 - dayOfWeek + targetDay;
adjustedDate.setDate(adjustedDate.getDate() + daysToAdd);
console.log(`应用规则: ${rule.description || '未知规则'}, 原日期: ${format(date, 'yyyy-MM-dd')}, 调整后: ${format(adjustedDate, 'yyyy-MM-dd')}`);
break; // 只应用第一个匹配的规则
}
}
}
}
return adjustedDate;
} catch (error) {
console.error('解析日期调整规则失败:', error);
return date; // 解析失败时返回原日期
}
};
// JSON格式日期调整函数 - 修改为返回调整结果和描述
const adjustStartDateByJsonRule = (date: Date, ruleJson: string): { adjustedDate: Date, description?: string } => {
if (!ruleJson || ruleJson.trim() === '') {
return { adjustedDate: date };
}
try {
const rules = JSON.parse(ruleJson);
const dayOfWeek = date.getDay(); // 0=周日, 1=周一, ..., 6=周六
const dayKey = dayOfWeek === 0 ? '7' : dayOfWeek.toString(); // 转换为1-7格式
const rule = rules[dayKey];
if (!rule) {
return { adjustedDate: date }; // 没有匹配的规则,返回原日期
}
const adjustedDate = new Date(date);
let description = rule.description || '';
switch (rule.action) {
case 'delayInSameWeek':
// 在本周内延期到指定星期几
const targetDay = rule.targetDay;
const currentDay = dayOfWeek === 0 ? 7 : dayOfWeek; // 转换为1-7格式
if (targetDay > currentDay) {
// 延期到本周的目标日期
adjustedDate.setDate(adjustedDate.getDate() + (targetDay - currentDay));
} else {
// 如果目标日期已过,延期到下周的目标日期
adjustedDate.setDate(adjustedDate.getDate() + (7 - currentDay + targetDay));
}
break;
case 'delayToNextWeek':
// 延期到下周的指定星期几
const nextWeekTargetDay = rule.targetDay;
const daysToAdd = 7 - (dayOfWeek === 0 ? 7 : dayOfWeek) + nextWeekTargetDay;
adjustedDate.setDate(adjustedDate.getDate() + daysToAdd);
break;
case 'delayDays':
// 直接延期指定天数
const delayDays = rule.days || 0;
adjustedDate.setDate(adjustedDate.getDate() + delayDays);
break;
default:
console.warn(`未知的调整动作: ${rule.action}`);
break;
}
console.log(`应用调整规则: ${dayKey} -> ${JSON.stringify(rule)}, 原日期: ${date.toDateString()}, 调整后: ${adjustedDate.toDateString()}`);
return { adjustedDate, description };
} catch (error) {
console.error('解析日期调整规则失败:', error, '规则内容:', ruleJson);
return { adjustedDate: date }; // 解析失败时返回原日期
}
};
// 调整到下一个工作时间开始点
const adjustToNextWorkingHour = (date: Date, calculationMethod: string, weekendDays: number[] = [], excludedDates: string[] = []): Date => {
const result = new Date(date);
if (calculationMethod === '内部') {
const hour = result.getHours();
const minute = result.getMinutes();
// 如果是工作时间外18:00之后或9:00之前调整到下一个工作日的9:00
if (hour >= 18 || hour < 9) {
// 如果是当天18:00之后调整到下一个工作日
if (hour >= 18) {
result.setDate(result.getDate() + 1);
}
// 找到下一个工作日(考虑休息日和自定义跳过日期)
while (!isBusinessDay(result, weekendDays, excludedDates)) {
result.setDate(result.getDate() + 1);
}
// 设置为9:00:00
result.setHours(9, 0, 0, 0);
}
}
return result;
};
// 内部工作时间计算函数
const addInternalBusinessTime = (startDate: Date, businessDays: number, weekendDays: number[] = [], excludedDates: string[] = []): Date => {
const result = new Date(startDate);
2025-11-13 15:59:08 +08:00
if (!businessDays || businessDays === 0) return result;
const isNegative = businessDays < 0;
const absDays = Math.abs(businessDays);
2025-11-06 16:19:08 +08:00
2025-11-13 15:59:08 +08:00
// 处理整数天(仅计入工作日)
const wholeDays = Math.floor(absDays);
let processedDays = 0;
while (processedDays < wholeDays) {
result.setDate(result.getDate() + (isNegative ? -1 : 1));
2025-11-06 16:19:08 +08:00
if (isBusinessDay(result, weekendDays, excludedDates)) {
2025-11-13 15:59:08 +08:00
processedDays++;
2025-11-06 16:19:08 +08:00
}
}
// 处理小数部分按9小时工作制
2025-11-13 15:59:08 +08:00
const fractionalDays = absDays - wholeDays;
2025-11-06 16:19:08 +08:00
if (fractionalDays > 0) {
2025-11-13 15:59:08 +08:00
const workingHours = fractionalDays * 9; // 内部按9小时工作制
2025-11-06 16:19:08 +08:00
let currentHour = result.getHours();
let currentMinute = result.getMinutes();
2025-11-13 15:59:08 +08:00
if (!isNegative) {
// 正向:确保在工作时间内开始
if (currentHour < 9) {
currentHour = 9;
currentMinute = 0;
} else if (currentHour >= 18) {
// 跳到下一个工作日的9:00
2025-11-06 16:19:08 +08:00
result.setDate(result.getDate() + 1);
2025-11-13 15:59:08 +08:00
while (!isBusinessDay(result, weekendDays, excludedDates)) {
result.setDate(result.getDate() + 1);
}
currentHour = 9;
currentMinute = 0;
2025-11-06 16:19:08 +08:00
}
2025-11-13 15:59:08 +08:00
const totalMinutes = currentHour * 60 + currentMinute + workingHours * 60;
const finalHour = Math.floor(totalMinutes / 60);
const finalMinute = totalMinutes % 60;
if (finalHour >= 18) {
const overflowHours = finalHour - 18;
const overflowMinutes = finalMinute;
2025-11-06 16:19:08 +08:00
result.setDate(result.getDate() + 1);
2025-11-13 15:59:08 +08:00
while (!isBusinessDay(result, weekendDays, excludedDates)) {
result.setDate(result.getDate() + 1);
}
result.setHours(9 + overflowHours, overflowMinutes, 0, 0);
} else {
result.setHours(finalHour, finalMinute, 0, 0);
2025-11-06 16:19:08 +08:00
}
} else {
2025-11-13 15:59:08 +08:00
// 负向:从当前时间向前回退工作小时,规范到工作时间窗口
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);
}
2025-11-06 16:19:08 +08:00
}
}
return result;
};
// 添加工作日 - 使用表格配置的休息日与节点自定义跳过日期
const addBusinessDaysWithHolidays = (startDate: Date, businessDays: number, weekendDays: number[] = [], excludedDates: string[] = []): Date => {
const result = new Date(startDate);
2025-11-13 15:59:08 +08:00
if (!businessDays || businessDays === 0) return result;
const isNegative = businessDays < 0;
const absDays = Math.abs(businessDays);
let processedDays = 0;
2025-11-06 16:19:08 +08:00
2025-11-13 15:59:08 +08:00
// 先处理整数工作日
const wholeDays = Math.floor(absDays);
while (processedDays < wholeDays) {
result.setDate(result.getDate() + (isNegative ? -1 : 1));
2025-11-06 16:19:08 +08:00
if (isBusinessDay(result, weekendDays, excludedDates)) {
2025-11-13 15:59:08 +08:00
processedDays++;
2025-11-06 16:19:08 +08:00
}
}
2025-11-13 15:59:08 +08:00
// 再处理小数部分按24小时制
const fractionalDays = absDays - wholeDays;
2025-11-06 16:19:08 +08:00
if (fractionalDays > 0) {
2025-11-13 15:59:08 +08:00
const hours = fractionalDays * 24;
result.setHours(result.getHours() + (isNegative ? -hours : hours));
2025-11-06 16:19:08 +08:00
}
return result;
};
// 获取标签数据
const fetchLabelOptions = async () => {
setLabelLoading(true);
try {
// 获取标签表
const labelTable = await bitable.base.getTable(LABEL_TABLE_ID);
// 1. 先获取所有字段的元数据
const fieldMetaList = await labelTable.getFieldMetaList();
// 2. 筛选出标签1-标签10的字段
const labelFields: {[key: string]: string} = {}; // 存储字段名到字段ID的映射
for (const fieldMeta of fieldMetaList) {
// 检查字段名是否匹配标签1-标签10的模式
const match = fieldMeta.name.match(/^标签([1-9]|10)$/);
if (match) {
const labelKey = `标签${match[1]}`;
labelFields[labelKey] = fieldMeta.id;
}
}
console.log('找到的标签字段:', labelFields);
// 3. 处理标签数据 - 从字段选项获取而不是从记录数据获取
const options: {[key: string]: any[]} = {};
// 初始化十个标签的选项数组
for (let i = 1; i <= 10; i++) {
options[`标签${i}`] = [];
}
// 4. 遍历标签字段,获取每个字段的选项
for (const [labelKey, fieldId] of Object.entries(labelFields)) {
try {
const field = await labelTable.getField(fieldId);
const fieldMeta = await field.getMeta();
// 检查是否是选择字段(单选或多选)
if (fieldMeta.type === FieldType.SingleSelect || fieldMeta.type === FieldType.MultiSelect) {
const selectField = field as any; // 类型断言
const fieldOptions = await selectField.getOptions();
// 转换为我们需要的格式
options[labelKey] = fieldOptions.map((option: any) => ({
label: option.name,
value: option.name
}));
}
} catch (error) {
console.warn(`获取${labelKey}字段选项失败:`, error);
// 如果获取选项失败,保持空数组
options[labelKey] = [];
}
}
console.log('处理后的标签选项:', options);
setLabelOptions(options);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'success',
message: `标签选项加载成功,共找到 ${Object.keys(labelFields).length} 个标签字段`
});
}
} catch (error) {
console.error('获取标签选项失败:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'error',
message: '获取标签选项失败,请检查表格配置'
});
}
} finally {
setLabelLoading(false);
}
};
// 组件加载时获取标签数据和初始化起始时间
useEffect(() => {
const initializeData = async () => {
await fetchLabelOptions();
await initializeStartTime();
await loadAvailableTables(); // 加载可用表列表
};
initializeData();
}, []);
// 初始化起始时间:从货期记录表获取,新记录则使用当前时间
const initializeStartTime = async () => {
try {
const selection = await bitable.base.getSelection();
const recordId = selection?.recordId || '';
const tableId = selection?.tableId || '';
if (recordId && tableId === DELIVERY_RECORD_TABLE_ID) {
// 如果选中的是货期记录表的记录,从中获取起始时间
const deliveryTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID);
const deliveryRecord = await deliveryTable.getRecordById(recordId);
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;
}
}
2025-11-13 15:59:08 +08:00
} 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);
}
2025-11-06 16:19:08 +08:00
}
// 如果没有找到有效的起始时间,使用当前时间
setStartTime(new Date());
} catch (error) {
console.error('初始化起始时间失败:', error);
// 出错时使用当前时间作为默认值
setStartTime(new Date());
}
};
2025-11-13 15:59:08 +08:00
// 根据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;
}
};
2025-11-06 16:19:08 +08:00
// 加载可用表列表
const loadAvailableTables = async () => {
try {
setTablesLoading(true);
const tableMetaList = await bitable.base.getTableMetaList();
const tables = tableMetaList.map(table => ({
id: table.id,
name: table.name
}));
setAvailableTables(tables);
} catch (error) {
console.error('加载表列表失败:', error);
} finally {
setTablesLoading(false);
}
};
// 加载指定表的视图列表
const loadAvailableViews = async (tableId: string) => {
if (!tableId) {
setAvailableViews([]);
return;
}
try {
setViewsLoading(true);
const table = await bitable.base.getTable(tableId);
const viewMetaList = await table.getViewMetaList();
const views = viewMetaList.map(view => ({
id: view.id,
name: view.name
}));
setAvailableViews(views);
} catch (error) {
console.error('加载视图列表失败:', error);
setAvailableViews([]);
} finally {
setViewsLoading(false);
}
};
// 处理表选择变化
const handleBatchTableChange = (tableId: string) => {
// 切换批量来源表时重置全局变量,避免旧状态残留
resetGlobalState();
setSelectedBatchTableId(tableId);
setSelectedBatchViewId(''); // 重置视图选择
loadAvailableViews(tableId); // 加载新表的视图列表
};
// 处理标签选择变化
const handleLabelChange = (labelKey: string, value: string | string[]) => {
setSelectedLabels(prev => ({
...prev,
[labelKey]: value
}));
};
// 清空标签选择
const handleClearLabels = () => {
setSelectedLabels({});
setExpectedDate(null); // 清空客户期望日期
};
// 计算预计开始和完成时间
const handleCalculateTimeline = async (
skipValidation: boolean = false,
overrideData?: {
selectedRecords?: string[],
recordDetails?: any[],
selectedLabels?: {[key: string]: string | string[]},
expectedDate?: Date | null,
startTime?: Date | null
},
showUI: boolean = true // 新增参数控制是否显示UI
) => {
// 使用传入的数据或全局状态
const currentSelectedRecords = overrideData?.selectedRecords || selectedRecords;
const currentRecordDetails = overrideData?.recordDetails || recordDetails;
const currentSelectedLabels = overrideData?.selectedLabels || selectedLabels;
const currentExpectedDate = overrideData?.expectedDate || expectedDate;
const currentStartTime = overrideData?.startTime || startTime;
console.log('=== handleCalculateTimeline - 使用的数据 ===');
console.log('currentSelectedRecords:', currentSelectedRecords);
console.log('currentSelectedLabels:', currentSelectedLabels);
console.log('currentExpectedDate:', currentExpectedDate);
console.log('currentStartTime:', currentStartTime);
// 跳过验证(用于批量模式)
if (!skipValidation) {
// 检查是否选择了多条记录
if (currentSelectedRecords.length > 1) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'warning',
message: '计算时效功能仅支持单条记录,请重新选择单条记录后再试'
});
}
return;
}
// 检查是否选择了记录
if (currentSelectedRecords.length === 0) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'warning',
message: '请先选择一条记录'
});
}
return;
}
// 检查是否选择了标签
const hasSelectedLabels = Object.values(currentSelectedLabels).some(value => {
return Array.isArray(value) ? value.length > 0 : Boolean(value);
});
if (!hasSelectedLabels) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'warning',
message: '请先选择至少一个标签'
});
}
return;
}
// 可选:检查是否选择了客户期望日期
if (!currentExpectedDate) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'info',
message: '建议选择客户期望日期以便更好地进行时效计算'
});
}
}
}
// 移除冗余日志:客户期望日期输出
setTimelineLoading(true);
// 生成模式:输出当前数据结构,便于与批量模式对比
try {
console.group('=== 生成模式:计算时效 - 当前数据结构 ===');
// 模式与核心输入
console.log('mode:', mode);
console.log('selectedRecords:', currentSelectedRecords);
console.log('recordDetails:', currentRecordDetails);
console.log('selectedLabels:', currentSelectedLabels);
console.log('expectedDate:', currentExpectedDate);
console.groupEnd();
} catch (logErr) {
console.warn('生成模式结构化日志输出失败:', logErr);
}
try {
// 构建业务选择的所有标签值集合(用于快速查找)
const businessLabelValues = new Set<string>();
for (const [labelKey, selectedValue] of Object.entries(currentSelectedLabels)) {
if (selectedValue) {
const values = Array.isArray(selectedValue) ? selectedValue : [selectedValue];
values.forEach(value => {
if (typeof value === 'string' && value.trim()) {
businessLabelValues.add(value.trim());
}
});
}
}
// 已移除冗余日志:业务选择标签值
const timelineTable = await bitable.base.getTable(TIMELINE_TABLE_ID);
const timelineFieldMetaList = await timelineTable.getFieldMetaList();
const timelineLabelFields: {[key: string]: string} = {};
// 构建时效表的标签字段映射(只执行一次)
let relationshipFieldId = ''; // 关系字段ID
let calculationMethodFieldId = ''; // 时效计算方式字段ID
for (const fieldMeta of timelineFieldMetaList) {
const match = fieldMeta.name.match(/^标签([1-9]|10)$/);
if (match) {
const labelKey = `标签${match[1]}`;
timelineLabelFields[labelKey] = fieldMeta.id;
}
// 查找关系字段
if (fieldMeta.name === '关系' || fieldMeta.id === 'fldaIDAhab') {
relationshipFieldId = fieldMeta.id;
}
// 查找时效计算方式字段
if (fieldMeta.name === '时效计算方式' || fieldMeta.id === 'fldxfLZNUu') {
calculationMethodFieldId = fieldMeta.id;
}
}
console.log('时效表标签字段映射:', timelineLabelFields);
console.log('关系字段ID:', relationshipFieldId);
// 1. 先获取匹配的流程节点(复用预览功能的逻辑)
const processTable = await bitable.base.getTable(PROCESS_CONFIG_TABLE_ID);
const processRecordsResult = await processTable.getRecords({
pageSize: 5000
});
const processRecords = processRecordsResult.records || [];
const matchedProcessNodes: any[] = [];
// 匹配流程配置节点
for (const record of processRecords) {
const fields = record.fields;
const processLabels = fields[PROCESS_LABEL_FIELD_ID];
const nodeName = fields[NODE_NAME_FIELD_ID];
const processOrder = fields[PROCESS_ORDER_FIELD_ID]; // 获取流程顺序
if (!processLabels || !nodeName) continue;
// 处理流程配置表中的标签数据 - 修复多选字段处理
let processLabelTexts: string[] = [];
if (typeof processLabels === 'string') {
processLabelTexts = [processLabels];
} else if (processLabels && processLabels.text) {
processLabelTexts = [processLabels.text];
} else if (Array.isArray(processLabels) && processLabels.length > 0) {
// 处理多选字段,获取所有选项的值
processLabelTexts = processLabels.map(item => {
if (typeof item === 'string') {
return item;
} else if (item && item.text) {
return item.text;
} else if (item && item.value) {
return item.value;
}
return String(item);
}).filter(text => text && text.trim());
}
// 处理节点名称
let nodeNameText = '';
if (typeof nodeName === 'string') {
nodeNameText = nodeName;
} else if (nodeName.text) {
nodeNameText = nodeName.text;
}
// 处理流程顺序
let orderValue = 0;
if (typeof processOrder === 'number') {
orderValue = processOrder;
} else if (typeof processOrder === 'string') {
orderValue = parseInt(processOrder) || 0;
} else if (processOrder && processOrder.value !== undefined) {
orderValue = processOrder.value;
}
if (processLabelTexts.length === 0 || !nodeNameText) continue;
// 检查是否匹配当前选择的标签 - 修复匹配逻辑
let isMatched = false;
const matchedLabels: string[] = [];
for (const [labelKey, labelValue] of Object.entries(currentSelectedLabels)) {
if (!labelValue) continue;
const valuesToCheck = Array.isArray(labelValue) ? labelValue : [labelValue];
for (const value of valuesToCheck) {
// 检查用户选择的值是否在流程配置的任何一个标签选项中
const isValueMatched = processLabelTexts.some(processLabelText => {
// 直接匹配
if (processLabelText === value) {
return true;
}
// 包含匹配(用于处理可能的格式差异)
if (processLabelText.includes(value)) {
return true;
}
return false;
});
if (isValueMatched) {
isMatched = true;
matchedLabels.push(`${labelKey}: ${value}`);
console.log(`匹配成功: ${labelKey} = ${value}`);
} else {
console.log(`匹配失败: ${labelKey} = ${value}, 流程配置标签: [${processLabelTexts.join(', ')}]`);
}
}
}
// 处理休息日配置 - 完全从表格字段获取
let weekendDays: number[] = [];
const weekendDaysField = fields[WEEKEND_DAYS_FIELD_ID]; // 获取休息日配置
if (weekendDaysField) {
console.log('原始休息日字段数据:', weekendDaysField);
if (Array.isArray(weekendDaysField)) {
// 多选字段返回数组,每个元素可能是选项对象
weekendDays = weekendDaysField.map(item => {
// 处理选项对象 {id: "xxx", text: "0"} 或直接的值
if (item && typeof item === 'object') {
// 如果是选项对象取text或id字段
const value = item.text || item.id || item.value;
if (typeof value === 'string') {
const parsed = parseInt(value);
return !isNaN(parsed) && parsed >= 0 && parsed <= 6 ? parsed : null;
} else if (typeof value === 'number') {
return value >= 0 && value <= 6 ? value : null;
}
} else if (typeof item === 'string') {
const parsed = parseInt(item);
return !isNaN(parsed) && parsed >= 0 && parsed <= 6 ? parsed : null;
} else if (typeof item === 'number') {
return item >= 0 && item <= 6 ? item : null;
}
return null;
}).filter(day => day !== null) as number[];
} else if (typeof weekendDaysField === 'string') {
// 如果是字符串,尝试解析
try {
const parsed = JSON.parse(weekendDaysField);
if (Array.isArray(parsed)) {
weekendDays = parsed.filter(day => typeof day === 'number' && day >= 0 && day <= 6);
}
} catch {
// 如果解析失败,尝试按逗号分割
const parts = weekendDaysField.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n) && n >= 0 && n <= 6);
if (parts.length > 0) {
weekendDays = parts;
}
}
} else if (typeof weekendDaysField === 'number' && weekendDaysField >= 0 && weekendDaysField <= 6) {
weekendDays = [weekendDaysField];
}
console.log('解析后的休息日配置:', weekendDays);
}
// 如果表格中没有配置休息日或配置为空,则该节点没有固定休息日
// 这样就完全依赖表格数据,不会有任何硬编码的默认值
// 处理不参与计算日期(自定义跳过日期)
let excludedDates: string[] = [];
const excludedDatesField = fields[EXCLUDED_DATES_FIELD_ID];
if (excludedDatesField) {
console.log('原始不参与计算日期字段数据:', excludedDatesField);
if (Array.isArray(excludedDatesField)) {
excludedDates = excludedDatesField.map(item => {
if (item && typeof item === 'object') {
const val = item.text || item.name || item.value || item.id || '';
return String(val).trim();
}
return String(item).trim();
}).filter(d => !!d);
} else if (typeof excludedDatesField === 'string') {
excludedDates = excludedDatesField.split(/[\,\s]+/).map(s => s.trim()).filter(Boolean);
} else if (excludedDatesField && typeof excludedDatesField === 'object' && excludedDatesField.text) {
excludedDates = [String(excludedDatesField.text).trim()].filter(Boolean);
}
console.log('解析后的不参与计算日期:', excludedDates);
}
if (isMatched) {
// 获取起始日期调整规则
const startDateRule = fields[START_DATE_RULE_FIELD_ID];
// 获取JSON格式的日期调整规则
const dateAdjustmentRule = fields[DATE_ADJUSTMENT_RULE_FIELD_ID];
matchedProcessNodes.push({
id: record.id,
nodeName: nodeNameText,
processLabels: processLabelTexts.join(', '),
matchedLabels: matchedLabels,
processOrder: orderValue, // 添加流程顺序
weekendDays: weekendDays, // 添加休息日配置
excludedDates: excludedDates, // 添加自定义跳过日期
startDateRule: startDateRule, // 添加起始日期调整规则
dateAdjustmentRule: dateAdjustmentRule // 添加JSON格式日期调整规则
});
}
}
// 按流程顺序排序
matchedProcessNodes.sort((a, b) => a.processOrder - b.processOrder);
console.log('按顺序排列的流程节点:', matchedProcessNodes);
// 2. 优化:预先获取所有时效数据并建立索引
const timelineRecordsResult = await timelineTable.getRecords({
pageSize: 5000
});
const timelineRecords = timelineRecordsResult.records || [];
// 优化2预处理时效数据建立节点名称到记录的映射
const timelineIndexByNode = new Map<string, any[]>();
for (const timelineRecord of timelineRecords) {
const timelineFields = timelineRecord.fields;
const timelineNodeName = timelineFields[TIMELINE_NODE_FIELD_ID];
// 处理时效表中的节点名称
let timelineNodeNames: string[] = [];
if (typeof timelineNodeName === 'string') {
timelineNodeNames = timelineNodeName.split(',').map(name => name.trim());
} else if (Array.isArray(timelineNodeName)) {
timelineNodeNames = timelineNodeName.map(item => {
if (typeof item === 'string') {
return item.trim();
} else if (item && item.text) {
return item.text.trim();
}
return '';
}).filter(name => name);
} else if (timelineNodeName && timelineNodeName.text) {
timelineNodeNames = timelineNodeName.text.split(',').map(name => name.trim());
}
// 为每个节点名称建立索引
for (const nodeName of timelineNodeNames) {
const normalizedName = nodeName.toLowerCase();
if (!timelineIndexByNode.has(normalizedName)) {
timelineIndexByNode.set(normalizedName, []);
}
timelineIndexByNode.get(normalizedName)!.push({
record: timelineRecord,
fields: timelineFields
});
}
}
console.log('时效数据索引构建完成,节点数量:', timelineIndexByNode.size);
const results: any[] = [];
// 3. 按顺序为每个匹配的流程节点查找时效数据并计算累积时间
let cumulativeStartTime = currentStartTime ? new Date(currentStartTime) : new Date(); // 累积开始时间
for (let i = 0; i < matchedProcessNodes.length; i++) {
const processNode = matchedProcessNodes[i];
let timelineValue = null;
let matchedTimelineRecord = null;
// 优化3使用索引快速查找候选记录
const normalizedProcessName = processNode.nodeName.trim().toLowerCase();
const candidateRecords = timelineIndexByNode.get(normalizedProcessName) || [];
console.log(`节点 ${processNode.nodeName} 找到 ${candidateRecords.length} 个候选时效记录`);
// 在候选记录中进行标签匹配
let matchedCandidates = []; // 收集所有匹配的候选记录
for (const candidate of candidateRecords) {
const { record: timelineRecord, fields: timelineFields } = candidate;
// 优化4使用预构建的字段映射进行标签匹配
let isLabelsMatched = true;
// 检查时效表中每个有值的标签是否都包含在业务标签中
for (const [labelKey, timelineFieldId] of Object.entries(timelineLabelFields)) {
const timelineLabelValue = timelineFields[timelineFieldId];
// 处理时效表中的标签值
let timelineLabelTexts: string[] = [];
if (typeof timelineLabelValue === 'string' && timelineLabelValue.trim()) {
timelineLabelTexts = [timelineLabelValue.trim()];
} else if (timelineLabelValue && timelineLabelValue.text && timelineLabelValue.text.trim()) {
timelineLabelTexts = [timelineLabelValue.text.trim()];
} else if (Array.isArray(timelineLabelValue) && timelineLabelValue.length > 0) {
// 多选字段,获取所有值
timelineLabelTexts = timelineLabelValue.map(item => {
if (typeof item === 'string') return item.trim();
if (item && item.text) return item.text.trim();
return '';
}).filter(text => text);
}
// 如果时效表中该标签有值,则检查该标签的所有值是否都包含在业务标签中
if (timelineLabelTexts.length > 0) {
let allValuesMatched = true;
// 优化5使用Set的has方法进行快速查找
for (const timelineText of timelineLabelTexts) {
if (!businessLabelValues.has(timelineText)) {
allValuesMatched = false;
console.log(`时效表标签 ${labelKey} 的值 "${timelineText}" 不在业务选择的标签中`);
break;
}
}
if (!allValuesMatched) {
console.log(`时效表标签 ${labelKey} 的值 [${timelineLabelTexts.join(', ')}] 不完全包含在业务选择的标签中`);
isLabelsMatched = false;
break;
} else {
console.log(`时效表标签 ${labelKey} 的值 [${timelineLabelTexts.join(', ')}] 完全匹配成功`);
}
}
// 如果时效表中该标签为空,则跳过检查(空标签不影响匹配)
}
// 只有标签匹配成功才获取时效数据
if (isLabelsMatched) {
// 找到匹配的节点,获取时效值
const timelineValueField = timelineFields[TIMELINE_FIELD_ID];
// 获取关系字段值
const relationshipField = timelineFields[relationshipFieldId];
// 获取时效计算方式字段值
const calculationMethodField = timelineFields[calculationMethodFieldId];
if (timelineValueField !== null && timelineValueField !== undefined) {
let candidateTimelineValue = 0;
// 解析时效值
if (typeof timelineValueField === 'number') {
candidateTimelineValue = timelineValueField;
} else if (typeof timelineValueField === 'string') {
const parsedValue = parseFloat(timelineValueField);
candidateTimelineValue = isNaN(parsedValue) ? 0 : parsedValue;
} else if (timelineValueField && typeof timelineValueField === 'object' && timelineValueField.value !== undefined) {
candidateTimelineValue = timelineValueField.value;
}
// 获取计算方式
let calculationMethod = '外部'; // 默认值
if (calculationMethodField) {
if (typeof calculationMethodField === 'string') {
calculationMethod = calculationMethodField;
} else if (calculationMethodField && typeof calculationMethodField === 'object' && calculationMethodField.text) {
calculationMethod = calculationMethodField.text;
}
}
// 根据计算方式转换时效值(小时转天)
let convertedTimelineValue = 0;
if (calculationMethod === '内部') {
convertedTimelineValue = Math.round((candidateTimelineValue / 9) * 1000) / 1000; // 精确到3位小数
console.log(`内部计算方式: ${candidateTimelineValue}小时 → ${convertedTimelineValue.toFixed(3)}`);
} else {
convertedTimelineValue = Math.round((candidateTimelineValue / 24) * 1000) / 1000; // 精确到3位小数
console.log(`外部计算方式: ${candidateTimelineValue}小时 → ${convertedTimelineValue.toFixed(3)}`);
}
// 获取关系类型
let relationshipType = '默认';
if (relationshipField) {
if (typeof relationshipField === 'string') {
relationshipType = relationshipField;
} else if (relationshipField && typeof relationshipField === 'object' && relationshipField.text) {
relationshipType = relationshipField.text;
}
}
// 调试时效记录对象结构
console.log('时效记录对象结构:', timelineRecord);
console.log('记录ID字段:', {
id: timelineRecord.id,
recordId: timelineRecord.recordId,
_id: timelineRecord._id,
record_id: timelineRecord.record_id
});
matchedCandidates.push({
record: timelineRecord,
timelineValue: convertedTimelineValue, // 使用转换后的值
relationshipType: relationshipType,
calculationMethod: calculationMethod, // 记录计算方式
originalHours: candidateTimelineValue // 保留原始小时值用于日志
});
// 移除单条记录的详细日志输出,避免日志冗余
// console.log(`节点 ${processNode.nodeName} 找到匹配记录:`, {
// 记录ID: timelineRecord.id || timelineRecord.recordId || timelineRecord._id || timelineRecord.record_id,
// 原始时效值小时: candidateTimelineValue,
// 转换后天数: convertedTimelineValue,
// 关系类型: relationshipType,
// 时效计算方式: calculationMethod
// });
}
}
}
// 在 matchedCandidates 处理之前定义 processingRule
let processingRule = '默认'; // 默认值
// 根据关系字段和时效计算方式决定如何处理多个匹配的记录
if (matchedCandidates.length > 0) {
// 检查所有匹配记录的关系类型和计算方式
const relationshipTypes = [...new Set(matchedCandidates.map(c => c.relationshipType))];
const calculationMethods = [...new Set(matchedCandidates.map(c => c.calculationMethod))];
if (relationshipTypes.length > 1) {
console.warn(`节点 ${processNode.nodeName} 存在多种关系类型:`, relationshipTypes);
}
if (calculationMethods.length > 1) {
console.warn(`节点 ${processNode.nodeName} 存在多种时效计算方式:`, calculationMethods);
}
// 使用第一个匹配记录的关系类型和计算方式作为处理方式
const primaryRelationshipType = matchedCandidates[0].relationshipType;
const primaryCalculationMethod = matchedCandidates[0].calculationMethod;
// 添加调试日志
console.log(`节点 ${processNode.nodeName} 调试信息:`);
console.log('primaryCalculationMethod:', primaryCalculationMethod);
console.log('primaryRelationshipType:', primaryRelationshipType);
console.log('所有关系类型:', relationshipTypes);
console.log('所有计算方式:', calculationMethods);
let finalTimelineValue = 0;
// 使用关系字段决定处理方式(累加值、最大值、默认)
processingRule = primaryRelationshipType || '默认';
console.log('processingRule:', processingRule);
if (processingRule === '累加值') {
finalTimelineValue = matchedCandidates.reduce((sum, candidate) => sum + candidate.timelineValue, 0);
const totalOriginalHours = matchedCandidates.reduce((sum, candidate) => sum + candidate.originalHours, 0);
console.log(`节点 ${processNode.nodeName} 累加值处理 - 找到 ${matchedCandidates.length} 条匹配记录:`);
matchedCandidates.forEach((candidate, index) => {
console.log(` 记录${index + 1}: ID=${candidate.record.id || candidate.record.recordId}, 时效=${candidate.originalHours}小时(${candidate.timelineValue}天), 计算方式=${candidate.calculationMethod}`);
});
console.log(`累加结果: 总计${totalOriginalHours}小时 → ${finalTimelineValue}`);
matchedTimelineRecord = matchedCandidates[0].record;
} else if (processingRule === '最大值') {
finalTimelineValue = Math.max(...matchedCandidates.map(c => c.timelineValue));
const maxCandidate = matchedCandidates.find(c => c.timelineValue === finalTimelineValue);
console.log(`节点 ${processNode.nodeName} 最大值处理 - 找到 ${matchedCandidates.length} 条匹配记录:`);
matchedCandidates.forEach((candidate, index) => {
console.log(` 记录${index + 1}: ID=${candidate.record.id || candidate.record.recordId}, 时效=${candidate.originalHours}小时(${candidate.timelineValue}天), 计算方式=${candidate.calculationMethod}`);
});
console.log(`最大值结果: ${maxCandidate?.originalHours}小时(${maxCandidate?.calculationMethod}) → ${finalTimelineValue}`);
matchedTimelineRecord = maxCandidate?.record || matchedCandidates[0].record;
} else {
finalTimelineValue = matchedCandidates[0].timelineValue;
console.log(`节点 ${processNode.nodeName} 默认处理 - 找到 ${matchedCandidates.length} 条匹配记录:`);
matchedCandidates.forEach((candidate, index) => {
console.log(` 记录${index + 1}: ID=${candidate.record.id || candidate.record.recordId}, 时效=${candidate.originalHours}小时(${candidate.timelineValue}天), 计算方式=${candidate.calculationMethod}`);
});
console.log(`默认结果: 使用第一条记录 ${matchedCandidates[0].originalHours}小时(${matchedCandidates[0].calculationMethod}) → ${finalTimelineValue}`);
matchedTimelineRecord = matchedCandidates[0].record;
}
timelineValue = finalTimelineValue;
// matchedTimelineRecord 已在上面的处理逻辑中设置
}
// 计算当前节点的开始和完成时间(使用工作日计算)
const calculateTimeline = (startDate: Date, timelineValue: number, calculationMethod: string = '外部') => {
// 根据计算方式调整开始时间
const adjustedStartDate = adjustToNextWorkingHour(startDate, calculationMethod, processNode.weekendDays, processNode.excludedDates || []);
let endDate: Date;
if (calculationMethod === '内部') {
// 使用内部工作时间计算
endDate = addInternalBusinessTime(adjustedStartDate, timelineValue, processNode.weekendDays, processNode.excludedDates || []);
} else {
// 使用原有的24小时制计算
endDate = addBusinessDaysWithHolidays(adjustedStartDate, timelineValue, processNode.weekendDays, processNode.excludedDates || []);
}
return {
startDate: formatDate(adjustedStartDate, 'STORAGE_FORMAT'),
endDate: formatDate(endDate, 'STORAGE_FORMAT')
};
};
let nodeStartTime = new Date(cumulativeStartTime);
// 应用起始日期调整规则
if (processNode.startDateRule) {
let ruleJson = '';
if (typeof processNode.startDateRule === 'string') {
ruleJson = processNode.startDateRule;
} else if (processNode.startDateRule && processNode.startDateRule.text) {
ruleJson = processNode.startDateRule.text;
}
if (ruleJson.trim()) {
nodeStartTime = adjustStartDateByRule(nodeStartTime, ruleJson);
console.log(`节点 ${processNode.nodeName} 应用起始日期调整规则后的开始时间:`, formatDate(nodeStartTime));
}
}
// 应用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 => item.type === 'text')
.map(item => item.text)
.join('');
} else if (processNode.dateAdjustmentRule && processNode.dateAdjustmentRule.text) {
ruleText = processNode.dateAdjustmentRule.text;
}
console.log('解析后的ruleText:', ruleText);
console.log('调整前的nodeStartTime:', formatDate(nodeStartTime));
if (ruleText && ruleText.trim() !== '') {
const result = adjustStartDateByJsonRule(nodeStartTime, ruleText);
nodeStartTime = result.adjustedDate;
ruleDescription = result.description || '';
console.log(`节点 ${processNode.nodeName} 应用JSON日期调整规则后的开始时间:`, formatDate(nodeStartTime));
}
}
// 获取当前节点的计算方式
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) :
{
startDate: formatDate(nodeStartTime, 'STORAGE_FORMAT'),
endDate: '未找到时效数据'
};
let nodeEndTime: Date;
if (timelineValue) {
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, processNode.weekendDays, processNode.excludedDates || []);
if (nodeCalculationMethod === '内部') {
nodeEndTime = addInternalBusinessTime(adjustedStartTime, timelineValue, processNode.weekendDays, processNode.excludedDates || []);
} else {
nodeEndTime = addBusinessDaysWithHolidays(adjustedStartTime, timelineValue, processNode.weekendDays, processNode.excludedDates || []);
}
} else {
nodeEndTime = new Date(nodeStartTime);
}
// 计算跳过的天数
const skippedWeekends = calculateSkippedWeekends(nodeStartTime, nodeEndTime, processNode.weekendDays);
const actualDays = calculateActualDays(timelineResult.startDate, timelineResult.endDate);
// 计算时间范围内实际跳过的自定义日期
const excludedDatesInRange = calculateExcludedDatesInRange(nodeStartTime, nodeEndTime, processNode.excludedDates || []);
results.push({
processOrder: processNode.processOrder,
nodeName: processNode.nodeName,
matchedLabels: processNode.matchedLabels,
timelineValue: timelineValue,
estimatedStart: timelineResult.startDate,
estimatedEnd: timelineResult.endDate,
timelineRecordId: matchedTimelineRecord ?
(matchedTimelineRecord.id || matchedTimelineRecord.recordId || matchedTimelineRecord._id || matchedTimelineRecord.record_id) : null,
// 新增:保存所有匹配记录的信息(用于累加情况)
allMatchedRecords: matchedCandidates.length > 1 ? matchedCandidates.map(candidate => ({
recordId: candidate.record.id || candidate.record.recordId || candidate.record._id || candidate.record.record_id,
timelineValue: candidate.timelineValue,
originalHours: candidate.originalHours,
calculationMethod: candidate.calculationMethod,
relationshipType: candidate.relationshipType
})) : null,
// 新增:标识是否为累加处理
isAccumulated: processingRule === '累加值' && matchedCandidates.length > 1,
weekendDaysConfig: processNode.weekendDays, // 新增:保存休息日配置用于显示
excludedDates: processNode.excludedDates || [], // 新增:保存不参与计算日期用于显示与快照
// 新增:保存时间范围内实际跳过的日期
actualExcludedDates: excludedDatesInRange.dates,
actualExcludedDatesCount: excludedDatesInRange.count,
calculationMethod: nodeCalculationMethod, // 新增:保存计算方式
ruleDescription: ruleDescription, // 新增:保存规则描述
skippedWeekends: skippedWeekends,
actualDays: actualDays,
// 新增:保存调整规则用于重新计算
startDateRule: processNode.startDateRule,
dateAdjustmentRule: processNode.dateAdjustmentRule,
adjustmentDescription: ruleDescription // 新增:保存调整规则描述
});
// 更新累积时间:当前节点的完成时间成为下一个节点的开始时间
if (timelineValue) {
cumulativeStartTime = new Date(nodeEndTime);
}
console.log(`节点 ${processNode.nodeName} (顺序: ${processNode.processOrder}):`, {
开始时间: formatDate(nodeStartTime),
完成时间: formatDate(nodeEndTime),
时效天数: timelineValue,
计算方式: nodeCalculationMethod
});
}
setTimelineResults(results);
if (showUI) {
setTimelineVisible(true);
}
console.log('按流程顺序计算的时效结果:', results);
return results; // 返回计算结果
} catch (error) {
console.error('计算时效失败:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'error',
message: '计算时效失败,请检查表格配置'
});
}
throw error; // 重新抛出错误
} finally {
setTimelineLoading(false);
}
};
// 复合调整处理函数:根据缓冲期和交期余量状态决定调整方式
const handleComplexAdjustment = (nodeIndex: number, adjustment: number) => {
// 计算当前状态
const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0);
const baseBuferDays = baseBufferDays;
// 智能缓冲期计算逻辑
let dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments);
// 只有在缓冲期为0时才检查最终限制最后节点预计完成时间是否已达到客户期望日期
let hasReachedFinalLimit = false;
if (dynamicBufferDays === 0 && expectedDate && timelineResults.length > 0) {
// 获取有效的最后流程完成日期
let effectiveLastProcess = null;
let lastCompletionDate = null;
for (let i = timelineResults.length - 1; i >= 0; i--) {
const process = timelineResults[i];
const processDate = new Date(process.estimatedEnd);
if (!isNaN(processDate.getTime()) && process.estimatedEnd !== '时效值为0') {
effectiveLastProcess = process;
lastCompletionDate = processDate;
break;
}
}
if (!effectiveLastProcess) {
effectiveLastProcess = timelineResults[timelineResults.length - 1];
lastCompletionDate = new Date(effectiveLastProcess.estimatedEnd);
}
// 计算最后流程完成日期(包含调整)
const adjustedCompletionDate = new Date(lastCompletionDate);
adjustedCompletionDate.setDate(adjustedCompletionDate.getDate() + completionDateAdjustment);
// 检查是否已达到客户期望日期
const timeDiffToExpected = expectedDate.getTime() - adjustedCompletionDate.getTime();
const daysToExpected = Math.ceil(timeDiffToExpected / (1000 * 60 * 60 * 24));
hasReachedFinalLimit = daysToExpected <= 0;
}
// 如果已达到最终限制且是正向调整,则禁止操作
if (hasReachedFinalLimit && adjustment > 0) {
return;
}
// 调整逻辑:
// 1. 优先使用缓冲期
// 2. 缓冲期为0时通过调整节点时效值实现连锁传递
if (adjustment > 0) {
// 正数调整:需要扣减
if (dynamicBufferDays > 0) {
// 1. 缓冲期 > 0优先扣减缓冲期通过调整节点时间
const updatedResults = handleTimelineAdjustment(nodeIndex, adjustment);
// 如果调整失败,不进行后续操作
if (!updatedResults) return;
} else {
// 2. 缓冲期 = 0通过调整节点时效值实现连锁传递
const updatedResults = handleTimelineAdjustment(nodeIndex, adjustment);
// 如果调整失败,不进行后续操作
if (!updatedResults) return;
}
} else if (adjustment < 0) {
// 负数调整:需要恢复
if (completionDateAdjustment > 0) {
// 1. 优先恢复最后流程完成日期的调整
const restoreAmount = Math.min(-adjustment, completionDateAdjustment);
setCompletionDateAdjustment(completionDateAdjustment - restoreAmount);
const remainingRestore = -adjustment - restoreAmount;
if (remainingRestore > 0) {
// 2. 如果还有剩余恢复量,恢复缓冲期(通过调整节点时间)
const updatedResults = handleTimelineAdjustment(nodeIndex, -remainingRestore);
// 如果调整失败,不进行后续操作
if (!updatedResults) return;
}
} else if (totalAdjustments > 0) {
// 直接恢复缓冲期
const updatedResults = handleTimelineAdjustment(nodeIndex, adjustment);
// 如果调整失败,不进行后续操作
if (!updatedResults) return;
}
}
};
// 调整时效值的函数
const handleTimelineAdjustment = (nodeIndex: number, adjustment: number) => {
const newAdjustments = { ...timelineAdjustments };
const currentAdjustment = newAdjustments[nodeIndex] || 0;
const newAdjustment = currentAdjustment + adjustment;
2025-11-13 15:59:08 +08:00
// 允许调整后的时效值为负数,用于向前回退结束时间
2025-11-06 16:19:08 +08:00
const baseValue = (typeof timelineResults[nodeIndex]?.timelineValue === 'number')
? timelineResults[nodeIndex]!.timelineValue
: (typeof timelineResults[nodeIndex]?.adjustedTimelineValue === 'number')
? timelineResults[nodeIndex]!.adjustedTimelineValue
: 0;
// 检查当前调整的节点是否为周转周期节点
const currentNodeName = timelineResults[nodeIndex]?.nodeName;
const isTurnoverNode = currentNodeName === '周转周期';
// 如果调整的是周转周期节点,直接返回(禁用手动调整)
if (isTurnoverNode) {
return null;
}
newAdjustments[nodeIndex] = newAdjustment;
// 查找周转周期节点的索引
const turnoverNodeIndex = timelineResults.findIndex(result => result.nodeName === '周转周期');
// 如果存在周转周期节点,进行反向调整
if (turnoverNodeIndex !== -1 && turnoverNodeIndex !== nodeIndex) {
const turnoverCurrentAdjustment = newAdjustments[turnoverNodeIndex] || 0;
const turnoverBaseValue = (typeof timelineResults[turnoverNodeIndex]?.timelineValue === 'number')
? timelineResults[turnoverNodeIndex]!.timelineValue
: (typeof timelineResults[turnoverNodeIndex]?.adjustedTimelineValue === 'number')
? timelineResults[turnoverNodeIndex]!.adjustedTimelineValue
: 0;
// 计算周转周期的反向调整值
const turnoverNewAdjustment = turnoverCurrentAdjustment - adjustment;
// 确保周转周期调整后的值不小于0
if (turnoverBaseValue + turnoverNewAdjustment >= 0) {
newAdjustments[turnoverNodeIndex] = turnoverNewAdjustment;
}
}
setTimelineAdjustments(newAdjustments);
// 使用智能重算逻辑,只重算被调整的节点及其后续节点
recalculateTimeline(newAdjustments);
return timelineResults;
};
// 获取重新计算后的时间线结果(不更新状态)
const getRecalculatedTimeline = (adjustments: {[key: number]: number}) => {
const updatedResults = [...timelineResults];
let cumulativeStartTime = startTime ? new Date(startTime) : new Date(); // 从起始时间开始
for (let i = 0; i < updatedResults.length; 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;
// 计算当前节点的开始时间
let nodeStartTime = new Date(cumulativeStartTime);
// 获取节点的计算方式、周末天数和排除日期
const nodeCalculationMethod = result.calculationMethod || 'external';
const nodeWeekendDays = result.weekendDays || [];
const nodeExcludedDates = result.excludedDates || [];
// 获取调整规则描述
const ruleDescription = result.ruleDescription || '';
2025-11-13 15:59:08 +08:00
// 计算节点的结束时间(允许负时效值向前回退)
2025-11-06 16:19:08 +08:00
let nodeEndTime;
2025-11-13 15:59:08 +08:00
if (adjustedTimelineValue !== 0) {
2025-11-06 16:19:08 +08:00
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates);
if (nodeCalculationMethod === 'internal') {
nodeEndTime = addInternalBusinessTime(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates);
} else {
nodeEndTime = addBusinessDaysWithHolidays(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates);
}
} else {
nodeEndTime = new Date(nodeStartTime);
}
// 计算跳过的天数
2025-11-13 15:59:08 +08:00
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates);
2025-11-06 16:19:08 +08:00
const skippedWeekends = calculateSkippedWeekends(adjustedStartTime, nodeEndTime, nodeWeekendDays);
const estimatedStartStr = formatDate(adjustedStartTime);
2025-11-13 15:59:08 +08:00
const estimatedEndStr = adjustedTimelineValue !== 0 ? formatDate(nodeEndTime) : '时效值为0';
2025-11-06 16:19:08 +08:00
const actualDays = calculateActualDays(estimatedStartStr, estimatedEndStr);
// 计算时间范围内实际跳过的自定义日期
const excludedDatesInRange = calculateExcludedDatesInRange(adjustedStartTime, nodeEndTime, nodeExcludedDates);
// 更新结果
updatedResults[i] = {
...result,
adjustedTimelineValue: adjustedTimelineValue,
estimatedStart: estimatedStartStr,
estimatedEnd: estimatedEndStr,
adjustment: adjustment,
calculationMethod: nodeCalculationMethod, // 保持计算方式
skippedWeekends: skippedWeekends,
actualDays: actualDays,
// 更新时间范围内实际跳过的日期
actualExcludedDates: excludedDatesInRange.dates,
actualExcludedDatesCount: excludedDatesInRange.count,
adjustmentDescription: result.adjustmentDescription, // 保持调整规则描述
ruleDescription: ruleDescription // 添加更新后的规则描述
};
// 更新累积时间:当前节点的完成时间成为下一个节点的开始时间
2025-11-13 15:59:08 +08:00
if (adjustedTimelineValue !== 0) {
2025-11-06 16:19:08 +08:00
cumulativeStartTime = new Date(nodeEndTime);
}
}
return updatedResults;
};
// 重新计算时间线的函数
const recalculateTimeline = (adjustments: {[key: number]: number}, forceRecalculateAll: boolean = false) => {
const updatedResults = [...timelineResults];
// 找到第一个被调整的节点索引
const adjustedIndices = Object.keys(adjustments).map(k => parseInt(k)).filter(i => adjustments[i] !== 0);
// 找到有实际完成时间的节点
const actualCompletionIndices = Object.keys(actualCompletionDates)
.map(k => parseInt(k))
.filter(i => actualCompletionDates[i] !== null && actualCompletionDates[i] !== undefined);
// 如果没有调整且不是强制重算,但有实际完成时间,需要重新计算
if (adjustedIndices.length === 0 && !forceRecalculateAll && actualCompletionIndices.length === 0) {
return; // 没有调整也没有实际完成时间,直接返回
}
// 确定第一个需要重新计算的节点索引
let firstAdjustedIndex: number;
if (forceRecalculateAll) {
firstAdjustedIndex = 0;
} else if (actualCompletionIndices.length > 0) {
// 如果有实际完成时间,从最早有实际完成时间的节点的下一个节点开始重新计算
const earliestActualCompletionIndex = Math.min(...actualCompletionIndices);
const earliestAdjustmentIndex = adjustedIndices.length > 0 ? Math.min(...adjustedIndices) : Infinity;
firstAdjustedIndex = Math.min(earliestActualCompletionIndex + 1, earliestAdjustmentIndex);
console.log(`检测到实际完成时间,从节点 ${firstAdjustedIndex} 开始重新计算`);
} else {
firstAdjustedIndex = adjustedIndices.length > 0 ? Math.min(...adjustedIndices) : 0;
}
// 确保索引不超出范围
firstAdjustedIndex = Math.max(0, Math.min(firstAdjustedIndex, updatedResults.length - 1));
// 确定累积开始时间
let cumulativeStartTime: Date;
if (firstAdjustedIndex === 0) {
// 如果调整的是第一个节点,从起始时间开始
cumulativeStartTime = startTime ? new Date(startTime) : new Date();
} else {
// 如果调整的不是第一个节点,从前一个节点的结束时间开始
const previousResult = updatedResults[firstAdjustedIndex - 1];
const previousIndex = firstAdjustedIndex - 1;
// 检查前一个节点是否有实际完成时间
if (actualCompletionDates[previousIndex]) {
// 使用实际完成时间作为下一个节点的开始时间
cumulativeStartTime = new Date(actualCompletionDates[previousIndex]!);
console.log(`节点 ${previousIndex} 使用实际完成时间: ${formatDate(cumulativeStartTime)}`);
} else {
// 使用预计完成时间
cumulativeStartTime = new Date(previousResult.estimatedEnd);
}
}
// 只重新计算从第一个调整节点开始的后续节点
for (let i = firstAdjustedIndex; i < updatedResults.length; 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;
// 计算当前节点的开始时间
let nodeStartTime = new Date(cumulativeStartTime);
// 重新应用起始日期调整规则
if (result.startDateRule) {
let ruleJson = '';
if (typeof result.startDateRule === 'string') {
ruleJson = result.startDateRule;
} else if (result.startDateRule && result.startDateRule.text) {
ruleJson = result.startDateRule.text;
}
if (ruleJson.trim()) {
nodeStartTime = adjustStartDateByRule(nodeStartTime, ruleJson);
}
}
// 重新应用JSON格式日期调整规则
let ruleDescription = result.ruleDescription; // 保持原有描述作为默认值
if (result.dateAdjustmentRule) {
let ruleText = '';
if (typeof result.dateAdjustmentRule === 'string') {
ruleText = result.dateAdjustmentRule;
} else if (Array.isArray(result.dateAdjustmentRule)) {
ruleText = result.dateAdjustmentRule
.filter(item => item.type === 'text')
.map(item => item.text)
.join('');
} else if (result.dateAdjustmentRule && result.dateAdjustmentRule.text) {
ruleText = result.dateAdjustmentRule.text;
}
if (ruleText && ruleText.trim() !== '') {
const adjustmentResult = adjustStartDateByJsonRule(nodeStartTime, ruleText);
nodeStartTime = adjustmentResult.adjustedDate;
// 更新规则描述
if (adjustmentResult.description) {
ruleDescription = adjustmentResult.description;
}
}
}
const nodeWeekendDays = result.weekendDaysConfig || []; // 使用节点特定的休息日配置
const nodeCalculationMethod = result.calculationMethod || '外部'; // 获取节点的计算方式
let nodeEndTime: Date;
const nodeExcludedDates = Array.isArray(result.excludedDates) ? result.excludedDates : [];
2025-11-13 15:59:08 +08:00
if (adjustedTimelineValue !== 0) {
2025-11-06 16:19:08 +08:00
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates);
if (nodeCalculationMethod === '内部') {
nodeEndTime = addInternalBusinessTime(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates);
} else {
nodeEndTime = addBusinessDaysWithHolidays(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays, nodeExcludedDates);
}
} else {
nodeEndTime = new Date(nodeStartTime);
}
// 计算跳过的天数
2025-11-13 15:59:08 +08:00
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates);
2025-11-06 16:19:08 +08:00
const skippedWeekends = calculateSkippedWeekends(adjustedStartTime, nodeEndTime, nodeWeekendDays);
const estimatedStartStr = formatDate(adjustedStartTime);
2025-11-13 15:59:08 +08:00
const estimatedEndStr = adjustedTimelineValue !== 0 ? formatDate(nodeEndTime) : '时效值为0';
2025-11-06 16:19:08 +08:00
const actualDays = calculateActualDays(estimatedStartStr, estimatedEndStr);
// 计算时间范围内实际跳过的自定义日期
const excludedDatesInRange = calculateExcludedDatesInRange(adjustedStartTime, nodeEndTime, nodeExcludedDates);
// 更新结果
updatedResults[i] = {
...result,
adjustedTimelineValue: adjustedTimelineValue,
estimatedStart: estimatedStartStr,
estimatedEnd: estimatedEndStr,
adjustment: adjustment,
calculationMethod: nodeCalculationMethod, // 保持计算方式
skippedWeekends: skippedWeekends,
actualDays: actualDays,
// 更新时间范围内实际跳过的日期
actualExcludedDates: excludedDatesInRange.dates,
actualExcludedDatesCount: excludedDatesInRange.count,
adjustmentDescription: result.adjustmentDescription, // 保持调整规则描述
ruleDescription: ruleDescription // 添加更新后的规则描述
};
// 更新累积时间:优先使用当前节点的实际完成时间,否则使用预计完成时间
2025-11-13 15:59:08 +08:00
if (adjustedTimelineValue !== 0) {
2025-11-06 16:19:08 +08:00
if (actualCompletionDates[i]) {
// 如果当前节点有实际完成时间,使用实际完成时间
cumulativeStartTime = new Date(actualCompletionDates[i]!);
} else {
// 否则使用预计完成时间
cumulativeStartTime = new Date(nodeEndTime);
}
}
}
setTimelineResults(updatedResults);
};
// 添加快照还原状态标志
const [isRestoringSnapshot, setIsRestoringSnapshot] = useState(false);
// 当起始时间变更时,重新以最新起始时间为基准重算全流程
useEffect(() => {
if (timelineResults.length > 0 && !isRestoringSnapshot) {
recalculateTimeline(timelineAdjustments, true); // 强制重算所有节点
}
}, [startTime, isRestoringSnapshot]);
// 重置调整的函数
const resetTimelineAdjustments = () => {
setTimelineAdjustments({});
setDeliveryMarginDeductions(0); // 同时重置交期余量扣减
setCompletionDateAdjustment(0); // 重置最后流程完成日期调整
setActualCompletionDates({}); // 重置实际完成日期
recalculateTimeline({}, true); // 强制重算所有节点
};
// 已移除未使用的 getTimelineLabelFieldId 辅助函数
// 写入货期记录表的函数
const writeToDeliveryRecordTable = async (
timelineResults: any[],
processRecordIds: string[],
timelineAdjustments: {[key: number]: number} = {},
batchData?: {
selectedRecords: string[],
recordDetails: any[],
labels?: Record<string, any>,
expectedDate?: any,
startTime?: any
}
) => {
try {
console.log('=== 开始写入货期记录表 ===');
console.log('当前模式:', mode);
console.log('timelineResults数量:', timelineResults?.length || 0);
console.log('processRecordIds数量:', processRecordIds?.length || 0);
console.log('timelineAdjustments:', timelineAdjustments);
console.log('timelineResults详情:', timelineResults);
console.log('processRecordIds详情:', processRecordIds);
// 获取货期记录表
console.log('正在获取货期记录表...');
const deliveryRecordTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID);
console.log('成功获取货期记录表');
// 检查字段是否存在
console.log('正在检查和获取所有必需字段...');
const fieldsToCheck = [
DELIVERY_FOREIGN_ID_FIELD_ID,
DELIVERY_LABELS_FIELD_ID,
DELIVERY_STYLE_FIELD_ID,
DELIVERY_COLOR_FIELD_ID,
DELIVERY_CREATE_TIME_FIELD_ID,
DELIVERY_EXPECTED_DATE_FIELD_ID,
DELIVERY_CUSTOMER_EXPECTED_DATE_FIELD_ID,
DELIVERY_NODE_DETAILS_FIELD_ID,
DELIVERY_ADJUSTMENT_INFO_FIELD_ID, // 添加货期调整信息字段
DELIVERY_START_TIME_FIELD_ID // 新增:起始时间字段
];
console.log('需要检查的字段ID列表:', fieldsToCheck);
// 获取各个字段
console.log('正在获取各个字段对象...');
const foreignIdField = await deliveryRecordTable.getField(DELIVERY_FOREIGN_ID_FIELD_ID);
const labelsField = await deliveryRecordTable.getField(DELIVERY_LABELS_FIELD_ID);
const styleField = await deliveryRecordTable.getField(DELIVERY_STYLE_FIELD_ID);
const colorField = await deliveryRecordTable.getField(DELIVERY_COLOR_FIELD_ID);
const text2Field = await deliveryRecordTable.getField(DELIVERY_TEXT2_FIELD_ID);
const createTimeField = await deliveryRecordTable.getField(DELIVERY_CREATE_TIME_FIELD_ID);
const expectedDateField = await deliveryRecordTable.getField(DELIVERY_EXPECTED_DATE_FIELD_ID);
const nodeDetailsField = await deliveryRecordTable.getField(DELIVERY_NODE_DETAILS_FIELD_ID);
const customerExpectedDateField = await deliveryRecordTable.getField(DELIVERY_CUSTOMER_EXPECTED_DATE_FIELD_ID);
const adjustmentInfoField = await deliveryRecordTable.getField(DELIVERY_ADJUSTMENT_INFO_FIELD_ID);
const versionField = await deliveryRecordTable.getField(DELIVERY_VERSION_FIELD_ID);
const startTimeField = await deliveryRecordTable.getField(DELIVERY_START_TIME_FIELD_ID);
const snapshotField = await deliveryRecordTable.getField(DELIVERY_SNAPSHOT_JSON_FIELD_ID);
2025-11-12 11:39:56 +08:00
const recordIdsTextField = await deliveryRecordTable.getField(DELIVERY_RECORD_IDS_FIELD_ID);
2025-11-06 16:19:08 +08:00
console.log('成功获取所有字段对象');
// 检查标签汇总字段的类型
console.log('标签汇总字段信息:', {
id: labelsField.id,
name: labelsField.name,
type: labelsField.type,
property: labelsField.property
});
// 获取foreign_id调整模式严格使用快照数据生成模式优先使用选择记录
console.log('=== 开始获取foreign_id ===');
let foreignId = '';
// 使用传递的数据或全局状态
const currentSelectedRecords = batchData?.selectedRecords || selectedRecords;
const currentRecordDetails = batchData?.recordDetails || recordDetails;
if (mode === 'adjust') {
// 调整模式严格使用快照回填的foreign_id即使为空也不回退
foreignId = currentForeignId;
console.log('调整模式严格使用快照恢复的foreign_id:', foreignId);
} else if (currentSelectedRecords.length > 0) {
// 生成模式:从选择记录获取
console.log('生成模式从选择记录获取foreign_id');
console.log('selectedRecords[0]:', currentSelectedRecords[0]);
if (batchData) {
// 批量模式:直接从传递的数据中获取
const firstRecord = currentRecordDetails[0];
if (firstRecord && firstRecord.fields && firstRecord.fields['fldpvBfeC0']) {
const fieldValue = firstRecord.fields['fldpvBfeC0'];
foreignId = extractText(fieldValue);
console.log('批量模式从传递数据获取foreign_id:', foreignId);
}
} else {
// 生成模式:从数据库获取
const table = await bitable.base.getTable(TABLE_ID);
const firstRecord = await table.getRecordById(currentSelectedRecords[0]);
console.log('获取到的记录:', firstRecord);
const fieldValue = firstRecord.fields['fldpvBfeC0'];
console.log('fldpvBfeC0字段值:', fieldValue);
foreignId = extractText(fieldValue);
}
}
// 生成模式的回退逻辑:记录详情
if (mode !== 'adjust' && !foreignId && currentRecordDetails.length > 0) {
const first = currentRecordDetails[0];
const val = first.fields['fldpvBfeC0'];
foreignId = extractText(val);
}
// 生成模式的最后回退:快照状态
if (mode !== 'adjust' && !foreignId && currentForeignId) {
foreignId = currentForeignId;
}
// 获取款式与颜色:调整模式优先使用快照数据,生成模式优先使用记录详情
let style = '';
let color = '';
if (mode === 'adjust') {
// 调整模式:严格使用快照回填的数据,即使为空也不回退
style = currentStyleText;
color = currentColorText;
console.log('调整模式:严格使用快照恢复的款式:', style, '颜色:', color);
} else {
// 生成模式:优先使用记录详情
if (currentRecordDetails.length > 0) {
const first = currentRecordDetails[0];
style = extractText(first.fields['fld6Uw95kt']) || currentStyleText || '';
color = extractText(first.fields['flde85ni4O']) || currentColorText || '';
} else {
// 回退:使用快照回填的状态
style = currentStyleText || '';
color = currentColorText || '';
// 若仍为空且有选择记录,仅做一次读取
if ((!style || !color) && currentSelectedRecords.length > 0) {
const table = await bitable.base.getTable(TABLE_ID);
const firstRecord = await table.getRecordById(currentSelectedRecords[0]);
style = style || extractText(firstRecord.fields['fld6Uw95kt']);
color = color || extractText(firstRecord.fields['flde85ni4O']);
}
}
}
// 获取文本2调整模式优先使用快照数据生成模式在批量模式下填写
let text2 = '';
if (mode === 'adjust') {
// 调整模式:严格使用快照回填的数据,即使为空也不回退
text2 = currentText2; // 直接使用快照值,不使用 || '' 的回退逻辑
console.log('调整模式严格使用快照恢复的文本2:', text2);
} else {
if (batchData && currentRecordDetails.length > 0) {
const first = currentRecordDetails[0];
if (first?.fields?.['fldG6LZnmU']) {
text2 = extractText(first.fields['fldG6LZnmU']);
}
// 兜底若批量数据中显式传递了text2则使用之
if (!text2 && (batchData as any).text2) {
try { text2 = extractText((batchData as any).text2); } catch { text2 = (batchData as any).text2; }
}
console.log('生成模式批量文本2来自批量数据:', text2);
} else {
// 非批量的生成模式:保持为空
text2 = '';
console.log('生成模式文本2字段保持为空');
}
}
// 获取标签汇总批量模式优先使用传入的labels
const selectedLabelValues = batchData?.labels
? Object.values(batchData.labels).flat().filter(Boolean)
: Object.values(selectedLabels).flat().filter(Boolean);
// 获取预计交付日期(交期余量的日期版本:最后流程完成日期 + 基础缓冲期)
let expectedDeliveryDate = null;
if (timelineResults.length > 0) {
// 从后往前查找第一个有效的流程完成日期(与交期余量计算逻辑一致)
let effectiveLastProcess = null;
for (let i = timelineResults.length - 1; i >= 0; i--) {
const process = timelineResults[i];
if (process.estimatedEnd &&
!process.estimatedEnd.includes('未找到') &&
!process.estimatedEnd.includes('时效值为0') &&
process.estimatedEnd !== 'Invalid Date') {
effectiveLastProcess = process;
break;
}
}
if (effectiveLastProcess) {
try {
const lastCompletionDate = new Date(effectiveLastProcess.estimatedEnd);
// 计算动态缓冲期:基础缓冲期 - 节点总调整量最小为0天
const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0);
const baseBuferDays = baseBufferDays;
const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments);
// 加上动态缓冲期
const deliveryDate = new Date(lastCompletionDate);
deliveryDate.setDate(deliveryDate.getDate() + dynamicBufferDays);
expectedDeliveryDate = deliveryDate.getTime();
} catch (error) {
console.error('转换预计交付日期失败:', error);
}
}
}
// 获取客户期望日期批量模式优先使用传入的expectedDate
let customerExpectedDate = null;
if (batchData?.expectedDate) {
try {
customerExpectedDate = new Date(batchData.expectedDate).getTime();
} catch { /* 忽略转换错误保持null */ }
} else if (expectedDate) {
customerExpectedDate = expectedDate.getTime(); // 转换为时间戳
}
// 创建当前时间戳
const currentTime = new Date().getTime();
// 计算版本号(数字)并格式化货期调整信息
let versionNumber = 1;
try {
if (mode === 'adjust' && currentVersionNumber !== null) {
// 调整模式优先使用快照version +1
versionNumber = currentVersionNumber + 1;
} else if (foreignId) {
const existing = await deliveryRecordTable.getRecords({
pageSize: 5000,
filter: {
conjunction: 'and',
conditions: [{
fieldId: DELIVERY_FOREIGN_ID_FIELD_ID,
operator: 'is',
value: [foreignId]
}]
}
});
const count = existing.records?.length || 0;
versionNumber = count + 1;
}
} catch (e) {
console.warn('计算版本号失败:', e);
}
let adjustmentInfo = `版本V${versionNumber}`;
if (Object.keys(timelineAdjustments).length > 0) {
const adjustmentTexts = Object.entries(timelineAdjustments).map(([nodeIndex, adjustment]) => {
const nodeName = timelineResults[parseInt(nodeIndex)]?.nodeName || `节点${parseInt(nodeIndex) + 1}`;
return `${nodeName}: ${adjustment > 0 ? '+' : ''}${adjustment.toFixed(1)}`;
});
adjustmentInfo += `\n当前调整\n${adjustmentTexts.join('\n')}`;
}
// 在创建Cell之前进行数据校验移除冗余日志
// ===== 构建完整快照(保持与流程数据表写入时的一致内容)并写入到货期记录表 =====
const expectedDateTimestamp = (batchData?.expectedDate || expectedDate)
? (batchData?.expectedDate || expectedDate).getTime()
: null;
const expectedDateString = (batchData?.expectedDate || expectedDate)
? format((batchData?.expectedDate || expectedDate), DATE_FORMATS.STORAGE_FORMAT)
: null;
const currentStartTime = batchData?.startTime || startTime;
const currentSelectedLabels = batchData?.labels || selectedLabels;
// 与快照字段保持相同的命名
const styleText = style || '';
const colorText = color || '';
const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0);
const baseBuferDays = baseBufferDays;
const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments);
// 检查是否达到最终限制
let hasReachedFinalLimit = false;
const currentExpectedDate = batchData?.expectedDate || expectedDate;
if (dynamicBufferDays === 0 && currentExpectedDate && timelineResults.length > 0) {
const lastNode = timelineResults[timelineResults.length - 1];
if (lastNode && lastNode.estimatedEnd && !lastNode.estimatedEnd.includes('未找到')) {
try {
const lastCompletionDate = new Date(lastNode.estimatedEnd);
const daysToExpected = Math.ceil((currentExpectedDate.getTime() - lastCompletionDate.getTime()) / (1000 * 60 * 60 * 24));
hasReachedFinalLimit = daysToExpected <= 0;
} catch (error) {
console.warn('计算最终限制状态失败:', error);
}
}
}
const globalSnapshot = {
version: versionNumber,
foreignId,
styleText,
colorText,
text2,
mode,
selectedLabels: currentSelectedLabels,
expectedDateTimestamp,
expectedDateString,
startTimestamp: currentStartTime ? currentStartTime.getTime() : undefined,
startString: currentStartTime ? formatDate(currentStartTime, 'STORAGE_FORMAT') : undefined,
timelineAdjustments,
generationModeState: {
currentForeignId,
currentStyleText,
currentColorText,
currentText2,
currentVersionNumber: versionNumber,
recordDetails: recordDetails || [],
hasSelectedLabels: Object.values(selectedLabels).some(value => {
return Array.isArray(value) ? value.length > 0 : Boolean(value);
}),
labelSelectionComplete: Object.keys(selectedLabels).length > 0
},
bufferManagement: {
baseDays: baseBuferDays,
totalAdjustments,
dynamicBufferDays,
hasReachedFinalLimit
},
chainAdjustmentSystem: {
enabled: true,
lastCalculationTime: new Date().getTime(),
adjustmentHistory: timelineAdjustments
},
timelineCalculationState: {
calculationTimestamp: new Date().getTime(),
totalNodes: timelineResults.length,
hasValidResults: timelineResults.length > 0,
lastCalculationMode: mode
},
totalNodes: timelineResults.length,
isGlobalSnapshot: true
};
// 选择用于快照的最后一个有效节点(与流程写入时的节点快照结构一致)
let selectedIndex = -1;
for (let i = timelineResults.length - 1; i >= 0; i--) {
const r = timelineResults[i];
if (r.estimatedEnd && !r.estimatedEnd.includes('未找到') && r.estimatedEnd.trim() !== '') {
selectedIndex = i;
break;
}
}
if (selectedIndex === -1) selectedIndex = 0; // 无有效结束时间时兜底为第一个
const selectedResult = timelineResults[selectedIndex];
let nodeStartTs = null as number | null;
let nodeEndTs = null as number | null;
if (selectedResult.estimatedStart && !selectedResult.estimatedStart.includes('未找到')) {
try { nodeStartTs = new Date(selectedResult.estimatedStart).getTime(); } catch {}
}
if (selectedResult.estimatedEnd && !selectedResult.estimatedEnd.includes('未找到')) {
try { nodeEndTs = new Date(selectedResult.estimatedEnd).getTime(); } catch {}
}
const nodeSnapshot = {
processOrder: selectedResult.processOrder,
nodeName: selectedResult.nodeName,
matchedLabels: selectedResult.matchedLabels,
timelineValue: selectedResult.timelineValue,
estimatedStart: selectedResult.estimatedStart,
estimatedEnd: selectedResult.estimatedEnd,
estimatedStartTimestamp: nodeStartTs,
estimatedEndTimestamp: nodeEndTs,
timelineRecordId: selectedResult.timelineRecordId,
allMatchedRecords: selectedResult.allMatchedRecords,
isAccumulated: selectedResult.isAccumulated,
weekendDaysConfig: selectedResult.weekendDaysConfig,
excludedDates: selectedResult.excludedDates,
actualExcludedDates: selectedResult.actualExcludedDates,
actualExcludedDatesCount: selectedResult.actualExcludedDatesCount,
calculationMethod: selectedResult.calculationMethod,
ruleDescription: selectedResult.ruleDescription,
skippedWeekends: selectedResult.skippedWeekends,
actualDays: selectedResult.actualDays,
adjustedTimelineValue: selectedResult.adjustedTimelineValue,
adjustment: selectedResult.adjustment,
adjustmentDescription: selectedResult.adjustmentDescription,
startDateRule: selectedResult.startDateRule,
dateAdjustmentRule: selectedResult.dateAdjustmentRule,
nodeCalculationState: {
2025-11-13 15:59:08 +08:00
hasValidTimelineValue: typeof selectedResult.timelineValue === 'number' && selectedResult.timelineValue !== 0,
2025-11-06 16:19:08 +08:00
hasValidStartTime: Boolean(nodeStartTs),
hasValidEndTime: Boolean(nodeEndTs),
calculationTimestamp: new Date().getTime(),
originalTimelineValue: selectedResult.timelineValue,
2025-11-13 15:59:08 +08:00
finalAdjustedValue: (selectedResult.adjustedTimelineValue ?? selectedResult.timelineValue)
2025-11-06 16:19:08 +08:00
},
chainAdjustmentNode: {
nodeIndex: selectedIndex,
hasAdjustment: selectedResult.adjustment !== undefined && selectedResult.adjustment !== 0,
adjustmentValue: selectedResult.adjustment || 0,
isChainSource: selectedResult.adjustment !== undefined && selectedResult.adjustment !== 0,
affectedByChain: selectedIndex > 0
},
isNodeSnapshot: true
};
const completeSnapshot = {
...globalSnapshot,
...nodeSnapshot,
timelineResults: timelineResults,
currentNodeIndex: selectedIndex,
currentNodeName: selectedResult.nodeName,
isCompleteSnapshot: true,
snapshotType: 'complete'
};
const snapshotJson = JSON.stringify(completeSnapshot);
// 使用createCell方法创建各个字段的Cell
const foreignIdCell = await foreignIdField.createCell(foreignId);
const labelsCell = selectedLabelValues.length > 0 ? await labelsField.createCell(selectedLabelValues) : null;
const styleCell = await styleField.createCell(style);
const colorCell = await colorField.createCell(color);
const text2Cell = await text2Field.createCell(text2);
const createTimeCell = await createTimeField.createCell(currentTime);
// 调试日志检查startTime参数
console.log('批量模式 - startTime参数:', startTime);
console.log('批量模式 - startTime类型:', typeof startTime);
console.log('批量模式 - currentTime:', currentTime, '对应日期:', new Date(currentTime).toLocaleString());
const startTimestamp = batchData?.startTime
? new Date(batchData.startTime).getTime()
: (startTime ? startTime.getTime() : currentTime);
console.log('批量模式 - 最终使用的startTimestamp:', startTimestamp, '对应日期:', new Date(startTimestamp).toLocaleString());
const startTimeCell = await startTimeField.createCell(startTimestamp);
const snapshotCell = await snapshotField.createCell(snapshotJson);
const expectedDateCell = expectedDeliveryDate ? await expectedDateField.createCell(expectedDeliveryDate) : null;
const customerExpectedDateCell = customerExpectedDate ? await customerExpectedDateField.createCell(customerExpectedDate) : null;
// 对于关联记录字段确保传入的是记录ID数组
const nodeDetailsCell = processRecordIds.length > 0 ?
await nodeDetailsField.createCell({ recordIds: processRecordIds }) : null;
// 创建货期调整信息Cell
const adjustmentInfoCell = adjustmentInfo ? await adjustmentInfoField.createCell(adjustmentInfo) : null;
// 创建版本号Cell数字
const versionCell = await versionField.createCell(versionNumber);
2025-11-12 11:39:56 +08:00
// 写回保持“原始文本格式”;如果读取为空,则写入空字符串
const recordIdsText = (restoredRecordIdsText && restoredRecordIdsText.trim() !== '')
? restoredRecordIdsText.trim()
: '';
const recordIdsCell = await recordIdsTextField.createCell(recordIdsText);
2025-11-06 16:19:08 +08:00
// 组合所有Cell到一个记录中
2025-11-12 11:39:56 +08:00
const recordCells = [foreignIdCell, styleCell, colorCell, text2Cell, createTimeCell, startTimeCell, versionCell, snapshotCell, recordIdsCell];
2025-11-06 16:19:08 +08:00
// 只有当数据存在时才添加对应的Cell
if (labelsCell) recordCells.push(labelsCell);
if (expectedDateCell) recordCells.push(expectedDateCell);
if (customerExpectedDateCell) recordCells.push(customerExpectedDateCell);
if (nodeDetailsCell) recordCells.push(nodeDetailsCell);
if (adjustmentInfoCell) recordCells.push(adjustmentInfoCell);
// 添加记录到货期记录表
const addedRecord = await deliveryRecordTable.addRecord(recordCells);
return addedRecord;
} catch (error) {
console.error('写入货期记录表详细错误:', {
error: error,
message: error.message,
stack: error.stack,
recordCellsLength: typeof recordCells !== 'undefined' ? recordCells.length : 'recordCells未定义'
});
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'error',
message: '写入货期记录表失败: ' + (error as Error).message
});
}
throw error;
}
};
// 写入流程数据表的函数
const writeToProcessDataTable = async (timelineResults: any[], batchData?: {
selectedRecords: string[],
recordDetails: any[],
labels?: Record<string, any>,
expectedDate?: any,
startTime?: any
}): Promise<string[]> => {
try {
console.log('=== 开始写入流程数据表 ===');
console.log('当前模式:', mode);
console.log('timelineResults数量:', timelineResults?.length || 0);
console.log('timelineResults详情:', timelineResults);
// 获取流程数据表和流程配置表
console.log('正在获取流程数据表和流程配置表...');
const processDataTable = await bitable.base.getTable(PROCESS_DATA_TABLE_ID);
const processConfigTable = await bitable.base.getTable(PROCESS_CONFIG_TABLE_ID);
console.log('成功获取数据表');
// 获取所有需要的字段
console.log('正在获取所有必需字段...');
const foreignIdField = await processDataTable.getField(FOREIGN_ID_FIELD_ID);
const processNameField = await processDataTable.getField(PROCESS_NAME_FIELD_ID);
const processOrderField = await processDataTable.getField(PROCESS_ORDER_FIELD_ID_DATA);
const startDateField = await processDataTable.getField(ESTIMATED_START_DATE_FIELD_ID);
const endDateField = await processDataTable.getField(ESTIMATED_END_DATE_FIELD_ID);
const versionField = await processDataTable.getField(PROCESS_VERSION_FIELD_ID);
const timelinessField = await processDataTable.getField(PROCESS_TIMELINESS_FIELD_ID);
2025-11-11 17:39:18 +08:00
const processStyleField = await processDataTable.getField(PROCESS_STYLE_FIELD_ID);
const processColorField = await processDataTable.getField(PROCESS_COLOR_FIELD_ID);
2025-11-06 16:19:08 +08:00
console.log('成功获取所有字段');
// 获取foreign_id - 支持批量模式直接传递数据
console.log('=== 开始获取foreign_id ===');
let foreignId = null;
// 使用传递的数据或全局状态
const currentSelectedRecords = batchData?.selectedRecords || selectedRecords;
const currentRecordDetails = batchData?.recordDetails || recordDetails;
console.log('selectedRecords数量:', currentSelectedRecords?.length || 0);
console.log('recordDetails数量:', currentRecordDetails?.length || 0);
console.log('selectedRecords:', currentSelectedRecords);
console.log('recordDetails:', currentRecordDetails);
if (currentSelectedRecords.length > 0 && currentRecordDetails.length > 0) {
// 从第一个选择的记录的详情中获取fldpvBfeC0字段的值
const firstRecord = currentRecordDetails[0];
if (firstRecord && firstRecord.fields && firstRecord.fields['fldpvBfeC0']) {
const fieldValue = firstRecord.fields['fldpvBfeC0'];
foreignId = extractText(fieldValue);
console.log('从fldpvBfeC0字段获取到的foreign_id:', foreignId);
} else {
console.warn('未在记录详情中找到fldpvBfeC0字段');
console.log('第一个记录的字段:', firstRecord?.fields);
}
}
// 快照回填在调整模式通过快照还原时使用当前foreign_id状态
if (!foreignId && currentForeignId) {
foreignId = currentForeignId;
console.log('使用快照恢复的foreign_id:', foreignId);
}
if (!foreignId) {
console.warn('未找到foreign_id跳过写入流程数据表');
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'warning',
message: '未找到foreign_id字段无法写入流程数据表'
});
}
return [];
}
2025-11-11 17:39:18 +08:00
// 获取款式与颜色:与货期记录写入逻辑保持一致
let style = '';
let color = '';
if (mode === 'adjust') {
// 调整模式:严格使用快照回填的数据
style = currentStyleText;
color = currentColorText;
console.log('调整模式:流程数据款式/颜色来自快照', { style, color });
} else {
// 生成模式:优先使用记录详情
if (currentRecordDetails.length > 0) {
const first = currentRecordDetails[0];
try { style = extractText(first.fields['fld6Uw95kt']); } catch { style = first?.fields?.['fld6Uw95kt'] || ''; }
try { color = extractText(first.fields['flde85ni4O']); } catch { color = first?.fields?.['flde85ni4O'] || ''; }
if (!style) style = currentStyleText || '';
if (!color) color = currentColorText || '';
} else {
// 回退:使用快照回填的状态
style = currentStyleText || '';
color = currentColorText || '';
// 若仍为空且有选择记录,仅做一次读取
if ((!style || !color) && currentSelectedRecords.length > 0) {
try {
const table = await bitable.base.getTable(TABLE_ID);
const firstRecord = await table.getRecordById(currentSelectedRecords[0]);
style = style || extractText(firstRecord.fields['fld6Uw95kt']);
color = color || extractText(firstRecord.fields['flde85ni4O']);
} catch (e) {
console.warn('读取源表款式/颜色失败,保持现有值', e);
}
}
}
}
2025-11-06 16:19:08 +08:00
// 先删除该foreign_id的所有现有记录
console.log('=== 开始删除现有记录 ===');
console.log('使用的foreign_id:', foreignId);
try {
console.log('正在查询现有记录...');
const existingRecords = await processDataTable.getRecords({
pageSize: 5000,
filter: {
conjunction: 'And',
conditions: [{
fieldId: FOREIGN_ID_FIELD_ID,
operator: 'is',
value: [actualForeignId]
}]
}
});
console.log('查询到现有记录数量:', existingRecords.records?.length || 0);
if (existingRecords.records && existingRecords.records.length > 0) {
const recordIdsToDelete = existingRecords.records.map(record => record.id);
console.log('准备删除的记录ID:', recordIdsToDelete);
await processDataTable.deleteRecords(recordIdsToDelete);
console.log(`成功删除 ${recordIdsToDelete.length} 条现有记录`);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'info',
message: `已删除 ${recordIdsToDelete.length} 条现有流程数据`
});
}
} else {
console.log('没有找到需要删除的现有记录');
}
} catch (deleteError) {
console.error('删除现有记录时出错:', deleteError);
// 继续执行,不中断流程
}
// 构建页面快照JSON确保可一模一样还原
// 计算版本号:数字。默认按 foreign_id 在货期记录表的记录数量 + 1
let versionNumber = 1;
try {
const deliveryRecordTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID);
if (mode === 'adjust' && currentVersionNumber !== null) {
// 调整模式优先使用快照version +1避免公式字段读取错误
versionNumber = currentVersionNumber + 1;
} else if (foreignId) {
const existing = await deliveryRecordTable.getRecords({
pageSize: 5000,
filter: {
conjunction: 'and',
conditions: [{
fieldId: DELIVERY_FOREIGN_ID_FIELD_ID,
operator: 'is',
value: [foreignId]
}]
}
});
const count = existing.records?.length || 0;
versionNumber = count + 1;
}
} catch (e) {
console.warn('计算版本号失败:', e);
}
// 使用createCell方法准备要写入的记录数据
const recordCellsToAdd = [];
for (let index = 0; index < timelineResults.length; index++) {
const result = timelineResults[index];
console.log('处理节点数据:', result);
// 检查是否有有效的预计完成时间(只检查结束时间)
const hasValidEndTime = (
result.estimatedEnd &&
!result.estimatedEnd.includes('未找到') &&
result.estimatedEnd.trim() !== ''
);
// 如果没有有效的预计完成时间,跳过这个节点
if (!hasValidEndTime) {
console.log(`跳过节点 "${result.nodeName}" - 未找到有效的预计完成时间`);
continue;
}
// 转换日期格式为时间戳(毫秒)
let startTimestamp = null;
let endTimestamp = null;
if (result.estimatedStart && !result.estimatedStart.includes('未找到')) {
try {
startTimestamp = new Date(result.estimatedStart).getTime();
} catch (error) {
console.error(`转换开始时间失败:`, error);
}
}
if (result.estimatedEnd && !result.estimatedEnd.includes('未找到')) {
try {
endTimestamp = new Date(result.estimatedEnd).getTime();
} catch (error) {
console.error(`转换结束时间失败:`, error);
}
}
// 使用createCell方法创建每个字段的Cell
const foreignIdCell = await foreignIdField.createCell(foreignId);
// 直接使用节点名称创建单元格
const processNameCell = await processNameField.createCell(result.nodeName);
const processOrderCell = await processOrderField.createCell(result.processOrder);
const startDateCell = startTimestamp ? await startDateField.createCell(startTimestamp) : null;
const endDateCell = endTimestamp ? await endDateField.createCell(endTimestamp) : null;
2025-11-11 17:39:18 +08:00
const styleCell = await processStyleField.createCell(style);
const colorCell = await processColorField.createCell(color);
2025-11-06 16:19:08 +08:00
// 组合所有Cell到一个记录中
const versionCell = await versionField.createCell(versionNumber);
const timelinessCell = (typeof result.timelineValue === 'number')
? await timelinessField.createCell(result.timelineValue)
: null;
const recordCells = [
foreignIdCell,
processNameCell,
processOrderCell,
2025-11-11 17:39:18 +08:00
styleCell,
colorCell,
2025-11-06 16:19:08 +08:00
versionCell
];
// 只有当时间戳存在时才添加日期Cell
if (startDateCell) recordCells.push(startDateCell);
if (endDateCell) recordCells.push(endDateCell);
if (timelinessCell) recordCells.push(timelinessCell);
console.log(`准备写入的Cell记录 - ${result.nodeName}:`, recordCells);
recordCellsToAdd.push(recordCells);
}
console.log('所有准备写入的Cell记录:', recordCellsToAdd);
// 在添加记录的部分收集记录ID
const addedRecordIds: string[] = [];
if (recordCellsToAdd.length > 0) {
try {
// 使用addRecords进行批量写入
const addedRecords = await processDataTable.addRecords(recordCellsToAdd);
// 直接使用返回值不需要map操作
addedRecordIds.push(...addedRecords);
console.log(`批量写入成功记录ID:`, addedRecords);
} catch (error) {
console.error('批量写入失败,尝试逐条写入:', error);
// 如果批量写入失败,回退到逐条写入
for (const recordCells of recordCellsToAdd) {
const addedRecord = await processDataTable.addRecord(recordCells);
addedRecordIds.push(addedRecord);
console.log('成功添加记录:', addedRecord);
}
}
console.log(`成功写入 ${addedRecordIds.length} 条流程数据`);
// 显示成功提示
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'success',
message: `成功写入 ${addedRecordIds.length} 条流程数据到流程数据表`
});
}
return addedRecordIds; // 返回记录ID列表
} else {
console.warn('没有有效的记录可以写入');
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'warning',
message: '没有有效的流程数据可以写入 - 所有节点都缺少时效数据'
});
}
return [];
}
} catch (error) {
console.error('写入流程数据表失败:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'error',
message: `写入流程数据表失败: ${(error as Error).message}`
});
}
return [];
}
};
// 批量处理函数
// 批量模式:模拟生成模式的操作(简化版本)
const simulateGenerationModeForBatch = async (
recordId: string,
extractedLabels: {[key: string]: string | string[]},
extractedStartTime: Date | null,
extractedExpectedDate: Date | null,
style: any,
color: any,
foreignId: any,
text2: any
) => {
console.log('=== 批量模式:模拟生成模式操作 ===');
console.log('recordId:', recordId);
console.log('extractedLabels:', extractedLabels);
console.log('extractedStartTime:', extractedStartTime);
console.log('extractedExpectedDate:', extractedExpectedDate);
// 1. 构造虚拟的 selectedRecords 和 recordDetails
const virtualSelectedRecords = [recordId];
const virtualRecordDetails = [{
id: recordId,
fields: {
'fldpvBfeC0': foreignId, // 外键ID字段
'fld6Uw95kt': style, // 样式字段
'flde85ni4O': color, // 颜色字段
'fldG6LZnmU': text2 // 文本字段2
}
}];
try {
// 不再设置全局状态直接通过overrideData传递数据
console.log('批量模式:直接调用计算逻辑,传递起始时间:', extractedStartTime);
// 3. 直接调用时效计算通过overrideData传递所有必要数据
const calculatedResults = await handleCalculateTimeline(true, {
selectedRecords: virtualSelectedRecords,
recordDetails: virtualRecordDetails,
selectedLabels: extractedLabels,
expectedDate: extractedExpectedDate,
startTime: extractedStartTime // 直接传递起始时间,避免异步状态更新问题
}, false); // showUI: false批量模式下不显示时间轴UI
console.log('批量模式:时效计算完成,结果:', calculatedResults);
// 批量模式更新UI显示最后处理记录的时间设置
console.log('批量模式更新UI显示时间设置');
setExpectedDate(extractedExpectedDate);
setStartTime(extractedStartTime);
// 批量模式:自动保存逻辑
console.log('批量模式:开始自动保存流程');
// 检查计算结果
if (!calculatedResults || calculatedResults.length === 0) {
console.error('批量模式:时效计算结果为空,无法保存');
throw new Error('时效计算结果为空,请检查流程配置和标签匹配');
}
// 自动触发保存
console.log('批量模式:开始自动保存数据');
await saveTimelineData({
timelineResults: calculatedResults, // 使用直接返回的结果
selectedRecords: virtualSelectedRecords,
recordDetails: virtualRecordDetails,
timelineAdjustments: {}, // 批量模式下没有手动调整
labels: extractedLabels,
expectedDate: extractedExpectedDate,
startTime: extractedStartTime
});
console.log('批量模式:自动保存完成');
} catch (error) {
console.error('批量模式:处理失败', error);
throw error; // 重新抛出错误让上层处理
}
};
// 保存时效数据的核心逻辑(从确定并保存按钮提取)
const saveTimelineData = async (batchData?: {
timelineResults?: any[],
selectedRecords?: string[],
recordDetails?: any[],
timelineAdjustments?: any,
labels?: Record<string, any>, // 添加标签汇总
expectedDate?: any, // 添加期望日期
startTime?: any // 添加起始时间
}) => {
try {
// 使用传入的数据或全局状态
const currentTimelineResults = batchData?.timelineResults || timelineResults;
const currentTimelineAdjustments = batchData?.timelineAdjustments || timelineAdjustments;
if (currentTimelineResults.length > 0) {
// 构造批量数据参数
const batchDataForWriting = batchData ? {
selectedRecords: batchData.selectedRecords || [],
recordDetails: batchData.recordDetails || [],
labels: batchData.labels, // 传递标签汇总
expectedDate: batchData.expectedDate, // 传递期望日期
startTime: batchData.startTime // 传递起始时间
} : undefined;
// 写入流程数据表
const processRecordIds = await writeToProcessDataTable(currentTimelineResults, batchDataForWriting);
// 写入货期记录表
await writeToDeliveryRecordTable(currentTimelineResults, processRecordIds, currentTimelineAdjustments, batchDataForWriting);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'success',
message: Object.keys(currentTimelineAdjustments).length > 0
? '已保存调整后的时效数据到流程数据表和货期记录表'
: '已保存计算的时效数据到流程数据表和货期记录表'
});
}
return true; // 保存成功
}
return false; // 没有数据需要保存
} catch (error) {
console.error('保存数据时出错:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'error',
message: '保存数据失败,请重试'
});
}
throw error; // 重新抛出错误
}
};
const handleBatchProcess = async (rowCount: number) => {
// 切换到批量处理流程时重置全局变量
resetGlobalState();
// 检查是否选择了表
if (!selectedBatchTableId) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'warning',
message: '请先选择要处理的数据表'
});
}
return;
}
setBatchProcessing(true);
setBatchProgress({ current: 0, total: rowCount });
try {
// 使用选择的表而不是硬编码的表ID
const batchTable = await bitable.base.getTable(selectedBatchTableId);
// 获取表中的记录(限制数量)
let records = [];
if (selectedBatchViewId) {
// 如果选择了视图从视图中获取记录ID列表然后获取记录
const view = await batchTable.getViewById(selectedBatchViewId);
const recordIdList = await view.getVisibleRecordIdList();
const limitedRecordIds = recordIdList.slice(0, rowCount).filter(id => id !== undefined);
// 批量获取记录
for (const recordId of limitedRecordIds) {
try {
const record = await batchTable.getRecordById(recordId);
// 将recordId添加到记录对象中确保记录对象包含正确的ID
record.recordId = recordId;
records.push(record);
} catch (error) {
console.warn(`Failed to get record ${recordId}:`, error);
}
}
} else {
// 如果没有选择视图,从表中获取记录
const recordsResult = await batchTable.getRecords({
pageSize: rowCount
});
records = recordsResult.records || [];
}
const actualCount = Math.min(records.length, rowCount);
if (actualCount === 0) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'warning',
message: selectedBatchViewId ? '选择的视图中没有找到记录' : '选择的表中没有找到记录'
});
}
return;
}
setBatchProgress({ current: 0, total: actualCount });
// 获取新表的字段映射
const fieldMetaList = await batchTable.getFieldMetaList();
const fieldMapping: {[key: string]: string} = {};
const optionMapping: {[fieldId: string]: {[optionId: string]: string}} = {};
// 建立字段映射和选项映射
for (const fieldMeta of fieldMetaList) {
if (fieldMeta.name.match(/^标签([1-9]|10)$/)) {
fieldMapping[fieldMeta.name] = fieldMeta.id;
// 如果是选择字段建立选项ID到名称的映射
if (fieldMeta.property && fieldMeta.property.dataType && fieldMeta.property.dataType.property && fieldMeta.property.dataType.property.options) {
optionMapping[fieldMeta.id] = {};
for (const option of fieldMeta.property.dataType.property.options) {
if (option.id && option.name) {
optionMapping[fieldMeta.id][option.id] = option.name;
}
}
}
} else if (fieldMeta.name === '起始日期') {
fieldMapping['起始日期'] = fieldMeta.id;
} else if (fieldMeta.name === '客户期望日期') {
fieldMapping['客户期望日期'] = fieldMeta.id;
} else if (fieldMeta.id === 'fldGSdZgMI') {
// 根据字段ID直接映射起始时间字段
fieldMapping['起始时间'] = fieldMeta.id;
} else if (fieldMeta.id === 'fldczh2nty') {
// 款式字段映射
fieldMapping['款式'] = fieldMeta.id;
} else if (fieldMeta.id === 'fldk5fVYvW') {
// 颜色字段映射
fieldMapping['颜色'] = fieldMeta.id;
} else if (fieldMeta.id === 'fldkKZecSv') {
// foreign_id字段映射
fieldMapping['foreign_id'] = fieldMeta.id;
} else if (fieldMeta.name === 'oms看板record_id' || fieldMeta.id === 'fldlaYgpYO') {
// 文本2字段映射 - 支持有空格和无空格的字段名称以及直接ID匹配
fieldMapping['文本2'] = fieldMeta.id;
}
}
console.log('字段映射:', fieldMapping);
console.log('选项映射:', optionMapping);
console.log('所有字段元数据:', fieldMetaList.map(f => ({ id: f.id, name: f.name })));
// 特别检查文本2字段
const text2Fields = fieldMetaList.filter(f => f.name.includes('文本') || f.id === 'fldlaYgpYO');
console.log('包含"文本"的字段或fldlaYgpYO:', text2Fields.map(f => ({ id: f.id, name: f.name })));
let successCount = 0;
let errorCount = 0;
const errors: string[] = [];
// 跟踪最后成功处理的记录的时间信息
let lastSuccessfulStartTime: Date | null = null;
let lastSuccessfulExpectedDate: Date | null = null;
// 逐条处理记录
for (let i = 0; i < actualCount; i++) {
const record = records[i];
setBatchProgress({ current: i + 1, total: actualCount });
try {
// 获取记录ID - 现在应该能正确获取到recordId
const recordId = record.recordId || record.id || `record_${i + 1}`;
console.log(`处理第 ${i + 1} 条记录:`, recordId);
console.log(`记录对象结构:`, record);
// 从记录中提取标签数据
const extractedLabels: {[key: string]: string | string[]} = {};
const recordFields = record.fields;
// 提取标签1-10
for (let labelNum = 1; labelNum <= 10; labelNum++) {
const labelKey = `标签${labelNum}`;
const fieldId = fieldMapping[labelKey];
if (fieldId && recordFields[fieldId]) {
const fieldValue = recordFields[fieldId];
// 处理不同类型的字段值
if (Array.isArray(fieldValue)) {
// 多选字段或选项ID数组
const values = fieldValue.map((item: any) => {
if (typeof item === 'string') {
// 如果是选项ID从选项映射中查找对应的名称
if (optionMapping[fieldId] && optionMapping[fieldId][item]) {
return optionMapping[fieldId][item];
}
return item;
}
if (item && item.text) return item.text;
if (item && item.name) return item.name;
return '';
}).filter(v => v);
if (values.length > 0) {
// 处理可能包含逗号分隔的复合标签值,并与生成模式保持一致
const expandedValues: string[] = [];
values.forEach(value => {
if (typeof value === 'string' && value.includes(',')) {
// 拆分逗号分隔的值
const splitValues = value.split(',').map(v => v.trim()).filter(v => v !== '');
expandedValues.push(...splitValues);
} else {
expandedValues.push(value);
}
});
// 与生成模式保持一致:单值返回字符串,多值返回数组
// 特殊处理标签7始终返回数组结构
if (labelKey === '标签7') {
extractedLabels[labelKey] = expandedValues;
} else {
extractedLabels[labelKey] = expandedValues.length === 1 ? expandedValues[0] : expandedValues;
}
}
} else if (typeof fieldValue === 'string') {
// 单行文本或选项ID
let finalValue: string;
if (optionMapping[fieldId] && optionMapping[fieldId][fieldValue]) {
finalValue = optionMapping[fieldId][fieldValue];
} else {
finalValue = fieldValue;
}
// 特殊处理标签7始终返回数组结构
if (labelKey === '标签7') {
extractedLabels[labelKey] = [finalValue];
} else {
extractedLabels[labelKey] = finalValue;
}
} else if (fieldValue && fieldValue.text) {
// 富文本等
const textValue = fieldValue.text;
// 特殊处理标签7始终返回数组结构
if (labelKey === '标签7') {
extractedLabels[labelKey] = [textValue];
} else {
extractedLabels[labelKey] = textValue;
}
} else if (fieldValue && typeof fieldValue === 'object') {
// 处理公式字段和其他复杂对象
if (fieldValue.value !== undefined) {
// 公式字段通常有value属性
if (Array.isArray(fieldValue.value)) {
const values = fieldValue.value.map((item: any) => {
if (typeof item === 'string') return item;
if (item && item.text) return item.text;
if (item && item.name) return item.name; // 处理选项的name属性
return '';
}).filter(v => v);
if (values.length > 0) {
// 与生成模式保持一致:单值返回字符串,多值返回数组
// 特殊处理标签7始终返回数组结构
if (labelKey === '标签7') {
extractedLabels[labelKey] = values;
} else {
extractedLabels[labelKey] = values.length === 1 ? values[0] : values;
}
}
} else if (typeof fieldValue.value === 'string') {
// 特殊处理标签7始终返回数组结构
if (labelKey === '标签7') {
extractedLabels[labelKey] = [fieldValue.value];
} else {
extractedLabels[labelKey] = fieldValue.value;
}
} else if (fieldValue.value && fieldValue.value.name) {
// 单选字段的选项对象
const nameValue = fieldValue.value.name;
// 特殊处理标签7始终返回数组结构
if (labelKey === '标签7') {
extractedLabels[labelKey] = [nameValue];
} else {
extractedLabels[labelKey] = nameValue;
}
}
} else if (fieldValue.displayValue !== undefined) {
// 有些字段使用displayValue
if (Array.isArray(fieldValue.displayValue)) {
const values = fieldValue.displayValue.map((item: any) => {
if (typeof item === 'string') return item;
if (item && item.text) return item.text;
if (item && item.name) return item.name; // 处理选项的name属性
return '';
}).filter(v => v);
if (values.length > 0) {
// 与生成模式保持一致:单值返回字符串,多值返回数组
// 特殊处理标签7始终返回数组结构
if (labelKey === '标签7') {
extractedLabels[labelKey] = values;
} else {
extractedLabels[labelKey] = values.length === 1 ? values[0] : values;
}
}
} else if (typeof fieldValue.displayValue === 'string') {
// 特殊处理标签7始终返回数组结构
if (labelKey === '标签7') {
extractedLabels[labelKey] = [fieldValue.displayValue];
} else {
extractedLabels[labelKey] = fieldValue.displayValue;
}
} else if (fieldValue.displayValue && fieldValue.displayValue.name) {
// 单选字段的选项对象
const nameValue = fieldValue.displayValue.name;
// 特殊处理标签7始终返回数组结构
if (labelKey === '标签7') {
extractedLabels[labelKey] = [nameValue];
} else {
extractedLabels[labelKey] = nameValue;
}
}
} else if (fieldValue.name) {
// 直接的选项对象有name属性
const nameValue = fieldValue.name;
// 特殊处理标签7始终返回数组结构
if (labelKey === '标签7') {
extractedLabels[labelKey] = [nameValue];
} else {
extractedLabels[labelKey] = nameValue;
}
}
}
}
}
// 提取起始日期
let extractedStartTime: Date | null = null;
const startDateFieldId = fieldMapping['起始日期'] || fieldMapping['起始时间'];
if (startDateFieldId && recordFields[startDateFieldId]) {
const startDateValue = recordFields[startDateFieldId];
if (typeof startDateValue === 'number') {
extractedStartTime = new Date(startDateValue);
} else if (typeof startDateValue === 'string') {
extractedStartTime = new Date(startDateValue);
} else if (Array.isArray(startDateValue) && startDateValue.length > 0) {
// 处理数组格式的时间戳,如 [1757260800000]
const timestamp = startDateValue[0];
if (typeof timestamp === 'number') {
extractedStartTime = new Date(timestamp);
}
}
}
// 提取客户期望日期
let extractedExpectedDate: Date | null = null;
const expectedDateFieldId = fieldMapping['客户期望日期'];
if (expectedDateFieldId && recordFields[expectedDateFieldId]) {
const expectedDateValue = recordFields[expectedDateFieldId];
if (typeof expectedDateValue === 'number') {
extractedExpectedDate = new Date(expectedDateValue);
} else if (typeof expectedDateValue === 'string') {
extractedExpectedDate = new Date(expectedDateValue);
} else if (Array.isArray(expectedDateValue) && expectedDateValue.length > 0) {
// 处理数组格式的时间戳,如 [1757260800000]
const timestamp = expectedDateValue[0];
if (typeof timestamp === 'number') {
extractedExpectedDate = new Date(timestamp);
}
}
}
// 统一的字段提取函数(复用生成模式的逻辑)
// 将提取的文本转换为富文本格式
const createRichTextFormat = (text: string) => {
return text ? [{ type: "text", text: text }] : null;
};
// 提取款式、颜色、foreign_id字段
let extractedStyle: any = null;
let extractedColor: any = null;
let extractedForeignId: any = null;
// 提取款式字段
const styleFieldId = fieldMapping['款式'];
if (styleFieldId && recordFields[styleFieldId]) {
const styleText = extractText(recordFields[styleFieldId]);
extractedStyle = createRichTextFormat(styleText);
}
// 提取颜色字段
const colorFieldId = fieldMapping['颜色'];
if (colorFieldId && recordFields[colorFieldId]) {
const colorText = extractText(recordFields[colorFieldId]);
extractedColor = createRichTextFormat(colorText);
}
// 提取foreign_id字段
const foreignIdFieldId = fieldMapping['foreign_id'];
if (foreignIdFieldId && recordFields[foreignIdFieldId]) {
const foreignIdText = extractText(recordFields[foreignIdFieldId]);
extractedForeignId = createRichTextFormat(foreignIdText);
}
// 提取文本2字段
let extractedText2: any = null;
const text2FieldId = fieldMapping['文本2'];
console.log('文本2字段映射:', { fieldName: '文本2', fieldId: text2FieldId });
console.log('记录字段keys:', Object.keys(recordFields));
console.log('fldlaYgpYO字段值:', recordFields['fldlaYgpYO']);
if (text2FieldId && recordFields[text2FieldId]) {
extractedText2 = extractText(recordFields[text2FieldId]);
console.log('提取后的extractedText2:', extractedText2);
} else {
console.log('文本2字段未找到或无数据:', {
fieldId: text2FieldId,
hasField: !!recordFields[text2FieldId],
fieldValue: recordFields[text2FieldId]
});
}
console.log(`记录 ${i + 1} 提取的数据:`, {
labels: extractedLabels,
startTime: extractedStartTime,
expectedDate: extractedExpectedDate,
style: extractedStyle,
color: extractedColor,
foreignId: extractedForeignId,
text2: extractedText2
});
console.log(`记录 ${i + 1} 字段映射:`, fieldMapping);
console.log(`记录 ${i + 1} 记录字段:`, Object.keys(recordFields));
// 如果没有提取到任何标签,跳过这条记录
if (Object.keys(extractedLabels).length === 0) {
console.warn(`记录 ${i + 1} 没有找到任何标签数据,跳过处理`);
errorCount++;
errors.push(`记录 ${i + 1}: 没有找到标签数据`);
continue;
}
// 使用模拟生成模式的方式进行时效计算和写入
await simulateGenerationModeForBatch(
recordId,
extractedLabels,
extractedStartTime,
extractedExpectedDate,
extractedStyle,
extractedColor,
extractedForeignId,
extractedText2
);
successCount++;
// 记录最后成功处理的记录的时间信息
lastSuccessfulStartTime = extractedStartTime;
lastSuccessfulExpectedDate = extractedExpectedDate;
// 添加延迟避免API限制
if (i < actualCount - 1) {
await new Promise(resolve => setTimeout(resolve, 100));
}
} catch (error) {
console.error(`处理记录 ${i + 1} 失败:`, error);
errorCount++;
errors.push(`记录 ${i + 1}: ${(error as Error).message}`);
}
}
// 显示处理结果
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: successCount > 0 ? 'success' : 'error',
message: `批量处理完成!成功: ${successCount} 条,失败: ${errorCount}`
});
}
// 如果有成功处理的记录更新UI时间显示为最后一条成功记录的时间
if (successCount > 0 && lastSuccessfulStartTime && lastSuccessfulExpectedDate) {
console.log('批量处理完成更新UI时间显示');
console.log('最后成功记录的起始时间:', lastSuccessfulStartTime);
console.log('最后成功记录的期望日期:', lastSuccessfulExpectedDate);
setStartTime(lastSuccessfulStartTime);
setExpectedDate(lastSuccessfulExpectedDate);
}
if (errors.length > 0) {
console.error('批量处理错误详情:', errors);
}
} catch (error) {
console.error('批量处理失败:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'error',
message: `批量处理失败: ${(error as Error).message}`
});
}
} finally {
setBatchProcessing(false);
setBatchProgress({ current: 0, total: 0 });
}
};
// 为批量处理优化的时效计算函数
const calculateTimelineForBatch = async (
labels: {[key: string]: string | string[]},
startTime: Date | null,
expectedDate: Date | null,
sourceRecordId: string,
style?: any,
color?: any,
foreignId?: any,
text2?: any
) => {
try {
// 批量模式:输出当前数据结构,便于与生成模式对比
try {
console.group('=== 批量模式:计算时效 - 当前数据结构 ===');
console.log('sourceRecordId:', sourceRecordId);
console.log('labels:', labels);
console.log('startTime:', startTime);
console.log('expectedDate:', expectedDate);
console.log('style:', style);
console.log('color:', color);
console.log('foreignId:', foreignId);
console.log('text2:', text2);
console.groupEnd();
} catch (logErr) {
console.warn('批量模式结构化日志输出失败:', logErr);
}
// 1. 获取流程配置表数据
const processConfigTable = await bitable.base.getTable(PROCESS_CONFIG_TABLE_ID);
const processConfigRecords = await processConfigTable.getRecords({
pageSize: 5000
});
const processNodes = processConfigRecords.records || [];
// 2. 构建业务选择的标签值集合
const businessLabelValues = new Set<string>();
console.log('批量模式 - 开始处理标签数据:', labels);
for (const [labelKey, selectedValue] of Object.entries(labels)) {
console.log(`批量模式 - 处理标签 ${labelKey}:`, selectedValue, '类型:', typeof selectedValue);
if (selectedValue) {
const values = Array.isArray(selectedValue) ? selectedValue : [selectedValue];
values.forEach((value, index) => {
console.log(`批量模式 - ${labelKey}${index}:`, value, '类型:', typeof value);
if (typeof value === 'string' && value.trim()) {
// 处理可能包含逗号分隔的复合标签值
const splitValues = value.split(',').map(v => v.trim()).filter(v => v !== '');
console.log(`批量模式 - ${labelKey} 分割后的值:`, splitValues);
splitValues.forEach(splitValue => {
businessLabelValues.add(splitValue);
console.log(`批量模式 - 添加到业务标签集合: "${splitValue}"`);
});
}
});
}
}
console.log('批量模式 - 最终业务标签值集合:', Array.from(businessLabelValues));
// 3. 匹配流程节点
const matchedProcessNodes: any[] = [];
console.log('批量模式 - 开始匹配流程节点,总节点数:', processNodes.length);
for (const processNode of processNodes) {
const processFields = processNode.fields;
const nodeName = processFields[NODE_NAME_FIELD_ID];
const processLabels = processFields[PROCESS_LABEL_FIELD_ID];
const processOrder = processFields[PROCESS_ORDER_FIELD_ID];
const weekendDays = processFields[WEEKEND_DAYS_FIELD_ID] || [];
const excludedDates = processFields[EXCLUDED_DATES_FIELD_ID] || [];
const startDateRule = processFields[START_DATE_RULE_FIELD_ID];
const dateAdjustmentRule = processFields[DATE_ADJUSTMENT_RULE_FIELD_ID];
// 处理节点名称
let nodeNameText = '';
if (typeof nodeName === 'string') {
nodeNameText = nodeName;
} else if (nodeName && nodeName.text) {
nodeNameText = nodeName.text;
}
if (!nodeNameText) continue;
// 处理流程标签数据,与生成模式保持一致
const processLabelTexts: string[] = [];
if (processLabels && Array.isArray(processLabels)) {
for (const labelItem of processLabels) {
const labelText = extractText(labelItem);
if (labelText && labelText.trim()) {
processLabelTexts.push(labelText.trim());
}
}
}
// 与生成模式保持一致:如果流程标签为空,跳过该节点
if (processLabelTexts.length === 0) {
console.log(`批量模式 - 流程节点 "${nodeNameText}" 标签为空,跳过`);
continue;
}
console.log(`批量模式 - 检查流程节点 "${nodeNameText}":`, {
流程标签: processLabelTexts,
流程顺序: processOrder
});
// 处理流程标签匹配
let isMatched = false;
const matchedLabels: string[] = [];
// 改为OR逻辑只要有一个标签匹配就认为节点匹配
for (const processLabelText of processLabelTexts) {
console.log(`批量模式 - 检查流程标签: "${processLabelText}"`);
// 对流程标签也进行分割处理,以支持复合标签值
const processLabelValues = processLabelText.split(',').map(v => v.trim()).filter(v => v !== '');
console.log(`批量模式 - 流程标签分割后: [${processLabelValues.join(', ')}]`);
// 检查分割后的每个流程标签值是否在业务标签集合中
for (const processLabelValue of processLabelValues) {
if (businessLabelValues.has(processLabelValue)) {
matchedLabels.push(processLabelValue);
isMatched = true;
console.log(`批量模式 - 流程节点 "${nodeNameText}" 匹配成功,匹配的标签: "${processLabelValue}"`);
break; // 找到一个匹配就足够了
} else {
console.log(`批量模式 - 流程标签值 "${processLabelValue}" 未匹配`);
}
}
if (isMatched) break; // 已经匹配,不需要继续检查其他标签
}
if (isMatched) {
matchedProcessNodes.push({
nodeName: nodeNameText,
processOrder: typeof processOrder === 'number' ? processOrder : parseInt(processOrder) || 0,
matchedLabels,
weekendDays: Array.isArray(weekendDays) ? weekendDays : [],
excludedDates: Array.isArray(excludedDates) ? excludedDates : [],
startDateRule,
dateAdjustmentRule
});
console.log(`批量模式 - 添加匹配的流程节点: "${nodeNameText}"`);
} else {
console.log(`批量模式 - 流程节点 "${nodeNameText}" 未匹配`);
}
}
// 按流程顺序排序
matchedProcessNodes.sort((a, b) => a.processOrder - b.processOrder);
if (matchedProcessNodes.length === 0) {
throw new Error('没有匹配的流程节点');
}
// 4. 获取时效数据并计算时间线
const timelineTable = await bitable.base.getTable(TIMELINE_TABLE_ID);
const timelineRecords = await timelineTable.getRecords({
pageSize: 5000
});
// 获取时效表的标签字段映射(只需要获取一次)
const timelineFieldMetaList = await timelineTable.getFieldMetaList();
const timelineLabelFields: {[key: string]: string} = {};
// 查找关系字段和计算方式字段
let relationshipFieldId = '';
let calculationMethodFieldId = '';
for (const fieldMeta of timelineFieldMetaList) {
const match = fieldMeta.name.match(/^标签([1-9]|10)$/);
if (match) {
const labelKey = `标签${match[1]}`;
timelineLabelFields[labelKey] = fieldMeta.id;
}
// 查找关系字段
if (fieldMeta.name === '关系' || fieldMeta.id === 'fldaIDAhab') {
relationshipFieldId = fieldMeta.id;
}
// 查找时效计算方式字段
if (fieldMeta.name === '时效计算方式' || fieldMeta.id === 'fldxfLZNUu') {
calculationMethodFieldId = fieldMeta.id;
}
}
// 构建时效数据索引
const timelineIndexByNode = new Map<string, any[]>();
for (const timelineRecord of timelineRecords.records || []) {
const timelineFields = timelineRecord.fields;
const timelineNodeName = timelineFields[TIMELINE_NODE_FIELD_ID];
let timelineNodeNames: string[] = [];
if (typeof timelineNodeName === 'string') {
timelineNodeNames = timelineNodeName.split(',').map(name => name.trim());
} else if (Array.isArray(timelineNodeName)) {
timelineNodeNames = timelineNodeName.map(item => {
if (typeof item === 'string') {
return item.trim();
} else if (item && item.text) {
return item.text.trim();
}
return '';
}).filter(name => name);
} else if (timelineNodeName && timelineNodeName.text) {
timelineNodeNames = timelineNodeName.text.split(',').map(name => name.trim());
}
for (const nodeName of timelineNodeNames) {
const normalizedName = nodeName.toLowerCase();
if (!timelineIndexByNode.has(normalizedName)) {
timelineIndexByNode.set(normalizedName, []);
}
timelineIndexByNode.get(normalizedName)!.push({
record: timelineRecord,
fields: timelineFields
});
}
}
// 5. 计算每个节点的时效(遵循生成模式的匹配逻辑)
const results: any[] = [];
let cumulativeStartTime = startTime || new Date();
for (const processNode of matchedProcessNodes) {
console.log(`批量模式 - 开始处理流程节点: "${processNode.nodeName}"`);
// 根据流程节点名称,在时效表中查找匹配的记录(与生成模式一致)
const normalizedProcessName = processNode.nodeName.trim().toLowerCase();
const candidateRecords = timelineIndexByNode.get(normalizedProcessName) || [];
console.log(`批量模式 - 节点 "${processNode.nodeName}" 找到 ${candidateRecords.length} 个候选时效记录`);
let timelineValue = null;
let matchedTimelineRecord = null;
// 收集所有匹配的候选记录(与生成模式保持一致)
const matchedCandidates: any[] = [];
// 在候选记录中进行标签匹配,收集所有匹配的记录
for (const candidate of candidateRecords) {
const { record: timelineRecord, fields: timelineFields } = candidate;
// 进行完整的标签匹配逻辑(与生成模式保持一致)
let isLabelsMatched = true;
// 检查时效表中每个有值的标签是否都包含在业务标签中
for (const [labelKey, timelineFieldId] of Object.entries(timelineLabelFields)) {
const timelineLabelValue = timelineFields[timelineFieldId];
// 处理时效表中的标签值
let timelineLabelTexts: string[] = [];
if (typeof timelineLabelValue === 'string' && timelineLabelValue.trim()) {
timelineLabelTexts = [timelineLabelValue.trim()];
} else if (timelineLabelValue && timelineLabelValue.text && timelineLabelValue.text.trim()) {
timelineLabelTexts = [timelineLabelValue.text.trim()];
} else if (Array.isArray(timelineLabelValue) && timelineLabelValue.length > 0) {
// 多选字段,获取所有值
timelineLabelTexts = timelineLabelValue.map(item => {
if (typeof item === 'string') return item.trim();
if (item && item.text) return item.text.trim();
return '';
}).filter(text => text);
}
// 如果时效表中该标签有值,则检查该标签的所有值是否都包含在业务标签中
if (timelineLabelTexts.length > 0) {
let allValuesMatched = true;
for (const timelineText of timelineLabelTexts) {
if (!businessLabelValues.has(timelineText)) {
allValuesMatched = false;
console.log(`批量模式 - 时效表标签 ${labelKey} 的值 "${timelineText}" 不在业务选择的标签中`);
break;
}
}
if (!allValuesMatched) {
console.log(`批量模式 - 时效表标签 ${labelKey} 的值 [${timelineLabelTexts.join(', ')}] 不完全包含在业务选择的标签中`);
isLabelsMatched = false;
break;
} else {
console.log(`批量模式 - 时效表标签 ${labelKey} 的值 [${timelineLabelTexts.join(', ')}] 完全匹配成功`);
}
}
// 如果时效表中该标签为空,则跳过检查(空标签不影响匹配)
}
if (isLabelsMatched) {
const timelineValueField = timelineFields[TIMELINE_FIELD_ID];
const calculationMethodField = timelineFields[calculationMethodFieldId];
const relationshipField = timelineFields[relationshipFieldId];
if (timelineValueField !== null && timelineValueField !== undefined) {
let candidateTimelineValue = 0;
// 解析时效值(与生成模式保持一致)
if (typeof timelineValueField === 'number') {
candidateTimelineValue = timelineValueField;
} else if (typeof timelineValueField === 'string') {
const parsedValue = parseFloat(timelineValueField);
candidateTimelineValue = isNaN(parsedValue) ? 0 : parsedValue;
} else if (timelineValueField && typeof timelineValueField === 'object' && timelineValueField.value !== undefined) {
candidateTimelineValue = timelineValueField.value;
}
// 获取计算方式
let calculationMethod = '外部'; // 默认值
if (calculationMethodField) {
if (typeof calculationMethodField === 'string') {
calculationMethod = calculationMethodField;
} else if (calculationMethodField && typeof calculationMethodField === 'object' && calculationMethodField.text) {
calculationMethod = calculationMethodField.text;
}
}
// 获取关系类型
let relationshipType = '默认';
if (relationshipField) {
if (typeof relationshipField === 'string') {
relationshipType = relationshipField;
} else if (relationshipField && typeof relationshipField === 'object' && relationshipField.text) {
relationshipType = relationshipField.text;
}
}
// 根据计算方式转换时效值(小时转天)
let convertedTimelineValue = 0;
if (calculationMethod === '内部') {
convertedTimelineValue = Math.round((candidateTimelineValue / 9) * 1000) / 1000;
} else {
convertedTimelineValue = Math.round((candidateTimelineValue / 24) * 1000) / 1000;
}
// 添加到匹配候选列表
matchedCandidates.push({
record: timelineRecord,
timelineValue: convertedTimelineValue,
originalHours: candidateTimelineValue,
calculationMethod: calculationMethod,
relationshipType: relationshipType
});
}
}
}
// 处理匹配的候选记录(与生成模式保持一致)
let finalTimelineValue = 0;
let processingRule = '默认';
if (matchedCandidates.length > 0) {
// 获取主要关系类型(用于决定处理方式)
const relationshipTypes = matchedCandidates.map(c => c.relationshipType);
const primaryRelationshipType = relationshipTypes.find(type => type === '累加值' || type === '最大值') || '默认';
// 使用关系字段决定处理方式(累加值、最大值、默认)
processingRule = primaryRelationshipType || '默认';
console.log('批量模式 - processingRule:', processingRule);
if (processingRule === '累加值') {
finalTimelineValue = matchedCandidates.reduce((sum, candidate) => sum + candidate.timelineValue, 0);
const totalOriginalHours = matchedCandidates.reduce((sum, candidate) => sum + candidate.originalHours, 0);
console.log(`批量模式 - 节点 ${processNode.nodeName} 累加值处理 - 找到 ${matchedCandidates.length} 条匹配记录:`);
matchedCandidates.forEach((candidate, index) => {
console.log(` 记录${index + 1}: ID=${candidate.record.id || candidate.record.recordId}, 时效=${candidate.originalHours}小时(${candidate.timelineValue}天), 计算方式=${candidate.calculationMethod}`);
});
console.log(`累加结果: 总计${totalOriginalHours}小时 → ${finalTimelineValue}`);
matchedTimelineRecord = matchedCandidates[0].record;
} else if (processingRule === '最大值') {
finalTimelineValue = Math.max(...matchedCandidates.map(c => c.timelineValue));
const maxCandidate = matchedCandidates.find(c => c.timelineValue === finalTimelineValue);
console.log(`批量模式 - 节点 ${processNode.nodeName} 最大值处理 - 找到 ${matchedCandidates.length} 条匹配记录:`);
matchedCandidates.forEach((candidate, index) => {
console.log(` 记录${index + 1}: ID=${candidate.record.id || candidate.record.recordId}, 时效=${candidate.originalHours}小时(${candidate.timelineValue}天), 计算方式=${candidate.calculationMethod}`);
});
console.log(`最大值结果: ${maxCandidate?.originalHours}小时(${maxCandidate?.calculationMethod}) → ${finalTimelineValue}`);
matchedTimelineRecord = maxCandidate?.record || matchedCandidates[0].record;
} else {
finalTimelineValue = matchedCandidates[0].timelineValue;
console.log(`批量模式 - 节点 ${processNode.nodeName} 默认处理 - 找到 ${matchedCandidates.length} 条匹配记录:`);
matchedCandidates.forEach((candidate, index) => {
console.log(` 记录${index + 1}: ID=${candidate.record.id || candidate.record.recordId}, 时效=${candidate.originalHours}小时(${candidate.timelineValue}天), 计算方式=${candidate.calculationMethod}`);
});
console.log(`默认结果: 使用第一条记录 ${matchedCandidates[0].originalHours}小时(${matchedCandidates[0].calculationMethod}) → ${finalTimelineValue}`);
matchedTimelineRecord = matchedCandidates[0].record;
}
timelineValue = finalTimelineValue;
}
// 计算节点时间(与生成模式保持一致)
let nodeStartTime = new Date(cumulativeStartTime);
let nodeEndTime = new Date(nodeStartTime);
let nodeCalculationMethod = '外部'; // 默认计算方式
// 如果有匹配的候选记录,使用其计算方式
if (matchedCandidates.length > 0) {
if (processingRule === '累加值') {
nodeCalculationMethod = matchedCandidates[0].calculationMethod;
} else if (processingRule === '最大值') {
const maxCandidate = matchedCandidates.find(c => c.timelineValue === finalTimelineValue);
nodeCalculationMethod = maxCandidate?.calculationMethod || matchedCandidates[0].calculationMethod;
} else {
nodeCalculationMethod = matchedCandidates[0].calculationMethod;
}
}
2025-11-13 15:59:08 +08:00
if (timelineValue && timelineValue !== 0) {
2025-11-06 16:19:08 +08:00
// 根据计算方式调整开始时间
const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, processNode.weekendDays, processNode.excludedDates || []);
let endDate: Date;
if (nodeCalculationMethod === '内部') {
// 使用内部工作时间计算
endDate = addInternalBusinessTime(adjustedStartTime, timelineValue, processNode.weekendDays, processNode.excludedDates || []);
} else {
// 使用原有的24小时制计算
endDate = addBusinessDaysWithHolidays(adjustedStartTime, timelineValue, processNode.weekendDays, processNode.excludedDates || []);
}
nodeStartTime = adjustedStartTime;
nodeEndTime = endDate;
}
results.push({
processOrder: processNode.processOrder,
nodeName: processNode.nodeName,
matchedLabels: processNode.matchedLabels,
timelineValue: timelineValue,
estimatedStart: formatDate(nodeStartTime), // 使用默认格式,与生成模式一致
estimatedEnd: formatDate(nodeEndTime), // 使用默认格式,与生成模式一致
timelineRecordId: matchedTimelineRecord ? (matchedTimelineRecord.id || matchedTimelineRecord.recordId) : null,
calculationMethod: nodeCalculationMethod, // 使用实际的计算方式
weekendDaysConfig: processNode.weekendDays,
skippedWeekends: 0,
actualDays: calculateActualDays(formatDate(nodeStartTime), formatDate(nodeEndTime)), // 使用一致的格式
startDateRule: processNode.startDateRule,
dateAdjustmentRule: processNode.dateAdjustmentRule,
// 新增:保存所有匹配记录的信息(用于累加情况)
allMatchedRecords: matchedCandidates.length > 1 ? matchedCandidates.map(candidate => ({
recordId: candidate.record.id || candidate.record.recordId || candidate.record._id || candidate.record.record_id,
timelineValue: candidate.timelineValue,
originalHours: candidate.originalHours,
calculationMethod: candidate.calculationMethod,
relationshipType: candidate.relationshipType
})) : null,
// 新增:标识是否为累加处理
isAccumulated: processingRule === '累加值' && matchedCandidates.length > 1
});
// 更新累积时间
2025-11-13 15:59:08 +08:00
if (timelineValue && timelineValue !== 0) {
2025-11-06 16:19:08 +08:00
cumulativeStartTime = new Date(nodeEndTime);
}
}
console.log('批量计算结果:', results);
// 6. 写入数据到货期记录表和流程数据表
if (results.length > 0) {
console.log('=== 批量模式:开始写入数据 ===');
console.log('批量写入 - sourceRecordId:', sourceRecordId);
console.log('批量写入 - labels:', labels);
console.log('批量写入 - expectedDate:', expectedDate);
console.log('批量写入 - style:', style);
console.log('批量写入 - color:', color);
console.log('批量写入 - foreignId:', foreignId);
console.log('批量写入 - text2:', text2);
console.log('批量写入 - startTime:', startTime);
try {
// 为标准写入函数准备数据:构造临时的数据结构
console.log('批量写入 - 设置临时状态以适配标准写入函数');
// 构造临时的recordDetails包含当前记录的字段信息
const tempRecordDetails = [{
id: sourceRecordId,
fields: {
'fldpvBfeC0': foreignId, // 外键ID字段
'fld6Uw95kt': style, // 样式字段
'flde85ni4O': color, // 颜色字段
'fldG6LZnmU': text2 // 文本字段2
}
}];
// 构造批量数据对象(包含必要的业务字段)
const batchDataForWriting = {
selectedRecords: [sourceRecordId],
recordDetails: tempRecordDetails,
labels, // 标签汇总(对象)
expectedDate, // 客户期望日期Date
startTime // 起始时间Date
};
console.log('批量写入 - 临时数据构造完成,开始调用标准写入函数');
console.log('批量写入 - batchDataForWriting:', batchDataForWriting);
// 写入流程数据表 - 使用标准函数,传递批量数据
console.log('批量写入 - 调用标准writeToProcessDataTable函数');
const processRecordIds = await writeToProcessDataTable(results, batchDataForWriting);
console.log('批量写入 - 流程数据表写入完成记录ID:', processRecordIds);
// 写入货期记录表 - 使用标准函数,传递批量数据和空的调整信息
console.log('批量写入 - 调用标准writeToDeliveryRecordTable函数');
await writeToDeliveryRecordTable(results, processRecordIds, {}, batchDataForWriting);
console.log('批量写入 - 货期记录表写入完成');
// 批量写入成功后,设置全局状态以便快照保存
console.log('批量写入 - 设置全局状态以便快照保存');
setSelectedRecords([sourceRecordId]);
setRecordDetails(tempRecordDetails);
setSelectedLabels(labels);
setCurrentStyleText(extractText(style));
setCurrentColorText(extractText(color));
setCurrentText2(extractText(text2));
setCurrentForeignId(extractText(foreignId));
setExpectedDate(expectedDate);
setStartTime(startTime);
} catch (error) {
console.error('批量写入失败:', error);
throw error;
}
console.log('=== 批量模式:数据写入完成 ===');
}
} catch (error) {
console.error('批量时效计算失败:', error);
throw error;
}
};
// 执行定价数据查询
const executeQuery = async (packId: string, packType: string) => {
setQueryLoading(true);
try {
// 使用 apiService 中的函数
const data = await executePricingQuery(packId, packType, selectedLabels);
setQueryResults(data);
// 处理品类属性填充到标签8的逻辑保持不变
if (data && data.length > 0) {
const label8Options = labelOptions['标签8'] || [];
const matchingCategoryValues: string[] = [];
data.forEach((record: any) => {
if (record. && record..trim() !== '') {
const categoryValue = record..trim();
const matchingOption = label8Options.find(option =>
option.value === categoryValue || option.label === categoryValue
);
if (matchingOption && !matchingCategoryValues.includes(categoryValue)) {
matchingCategoryValues.push(categoryValue);
}
}
});
if (matchingCategoryValues.length > 0) {
setSelectedLabels(prev => ({
...prev,
'标签8': matchingCategoryValues
}));
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'success',
message: `已将 ${matchingCategoryValues.length} 个品类属性填充到标签8: ${matchingCategoryValues.join(', ')}`
});
}
}
}
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'success',
message: '查询成功'
});
}
} catch (error: any) {
console.error('数据库查询出错:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'error',
message: `数据库查询失败: ${error.message}`
});
}
} finally {
setQueryLoading(false);
}
};
// 执行二次工艺查询
const executeSecondaryProcessQueryLocal = async (packId: string, packType: string) => {
setSecondaryProcessLoading(true);
try {
const data = await executeSecondaryProcessQuery(packId, packType);
setSecondaryProcessResults(data);
// 处理二次工艺填充到标签7的逻辑
if (data && data.length > 0) {
const label7Options = labelOptions['标签7'] || [];
const matchingProcessValues: string[] = [];
data.forEach((record: any) => {
if (record.costs_type && record.costs_type.trim() !== '') {
const processValue = record.costs_type.trim();
const matchingOption = label7Options.find(option =>
option.value === processValue || option.label === processValue
);
if (matchingOption && !matchingProcessValues.includes(processValue)) {
matchingProcessValues.push(processValue);
}
}
});
if (matchingProcessValues.length > 0) {
setSelectedLabels(prev => ({
...prev,
'标签7': matchingProcessValues
}));
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'success',
message: `已将 ${matchingProcessValues.length} 个二次工艺填充到标签7: ${matchingProcessValues.join(', ')}`
});
}
}
}
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'success',
message: '二次工艺查询成功'
});
}
} catch (error: any) {
console.error('二次工艺查询出错:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'error',
message: `二次工艺查询失败: ${error.message}`
});
}
} finally {
setSecondaryProcessLoading(false);
}
};
// 执行定价详情查询
const executePricingDetailsQueryLocal = async (packId: string) => {
setPricingDetailsLoading(true);
try {
const data = await executePricingDetailsQuery(packId);
setPricingDetailsResults(data);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'success',
message: '定价详情查询成功'
});
}
} catch (error: any) {
console.error('定价详情查询出错:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'error',
message: `定价详情查询失败: ${error.message}`
});
}
} finally {
setPricingDetailsLoading(false);
}
};
// 处理数据库查询
const handleQueryDatabase = async (record: any) => {
// 从记录字段中提取 packId 和 packType
let packId = '';
let packType = '';
// 提取 pack_id (fldpvBfeC0)
if (record.fields.fldpvBfeC0 && Array.isArray(record.fields.fldpvBfeC0) && record.fields.fldpvBfeC0.length > 0) {
packId = record.fields.fldpvBfeC0[0].text;
}
// 提取 pack_type (fldSAF9qXe)
if (record.fields.fldSAF9qXe && record.fields.fldSAF9qXe.text) {
packType = record.fields.fldSAF9qXe.text;
}
if (!packId || !packType) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'error',
message: '缺少必要的查询参数 (pack_id 或 pack_type)'
});
}
return;
}
await executeQuery(packId, packType);
};
// 处理二次工艺查询
const handleSecondaryProcessQuery = async (record: any) => {
// 从记录字段中提取 packId 和 packType
let packId = '';
let packType = '';
// 提取 pack_id (fldpvBfeC0)
if (record.fields.fldpvBfeC0 && Array.isArray(record.fields.fldpvBfeC0) && record.fields.fldpvBfeC0.length > 0) {
packId = record.fields.fldpvBfeC0[0].text;
}
// 提取 pack_type (fldSAF9qXe)
if (record.fields.fldSAF9qXe && record.fields.fldSAF9qXe.text) {
packType = record.fields.fldSAF9qXe.text;
}
if (!packId || !packType) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'error',
message: '缺少必要的查询参数 (pack_id 或 pack_type)'
});
}
return;
}
await executeSecondaryProcessQueryLocal(packId, packType);
};
// 处理定价详情查询
const handlePricingDetailsQuery = async (record: any) => {
// 从记录字段中提取 packId
let packId = '';
// 提取 pack_id (fldpvBfeC0)
if (record.fields.fldpvBfeC0 && Array.isArray(record.fields.fldpvBfeC0) && record.fields.fldpvBfeC0.length > 0) {
packId = record.fields.fldpvBfeC0[0].text;
}
if (!packId) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'error',
message: '缺少必要的查询参数 (pack_id)'
});
}
return;
}
await executePricingDetailsQueryLocal(packId);
};
// 获取记录详情的函数
const fetchRecordDetails = async (recordIdList: string[]) => {
try {
const table = await bitable.base.getTable(TABLE_ID);
// 并行获取所有记录详情
const recordPromises = recordIdList.map(recordId =>
table.getRecordById(recordId)
);
const records = await Promise.all(recordPromises);
const recordValList = records.map((record, index) => {
console.log(`记录 ${recordIdList[index]} 的详情:`, record);
console.log(`记录 ${recordIdList[index]} 的fldpvBfeC0字段:`, record.fields['fldpvBfeC0']);
return {
id: recordIdList[index],
fields: record.fields
};
});
setRecordDetails(recordValList);
} catch (error) {
console.error('获取记录详情失败:', error);
}
};
// 选择记录
const handleSelectRecords = async () => {
// 切换版单数据时重置全局变量,清空旧结果与回填状态
resetGlobalState();
setLoading(true);
// 清空标签选择
setSelectedLabels({});
setExpectedDate(null);
try {
// 修改这里:使用正确的 API
const recordIdList = await bitable.ui.selectRecordIdList(TABLE_ID, VIEW_ID);
setSelectedRecords(recordIdList);
// 获取记录的详细信息
if (recordIdList.length > 0) {
// 并行获取记录详情和字段元数据
const table = await bitable.base.getTable(TABLE_ID);
const [recordPromises, fieldMetaList] = await Promise.all([
Promise.all(recordIdList.map(recordId => table.getRecordById(recordId))),
table.getFieldMetaList()
]);
const recordValList = recordPromises.map((record, index) => ({
id: recordIdList[index],
fields: record.fields
}));
if (recordValList.length > 0) {
const firstRecord = recordValList[0];
const extractedLabels: {[key: string]: string} = {};
// 建立字段名到字段ID的映射
const fieldNameToId: {[key: string]: string} = {};
for (const fieldMeta of fieldMetaList) {
fieldNameToId[fieldMeta.name] = fieldMeta.id;
}
// 提取标签值的辅助函数
const extractFieldValue = (fieldName: string) => {
const fieldId = fieldNameToId[fieldName];
if (fieldId && firstRecord.fields[fieldId]) {
const fieldValue = firstRecord.fields[fieldId];
// 优先处理数组格式(公式字段)
if (Array.isArray(fieldValue) && fieldValue.length > 0) {
const firstItem = fieldValue[0];
if (typeof firstItem === 'string') {
return firstItem;
} else if (firstItem && (firstItem.text || firstItem.name)) {
return firstItem.text || firstItem.name;
}
}
// 处理对象格式(普通字段)
else if (typeof fieldValue === 'object' && fieldValue !== null) {
if (fieldValue.text) {
return fieldValue.text;
} else if (fieldValue.name) {
return fieldValue.name;
}
}
// 处理字符串格式
else if (typeof fieldValue === 'string') {
return fieldValue.trim();
}
}
return '';
};
// 直接通过字段ID提取fld6Uw95kt的值
const getFieldValueById = (fieldId: string) => {
if (fieldId && firstRecord.fields[fieldId]) {
const fieldValue = firstRecord.fields[fieldId];
// 优先处理数组格式(公式字段)
if (Array.isArray(fieldValue) && fieldValue.length > 0) {
const firstItem = fieldValue[0];
if (typeof firstItem === 'string') {
return firstItem;
} else if (firstItem && (firstItem.text || firstItem.name)) {
return firstItem.text || firstItem.name;
}
}
// 处理对象格式(普通字段)
else if (typeof fieldValue === 'object' && fieldValue !== null) {
if (fieldValue.text) {
return fieldValue.text;
} else if (fieldValue.name) {
return fieldValue.name;
}
}
// 处理字符串格式
else if (typeof fieldValue === 'string') {
return fieldValue.trim();
}
}
return '';
};
// 提取fld6Uw95kt字段的值
const mainRecordDisplayValue = getFieldValueById('fld6Uw95kt') || firstRecord.id;
// 将这个值存储到recordDetails中以便在UI中使用
const updatedRecordValList = recordValList.map((record, index) => ({
...record,
displayValue: index === 0 ? mainRecordDisplayValue : record.id
}));
setRecordDetails(updatedRecordValList);
// 提取各个标签的值
const label2Value = extractFieldValue('品类名称');
const label3Value = extractFieldValue('大类名称');
const label4Value = extractFieldValue('中类名称');
const label5Value = extractFieldValue('小类名称');
const label6Value = extractFieldValue('工艺难易度');
// 设置提取到的标签值
const newSelectedLabels: {[key: string]: string | string[]} = {};
if (label2Value) newSelectedLabels['标签2'] = label2Value;
if (label3Value) newSelectedLabels['标签3'] = label3Value;
if (label4Value) newSelectedLabels['标签4'] = label4Value;
if (label5Value) newSelectedLabels['标签5'] = label5Value;
if (label6Value) newSelectedLabels['标签6'] = label6Value;
// 添加标签10的自动填充
newSelectedLabels['标签10'] = ['复版', '开货版不打版'];
// 保留用户手动选择的标签1、7、8、9
setSelectedLabels(prev => ({
...prev,
...newSelectedLabels
}));
console.log('自动提取的标签值:', newSelectedLabels);
// 显示提取结果的提示
if (Object.keys(newSelectedLabels).length > 0 && bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'success',
message: `已自动提取 ${Object.keys(newSelectedLabels).length} 个标签值`
});
}
// 自动执行所有三个查询 - 对第一条记录顺序执行查询
// 顺序执行查询,避免 loading 状态冲突
try {
await handleQueryDatabase(recordValList[0]);
await handleSecondaryProcessQuery(recordValList[0]);
await handlePricingDetailsQuery(recordValList[0]);
} catch (queryError) {
console.error('自动查询出错:', queryError);
}
}
} else {
setRecordDetails([]);
}
} catch (error) {
console.error('选择记录时出错:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'error',
message: '选择记录时出错,请重试'
});
}
} finally {
setLoading(false);
}
};
// 清空选中的记录
const handleClearRecords = () => {
setSelectedRecords([]);
setRecordDetails([]);
setQueryResults([]);
setSecondaryProcessResults([]);
setPricingDetailsResults([]);
// 同时清空标签选择
setSelectedLabels({});
setExpectedDate(null);
};
// 定价数据表格列定义
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
},
{
title: 'Pack ID',
dataIndex: 'pack_id',
key: 'pack_id',
},
{
title: 'Pack Type',
dataIndex: 'pack_type',
key: 'pack_type',
},
{
title: '物料编码',
dataIndex: 'material_code',
key: 'material_code',
},
{
title: '一级分类',
dataIndex: 'category1_name',
key: 'category1_name',
},
{
title: '二级分类',
dataIndex: 'category2_name',
key: 'category2_name',
},
{
title: '三级分类',
dataIndex: 'category3_name',
key: 'category3_name',
},
{
title: '品类属性',
dataIndex: '品类属性',
key: '品类属性',
},
];
// 二次工艺表格列定义
const secondaryProcessColumns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
},
{
title: 'Foreign ID',
dataIndex: 'foreign_id',
key: 'foreign_id',
},
{
title: 'Pack Type',
dataIndex: 'pack_type',
key: 'pack_type',
},
{
title: '项目',
dataIndex: 'costs_item',
key: 'costs_item',
},
{
title: '二次工艺',
dataIndex: 'costs_type',
key: 'costs_type',
},
{
title: '备注',
dataIndex: 'remarks',
key: 'remarks',
},
];
// 定价详情表格列定义
const pricingDetailsColumns = [
{
title: 'Pack ID',
dataIndex: 'pack_id',
key: 'pack_id',
},
{
title: '倍率',
dataIndex: '倍率',
key: '倍率',
},
{
title: '加工费',
dataIndex: '加工费',
key: '加工费',
},
{
title: '总价',
dataIndex: '总价',
key: '总价',
},
];
return (
<div style={{ padding: '20px' }}>
{/* 入口选择弹窗 */}
<Modal
title="请选择功能入口"
visible={modeSelectionVisible}
footer={null}
onCancel={() => setModeSelectionVisible(false)}
maskClosable={false}
>
<div style={{ display: 'flex', gap: 24, justifyContent: 'center', padding: '12px 8px 8px', flexWrap: 'wrap' }}>
<Card
style={{
width: 280,
cursor: 'pointer',
borderRadius: 12,
boxShadow: '0 8px 24px rgba(0,0,0,0.06)',
border: '1px solid #e5e7eb',
background: 'linear-gradient(180deg, #fff, #f9fbff)'
}}
onClick={() => chooseMode('generate')}
>
<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={() => chooseMode('generate')}></Button>
</div>
</Card>
<Card
style={{
width: 280,
cursor: 'pointer',
borderRadius: 12,
boxShadow: '0 8px 24px rgba(0,0,0,0.06)',
border: '1px solid #e5e7eb',
background: 'linear-gradient(180deg, #fff, #f9fbff)'
}}
onClick={() => chooseMode('adjust')}
>
<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={() => chooseMode('adjust')}></Button>
</div>
</Card>
<Card
style={{
width: 280,
cursor: 'pointer',
borderRadius: 12,
boxShadow: '0 8px 24px rgba(0,0,0,0.06)',
border: '1px solid #e5e7eb',
background: 'linear-gradient(180deg, #fff, #f9fbff)'
}}
onClick={() => chooseMode('batch')}
>
<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={() => chooseMode('batch')}></Button>
</div>
</Card>
</div>
</Modal>
{mode === 'generate' && (
<div style={{ marginBottom: '24px' }}>
<Title heading={2} style={{
background: 'linear-gradient(135deg, #3b82f6, #1d4ed8)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
marginBottom: '8px'
}}>
</Title>
<Text type="tertiary" style={{ fontSize: '14px' }}>线</Text>
</div>
)}
{mode === 'batch' && (
<div style={{ marginBottom: '24px' }}>
<Title heading={2} style={{
background: 'linear-gradient(135deg, #10b981, #059669)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
marginBottom: '8px'
}}>
</Title>
<Text type="tertiary" style={{ fontSize: '14px' }}></Text>
</div>
)}
{mode === 'adjust' && (
<div style={{ marginBottom: '24px' }}>
<Title heading={2} style={{
background: 'linear-gradient(135deg, #f59e0b, #d97706)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
marginBottom: '8px'
}}>
</Title>
<Text type="tertiary" style={{ fontSize: '14px' }}>线</Text>
</div>
)}
{/* 功能入口切换与调整入口 */}
{mode !== null && (
<div style={{ margin: '18px 0 14px' }}>
<Space spacing={16} align='center'>
<Select value={mode} onChange={(v) => setMode(v as any)}
optionList={[
{ value: 'generate', label: '生成流程日期' },
{ value: 'adjust', label: '调整流程日期' },
{ value: 'batch', label: '批量生成' }
]} />
{mode === 'adjust' && (
<Space spacing={12} align='center'>
<Button type="primary" onClick={async () => {
try {
const selection = await bitable.base.getSelection();
const recordId = selection?.recordId || '';
const tableId = selection?.tableId || '';
if (!recordId) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 'warning', message: '请先在数据表中选中一条货期记录' });
}
return;
}
2025-11-13 15:59:08 +08:00
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: '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 {
2025-11-06 16:19:08 +08:00
if (bitable.ui.showToast) {
2025-11-13 15:59:08 +08:00
await bitable.ui.showToast({ toastType: 'warning', message: '请在货期记录或OMS看板表中选择记录' });
2025-11-06 16:19:08 +08:00
}
return;
}
} catch (e) {
console.error('读取当前选中记录失败:', e);
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 'error', message: '读取当前选中记录失败' });
}
}
}}></Button>
</Space>
)}
{/* 批量处理功能 - 只在批量模式下显示 */}
</Space>
</div>
)}
{/* 时效计算结果模态框 */}
<Modal
title="预计开始和完成时间"
visible={timelineVisible}
onCancel={() => {
setTimelineVisible(false);
setTimelineAdjustments({}); // 关闭时重置调整
setDeliveryMarginDeductions(0); // 关闭时重置交期余量扣减
setCompletionDateAdjustment(0); // 关闭时重置最后流程完成日期调整
}}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space align="center">
<Text>()</Text>
<InputNumber
min={0}
step={1}
value={baseBufferDays}
onChange={(val) => {
const n = Number(val);
setBaseBufferDays(Number.isFinite(n) && n >= 0 ? n : 0);
}}
style={{ width: 90 }}
/>
<Button onClick={resetTimelineAdjustments}>
</Button>
</Space>
<Button
type="primary"
onClick={async () => {
try {
// 如果有当前批量记录信息,使用批量数据进行保存
if (currentBatchRecord) {
console.log('使用批量记录数据进行保存:', currentBatchRecord);
await saveTimelineData({
timelineResults: timelineResults,
selectedRecords: currentBatchRecord.selectedRecords,
recordDetails: currentBatchRecord.recordDetails,
timelineAdjustments: timelineAdjustments,
labels: currentBatchRecord.labels,
expectedDate: currentBatchRecord.expectedDate,
startTime: currentBatchRecord.startTime
});
} else {
// 普通模式保存
await saveTimelineData();
}
} catch (error) {
// 错误已在saveTimelineData中处理
}
setTimelineVisible(false);
// 清除批量记录信息
setCurrentBatchRecord(null);
}}
>
</Button>
</div>
}
width={900}
>
<div style={{ maxHeight: '80vh', overflowY: 'auto' }}>
{timelineResults.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px' }}>
<Text type="tertiary"></Text>
</div>
) : (
<div>
<div style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '12px' }}>
<Text strong></Text>
<DatePicker
style={{ width: 280 }}
placeholder="请选择起始时间"
value={startTime}
onChange={(date) => {
setStartTime(date);
}}
type="dateTime"
format="yyyy-MM-dd HH:mm"
/>
</div>
{timelineResults.map((result, index) => {
const adjustment = timelineAdjustments[index] || 0;
const baseValue = (typeof result.timelineValue === 'number')
? result.timelineValue
: (typeof result.adjustedTimelineValue === 'number')
? result.adjustedTimelineValue
: 0;
const adjustedValue = baseValue + adjustment;
// 检查是否为周转周期节点
const isTurnoverNode = result.nodeName === '周转周期';
// 检查是否存在周转周期为零的情况
const hasTurnoverNodeWithZero = timelineResults.some((r, i) => {
if (r.nodeName === '周转周期') {
const adj = timelineAdjustments[i] || 0;
const baseVal = (typeof r.timelineValue === 'number')
? r.timelineValue
: (typeof r.adjustedTimelineValue === 'number')
? r.adjustedTimelineValue
: 0;
return (baseVal + adj) === 0;
}
return false;
});
// 当前节点是否为零值的周转周期节点
const isCurrentTurnoverZero = isTurnoverNode && adjustedValue === 0;
// 计算动态缓冲期,用于限制节点增加操作
const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0);
const baseBuferDays = baseBufferDays;
const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments);
// 新的复合限制逻辑:
// 1. 如果缓冲期 > 0允许操作
// 2. 如果缓冲期 = 0进一步判断交期余量
// 3. 如果缓冲期 = 0 且交期余量 <= 0禁止操作
let hasNegativeBuffer = false;
// 计算交期余量(仅用于显示,不用于限制)
let deliveryMargin = 0;
if (timelineResults.length > 0) {
// 获取有效的最后流程完成日期(与交期余量计算逻辑保持一致)
let effectiveLastProcess = null;
let lastCompletionDate = null;
for (let i = timelineResults.length - 1; i >= 0; i--) {
const process = timelineResults[i];
const processDate = new Date(process.estimatedEnd);
if (!isNaN(processDate.getTime()) && process.estimatedEnd !== '时效值为0') {
effectiveLastProcess = process;
lastCompletionDate = processDate;
break;
}
}
if (!effectiveLastProcess) {
effectiveLastProcess = timelineResults[timelineResults.length - 1];
lastCompletionDate = new Date(effectiveLastProcess.estimatedEnd);
}
// 计算最后完成日期(考虑调整)+ 动态缓冲期
const adjustedCompletionDate = new Date(lastCompletionDate);
adjustedCompletionDate.setDate(adjustedCompletionDate.getDate() + completionDateAdjustment);
const deliveryDate = new Date(adjustedCompletionDate);
deliveryDate.setDate(deliveryDate.getDate() + dynamicBufferDays);
if (expectedDate) {
// 有客户期望日期:交期余量 = 客户期望日期 - (最后流程完成 + 缓冲期)
const timeDiff = expectedDate.getTime() - deliveryDate.getTime();
const baseDeliveryMargin = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
deliveryMargin = baseDeliveryMargin - deliveryMarginDeductions;
} else {
// 无客户期望日期:交期余量 = (最后流程完成 + 缓冲期) - 今天
const today = new Date();
today.setHours(0, 0, 0, 0);
const timeDiff = deliveryDate.getTime() - today.getTime();
const baseDeliveryMargin = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
deliveryMargin = baseDeliveryMargin - deliveryMarginDeductions;
}
}
// 执行复合限制判断 - 只有在缓冲期为0时才检查最终限制
let canIncrease = false;
if (dynamicBufferDays > 0) {
// 1. 缓冲期 > 0允许操作
canIncrease = true;
} else {
// 2. 缓冲期 = 0检查最终限制最后节点预计完成时间是否已达到客户期望日期
if (expectedDate && timelineResults.length > 0) {
// 获取有效的最后流程完成日期
let effectiveLastProcess = null;
let lastCompletionDate = null;
for (let i = timelineResults.length - 1; i >= 0; i--) {
const process = timelineResults[i];
const processDate = new Date(process.estimatedEnd);
if (!isNaN(processDate.getTime()) && process.estimatedEnd !== '时效值为0') {
effectiveLastProcess = process;
lastCompletionDate = processDate;
break;
}
}
if (!effectiveLastProcess) {
effectiveLastProcess = timelineResults[timelineResults.length - 1];
lastCompletionDate = new Date(effectiveLastProcess.estimatedEnd);
}
// 计算最后流程完成日期(包含调整)
const adjustedCompletionDate = new Date(lastCompletionDate);
adjustedCompletionDate.setDate(adjustedCompletionDate.getDate() + completionDateAdjustment);
// 检查是否已达到客户期望日期
const timeDiffToExpected = expectedDate.getTime() - adjustedCompletionDate.getTime();
const daysToExpected = Math.ceil(timeDiffToExpected / (1000 * 60 * 60 * 24));
canIncrease = daysToExpected > 0; // 只有当还有剩余天数时才允许调整
} else {
// 无客户期望日期时,理论上可以无限调整
canIncrease = true;
}
}
hasNegativeBuffer = !canIncrease;
return (
<Card key={index} style={{ marginBottom: '8px', padding: '12px', position: 'relative' }}>
<div style={{ position: 'absolute', top: '12px', left: '12px', zIndex: 1, display: 'flex', alignItems: 'center', gap: '16px' }}>
<Title heading={6} style={{ margin: 0, color: '#1890ff', fontSize: '18px' }}>
{result.processOrder && `${result.processOrder}. `}{result.nodeName}
{result.isAccumulated && result.allMatchedRecords ? (
<span style={{
marginLeft: '8px',
fontSize: '12px',
color: '#000000',
fontWeight: 'normal'
}}>
( {result.allMatchedRecords.length} : {result.allMatchedRecords.map(record => record.recordId).join(', ')})
</span>
) : result.timelineRecordId ? (
<span style={{
marginLeft: '8px',
fontSize: '12px',
color: '#000000',
fontWeight: 'normal'
}}>
({result.timelineRecordId})
</span>
) : (
<span style={{
marginLeft: '8px',
fontSize: '12px',
color: '#ff4d4f',
fontWeight: 'normal'
}}>
()
</span>
)}
</Title>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Text style={{ fontSize: '14px', color: '#666', whiteSpace: 'nowrap' }}></Text>
<DatePicker
size="small"
2025-11-11 17:39:18 +08:00
type="dateTime"
format="yyyy-MM-dd HH:mm"
defaultPickerValue={(() => {
try {
const expectedEndStr = timelineResults?.[index]?.estimatedEnd;
if (expectedEndStr && typeof expectedEndStr === 'string' && !expectedEndStr.includes('未找到')) {
const expectedEnd = new Date(expectedEndStr);
if (!isNaN(expectedEnd.getTime())) {
const dv = new Date(expectedEnd);
dv.setSeconds(0, 0);
return dv;
}
}
} catch {}
return undefined;
})()}
placeholder="选择日期时间"
style={{ width: '200px' }}
2025-11-06 16:19:08 +08:00
value={actualCompletionDates[index] || null}
onChange={(date) => {
2025-11-11 17:39:18 +08:00
// 自动填充默认时分:仅在用户未指定具体时分时套用预计完成的时分
let nextDate = date ? new Date(date) : null;
try {
if (nextDate) {
const expectedEndStr = timelineResults?.[index]?.estimatedEnd;
if (expectedEndStr && typeof expectedEndStr === 'string' && !expectedEndStr.includes('未找到')) {
const expectedEnd = new Date(expectedEndStr);
if (!isNaN(expectedEnd.getTime())) {
const h = nextDate.getHours();
const m = nextDate.getMinutes();
const s = nextDate.getSeconds();
// 当用户未选择具体时间默认为00:00:00应用预计完成的时分
if (h === 0 && m === 0 && s === 0) {
nextDate.setHours(expectedEnd.getHours(), expectedEnd.getMinutes(), 0, 0);
}
}
}
}
} catch {}
2025-11-06 16:19:08 +08:00
setActualCompletionDates(prev => ({
...prev,
2025-11-11 17:39:18 +08:00
[index]: nextDate
2025-11-06 16:19:08 +08:00
}));
// 自动设置调整量:按工作日规则(考虑内部/外部、休息日、跳过日期)
try {
const currentResult = timelineResults[index];
const startStr = currentResult?.estimatedStart;
if (startStr && date) {
const startDate = new Date(startStr);
if (!isNaN(startDate.getTime())) {
const calcRaw = currentResult?.calculationMethod || '外部';
const calcMethod = (calcRaw === '内部' || calcRaw === 'internal') ? '内部' : '外部';
const weekendDays = currentResult?.weekendDaysConfig || currentResult?.weekendDays || [];
const excludedDates = Array.isArray(currentResult?.excludedDates) ? currentResult!.excludedDates : [];
// 与正向计算一致:先对起始时间应用工作时间调整
const adjustedStart = adjustToNextWorkingHour(startDate, calcMethod, weekendDays, excludedDates);
const targetDate = new Date(date);
// 使用二分搜索反推工作日数按0.5天粒度),使得正向计算的结束时间尽量贴近目标日期
const dayMs = 1000 * 60 * 60 * 24;
2025-11-13 15:59:08 +08:00
const approxNatural = (targetDate.getTime() - adjustedStart.getTime()) / dayMs;
2025-11-06 16:19:08 +08:00
const endFor = (bd: number): Date => {
2025-11-13 15:59:08 +08:00
if (bd === 0) return new Date(adjustedStart);
2025-11-06 16:19:08 +08:00
return calcMethod === '内部'
? addInternalBusinessTime(new Date(adjustedStart), bd, weekendDays, excludedDates)
: addBusinessDaysWithHolidays(new Date(adjustedStart), bd, weekendDays, excludedDates);
};
2025-11-13 15:59:08 +08:00
let lo = approxNatural - 50;
let hi = approxNatural + 50;
2025-11-06 16:19:08 +08:00
for (let it = 0; it < 40; it++) {
const mid = (lo + hi) / 2;
const end = endFor(mid);
if (end.getTime() < targetDate.getTime()) {
lo = mid;
} else {
hi = mid;
}
}
const desiredBusinessDays = Math.round(hi * 2) / 2; // 与UI保持0.5粒度一致
// 目标调整量 = 目标工作日数 - 基准时效值
const baseValue = (typeof currentResult.timelineValue === 'number')
? currentResult.timelineValue
: (typeof currentResult.adjustedTimelineValue === 'number')
? currentResult.adjustedTimelineValue
: 0;
const desiredAdjustmentAbs = desiredBusinessDays - baseValue;
const currentAdj = timelineAdjustments[index] || 0;
const deltaToApply = desiredAdjustmentAbs - currentAdj;
if (deltaToApply !== 0) {
const updated = handleTimelineAdjustment(index, deltaToApply);
if (!updated) {
// 如果该节点不允许调整,仍然重算以联动后续节点
setTimeout(() => {
recalculateTimeline(timelineAdjustments, false);
}, 0);
return;
}
}
}
}
} catch (e) {
console.warn('自动调整量计算失败:', e);
}
// 无论如何都重新计算时间线以联动交期余量
setTimeout(() => {
recalculateTimeline(timelineAdjustments, false);
}, 0);
}}
/>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr', gap: '12px', marginTop: '40px' }}>
<div>
<Text strong style={{ display: 'block', marginBottom: '2px', fontSize: '15px' }}></Text>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
2025-11-11 17:39:18 +08:00
{/* 注释掉 -0.5 功能按钮 */}
{false && (
<Button
size="small"
onClick={() => handleComplexAdjustment(index, -0.5)}
disabled={adjustedValue <= 0 || isCurrentTurnoverZero}
style={{ minWidth: '28px', height: '24px', fontSize: '13px' }}
title={isCurrentTurnoverZero ? '周转周期为零,无法调整' : ''}
>
-0.5
</Button>
)}
{/* 注释掉 -1 功能按钮 */}
{false && (
<Button
size="small"
onClick={() => handleComplexAdjustment(index, -1)}
disabled={adjustedValue <= 0 || isCurrentTurnoverZero}
style={{ minWidth: '24px', height: '24px', fontSize: '13px' }}
title={isCurrentTurnoverZero ? '周转周期为零,无法调整' : ''}
>
-1
</Button>
)}
2025-11-06 16:19:08 +08:00
<div style={{
minWidth: '70px',
textAlign: 'center',
padding: '2px 6px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
backgroundColor: adjustment !== 0 ? '#fff7e6' : '#f5f5f5',
fontSize: '13px'
}}>
<Text style={{
color: adjustedValue > 0 ? '#52c41a' : '#ff4d4f',
fontWeight: 'bold',
fontSize: '13px'
}}>
{adjustedValue.toFixed(1)}
</Text>
{adjustment !== 0 && (
<div style={{ fontSize: '10px', color: '#666', lineHeight: '1.2' }}>
: {baseValue.toFixed(1)}
<br />
: {adjustment > 0 ? '+' : ''}{adjustment.toFixed(1)}
</div>
)}
</div>
2025-11-11 17:39:18 +08:00
{/* 注释掉 +1 功能按钮 */}
{false && (
<Button
size="small"
onClick={() => handleComplexAdjustment(index, 1)}
disabled={isCurrentTurnoverZero || (hasTurnoverNodeWithZero && !isTurnoverNode) || (isTurnoverNode && !isCurrentTurnoverZero) || hasNegativeBuffer}
style={{ minWidth: '24px', height: '24px', fontSize: '13px' }}
title={
isCurrentTurnoverZero ? '周转周期为零,无法调整' :
(hasTurnoverNodeWithZero && !isTurnoverNode) ? '周转周期为零,其他节点无法增加' :
(isTurnoverNode && !isCurrentTurnoverZero) ? '周转周期节点会自动调整,无法手动修改' :
hasNegativeBuffer ? `缓冲期已耗尽且交期余量不足,不允许增加时效` : ''
}
>
+1
</Button>
)}
{/* 注释掉 +0.5 功能按钮 */}
{false && (
<Button
size="small"
onClick={() => handleComplexAdjustment(index, 0.5)}
disabled={isCurrentTurnoverZero || (hasTurnoverNodeWithZero && !isTurnoverNode) || (isTurnoverNode && !isCurrentTurnoverZero) || hasNegativeBuffer}
style={{ minWidth: '28px', height: '24px', fontSize: '13px' }}
title={
isCurrentTurnoverZero ? '周转周期为零,无法调整' :
(hasTurnoverNodeWithZero && !isTurnoverNode) ? '周转周期为零,其他节点无法增加' :
(isTurnoverNode && !isCurrentTurnoverZero) ? '周转周期节点会自动调整,无法手动修改' :
hasNegativeBuffer ? `缓冲期已耗尽且交期余量不足,不允许增加时效` : ''
}
>
+0.5
</Button>
)}
2025-11-06 16:19:08 +08:00
</div>
</div>
<div>
<Text strong style={{ display: 'block', marginBottom: '2px', fontSize: '15px' }}></Text>
<Text style={{ fontSize: '14px' }}>{formatDate(result.estimatedStart)}</Text>
<div style={{ fontSize: '12px', color: '#666', marginTop: '1px' }}>
<Text>{getDayOfWeek(result.estimatedStart)}</Text>
</div>
</div>
<div>
<Text strong style={{ display: 'block', marginBottom: '2px', fontSize: '15px' }}></Text>
<Text style={{
color: result.estimatedEnd.includes('未找到') || result.estimatedEnd.includes('时效值为0') ? '#ff4d4f' : '#52c41a',
fontSize: '14px'
}}>
{result.estimatedEnd === '时效值为0' ? result.estimatedEnd : formatDate(result.estimatedEnd)}
</Text>
<div style={{ fontSize: '12px', color: '#666', marginTop: '1px' }}>
<Text>{getDayOfWeek(result.estimatedEnd)}</Text>
</div>
</div>
<div>
<Text strong style={{ display: 'block', marginBottom: '2px', fontSize: '15px' }}></Text>
<Text style={{ color: '#1890ff', fontSize: '14px' }}>
{calculateActualDays(result.estimatedStart, result.estimatedEnd)}
</Text>
<div style={{ fontSize: '12px', color: '#666', marginTop: '1px' }}>
<Text> {adjustedValue.toFixed(1)} </Text>
</div>
</div>
<div>
<Text strong style={{ display: 'block', marginBottom: '2px', fontSize: '15px' }}></Text>
<Text style={{
color: result.calculationMethod === '内部' ? '#1890ff' : '#52c41a',
fontSize: '14px'
}}>
{result.calculationMethod || '外部'}
{result.ruleDescription && (
<span style={{
marginLeft: '8px',
color: '#ff7a00',
fontSize: '12px',
fontStyle: 'italic'
}}>
({result.ruleDescription})
</span>
)}
<span
style={{
marginLeft: '4px',
cursor: 'help',
color: '#666',
fontSize: '12px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: '14px',
height: '14px',
borderRadius: '50%',
backgroundColor: '#f0f0f0',
border: '1px solid #d9d9d9'
}}
title={`计算方式详情:\n${result.calculationMethod === '内部' ? '内部计算 (9小时工作制)' : '外部计算 (24小时制)'}${result.ruleDescription ? `\n应用规则${result.ruleDescription}` : ''}\n已跳过周末${result.skippedWeekends || 0}\n${result.weekendDaysConfig && result.weekendDaysConfig.length > 0 ? `休息日配置:${result.weekendDaysConfig.map(day => ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][day]).join(', ')}` : '休息日配置:无固定休息日'}${result.calculationMethod === '内部' ? '\n工作时间9:00-18:00 (9小时制)' : ''}${(Array.isArray(result.actualExcludedDates) && result.actualExcludedDates.length > 0) ? `\n\n跳过的具体日期\n${result.actualExcludedDates.join('\n')}` : ''}`}
>
?
</span>
</Text>
<div style={{ fontSize: '12px', color: '#666', marginTop: '2px' }}>
<Text>{(result.actualExcludedDatesCount && result.actualExcludedDatesCount > 0) ? `${result.actualExcludedDatesCount}` : '无'}</Text>
</div>
</div>
</div>
</Card>
);
})}
{/* 计算公式展示卡片 */}
{timelineResults.length > 0 && (
<Card style={{
marginBottom: '16px',
padding: '16px',
background: 'linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%)',
border: '2px solid #1890ff',
borderRadius: '12px',
boxShadow: '0 4px 12px rgba(24, 144, 255, 0.15)'
}}>
<div style={{ textAlign: 'center' }}>
<Title heading={5} style={{
margin: '0 0 16px 0',
color: '#1890ff',
fontSize: '20px',
fontWeight: 'bold'
}}>
📅
</Title>
<div style={{
display: 'grid',
gridTemplateColumns: expectedDate ? '1fr auto 1fr auto 1fr auto 1fr auto 1fr' : '1fr auto 1fr auto 1fr auto 1fr',
alignItems: 'center',
gap: '12px',
marginBottom: '16px',
padding: '16px',
backgroundColor: 'rgba(255, 255, 255, 0.8)',
borderRadius: '8px',
border: '1px solid rgba(24, 144, 255, 0.2)'
}}>
{(() => {
// 获取有效的最后流程完成日期
let effectiveLastProcess = null;
let lastCompletionDate = null;
// 从后往前查找第一个有效的流程完成日期
for (let i = timelineResults.length - 1; i >= 0; i--) {
const process = timelineResults[i];
const processDate = new Date(process.estimatedEnd);
// 检查日期是否有效且不是"时效值为0"的情况
if (!isNaN(processDate.getTime()) && process.estimatedEnd !== '时效值为0') {
effectiveLastProcess = process;
lastCompletionDate = processDate;
break;
}
}
// 如果没有找到有效的完成日期,使用最后一个流程
if (!effectiveLastProcess) {
effectiveLastProcess = timelineResults[timelineResults.length - 1];
lastCompletionDate = new Date(effectiveLastProcess.estimatedEnd);
}
// 计算所有节点的总调整量(用于动态缓冲期计算)
const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0);
// 计算动态缓冲期基础14天 - 节点总调整量最小为0天
const baseBuferDays = baseBufferDays;
const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments);
// 计算最后完成日期 + 动态缓冲期(考虑最后流程完成日期的调整)
const adjustedCompletionDate = new Date(lastCompletionDate);
adjustedCompletionDate.setDate(adjustedCompletionDate.getDate() + completionDateAdjustment);
const deliveryDate = new Date(adjustedCompletionDate);
deliveryDate.setDate(deliveryDate.getDate() + dynamicBufferDays);
// 计算交期余量
// 如果客户期望日期不为空:交期余量 = 客户期望日期 - (最后流程完成 + 缓冲期)
// 如果客户期望日期为空:交期余量 = (最后流程完成 + 缓冲期)
let timeDiff, baseDaysDiff, finalDaysDiff;
if (expectedDate) {
// 有客户期望日期:计算期望日期与交付日期的差值
timeDiff = expectedDate.getTime() - deliveryDate.getTime();
baseDaysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
} else {
// 无客户期望日期:直接显示交付日期(最后流程完成 + 缓冲期)
const today = new Date();
today.setHours(0, 0, 0, 0);
timeDiff = deliveryDate.getTime() - today.getTime();
baseDaysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
}
// 计算最终交期余量:基础余量 - 扣减天数
finalDaysDiff = baseDaysDiff - deliveryMarginDeductions;
return (
<>
{expectedDate && (
<>
{/* 客户期望日期 */}
<div style={{ textAlign: 'center' }}>
<Text strong style={{ display: 'block', fontSize: '14px', color: '#666', marginBottom: '4px' }}>
</Text>
<Text style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#722ed1',
display: 'block'
}}>
{formatDate(expectedDate)}
</Text>
<Text style={{ fontSize: '12px', color: '#999' }}>
{getDayOfWeek(expectedDate)}
</Text>
</div>
{/* 减号 */}
<div style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#1890ff',
display: 'flex',
alignItems: 'center'
}}>
-
</div>
{/* 最后流程完成 */}
<div style={{ textAlign: 'center' }}>
<Text strong style={{ display: 'block', fontSize: '14px', color: '#666', marginBottom: '4px' }}>
{effectiveLastProcess === timelineResults[timelineResults.length - 1]
? '最后流程完成'
: `有效流程完成 (${effectiveLastProcess.nodeName})`}
</Text>
<Text style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#52c41a',
display: 'block'
}}>
{formatDate(adjustedCompletionDate)}
</Text>
<Text style={{ fontSize: '12px', color: '#999' }}>
{getDayOfWeek(adjustedCompletionDate)}
</Text>
{effectiveLastProcess !== timelineResults[timelineResults.length - 1] && (
<Text style={{ fontSize: '11px', color: '#fa8c16', fontStyle: 'italic', display: 'block', marginTop: '2px' }}>
(0使)
</Text>
)}
{completionDateAdjustment > 0 && (
<Text style={{ fontSize: '11px', color: '#1890ff', fontStyle: 'italic', display: 'block', marginTop: '2px' }}>
( +{completionDateAdjustment})
</Text>
)}
</div>
{/* 加号 */}
<div style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#1890ff',
display: 'flex',
alignItems: 'center'
}}>
+
</div>
{/* 缓冲期 */}
<div style={{ textAlign: 'center' }}>
<Text strong style={{ display: 'block', fontSize: '14px', color: '#666', marginBottom: '4px' }}>
</Text>
<Text style={{
fontSize: '20px',
fontWeight: 'bold',
color: dynamicBufferDays < baseBufferDays ? '#ff4d4f' : '#fa8c16',
display: 'block'
}}>
{dynamicBufferDays}
</Text>
<Text style={{ fontSize: '12px', color: '#999' }}>
</Text>
</div>
{/* 等号 */}
<div style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#1890ff',
display: 'flex',
alignItems: 'center'
}}>
=
</div>
</>
)}
{!expectedDate && (
<>
{/* 最后流程完成 */}
<div style={{ textAlign: 'center' }}>
<Text strong style={{ display: 'block', fontSize: '14px', color: '#666', marginBottom: '4px' }}>
{effectiveLastProcess === timelineResults[timelineResults.length - 1]
? '最后流程完成'
: `有效流程完成 (${effectiveLastProcess.nodeName})`}
</Text>
<Text style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#52c41a',
display: 'block'
}}>
{formatDate(effectiveLastProcess.estimatedEnd)}
</Text>
<Text style={{ fontSize: '12px', color: '#999' }}>
{getDayOfWeek(effectiveLastProcess.estimatedEnd)}
</Text>
{effectiveLastProcess !== timelineResults[timelineResults.length - 1] && (
<Text style={{ fontSize: '11px', color: '#fa8c16', fontStyle: 'italic', display: 'block', marginTop: '2px' }}>
(0使)
</Text>
)}
</div>
{/* 加号 */}
<div style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#1890ff',
display: 'flex',
alignItems: 'center'
}}>
+
</div>
{/* 缓冲期 */}
<div style={{ textAlign: 'center' }}>
<Text strong style={{ display: 'block', fontSize: '14px', color: '#666', marginBottom: '4px' }}>
</Text>
<Text style={{
fontSize: '20px',
fontWeight: 'bold',
color: dynamicBufferDays < baseBufferDays ? '#ff4d4f' : '#fa8c16',
display: 'block'
}}>
{dynamicBufferDays}
</Text>
<Text style={{ fontSize: '12px', color: '#999' }}>
</Text>
</div>
{/* 减号 */}
<div style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#1890ff',
display: 'flex',
alignItems: 'center'
}}>
-
</div>
{/* 今天 */}
<div style={{ textAlign: 'center' }}>
<Text strong style={{ display: 'block', fontSize: '14px', color: '#666', marginBottom: '4px' }}>
</Text>
<Text style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#666',
display: 'block'
}}>
{formatDate(new Date())}
</Text>
<Text style={{ fontSize: '12px', color: '#999' }}>
{getDayOfWeek(new Date())}
</Text>
</div>
{/* 等号 */}
<div style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#1890ff',
display: 'flex',
alignItems: 'center'
}}>
=
</div>
</>
)}
{/* 结果日期说明(位于等式下方) */}
<div style={{textAlign:'center', marginTop:'8px'}}>
<Text style={{ fontSize: '12px', color: '#999' }}>
+ {formatDate(deliveryDate)}{getDayOfWeek(deliveryDate)}
</Text>
</div>
{/* 交期余量结果 */}
<div style={{
textAlign: 'center',
padding: '12px',
backgroundColor: finalDaysDiff >= 0 ? '#f6ffed' : '#fff2f0',
2025-11-06 15:36:54 +08:00
borderRadius: '8px',
border: `2px solid ${finalDaysDiff >= 0 ? '#52c41a' : '#ff4d4f'}`
}}>
<Text strong style={{ display: 'block', fontSize: '14px', color: '#666', marginBottom: '4px' }}>
</Text>
<Text style={{
fontSize: '24px',
fontWeight: 'bold',
color: finalDaysDiff >= 0 ? '#52c41a' : '#ff4d4f',
display: 'block'
}}>
{finalDaysDiff >= 0 ? '+' : ''}{finalDaysDiff}
</Text>
<Text style={{
fontSize: '12px',
color: finalDaysDiff >= 0 ? '#52c41a' : '#ff4d4f',
fontWeight: 'bold'
}}>
{finalDaysDiff >= 0 ? '✅ 时间充裕' : '⚠️ 时间紧张'}
</Text>
</div>
</>
);
})()}
</div>
<div style={{
padding: '12px',
backgroundColor: 'rgba(255, 255, 255, 0.6)',
borderRadius: '6px',
fontSize: '13px',
color: '#666',
lineHeight: '1.5'
}}>
{(() => {
// 计算动态缓冲期和总调整量
const totalAdjustments = Object.values(timelineAdjustments).reduce((sum, adj) => sum + adj, 0);
const baseBuferDays = baseBufferDays;
const dynamicBufferDays = Math.max(0, baseBuferDays - totalAdjustments);
return (
<Text>
💡 <strong></strong>
{expectedDate
? `交期余量 = 客户期望日期 - (最后流程完成日期 + ${dynamicBufferDays}天缓冲期)`
: `交期余量 = (最后流程完成日期 + ${dynamicBufferDays}天缓冲期) - 今天`
}
<br />
{expectedDate
? '• 正值表示有充裕时间,负值表示可能延期'
: '• 显示预计交付日期距离今天的天数'
}
<br />
= {baseBuferDays} - 0
{totalAdjustments > 0 && (
<>
<br />
+{totalAdjustments}
</>
)}
</Text>
);
})()}
</div>
</div>
</Card>
)}
2025-10-22 14:08:03 +08:00
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: '#f6f8fa', borderRadius: '6px' }}>
<Text strong>📊 </Text>
<Text style={{ display: 'block', marginTop: '4px' }}>
{timelineResults.length} 使
</Text>
<div style={{ marginTop: '8px', padding: '8px', backgroundColor: '#e6f7ff', borderRadius: '4px' }}>
<Text strong style={{ color: '#1890ff' }}>🗓 </Text>
<Text style={{ display: 'block', marginTop: '4px', fontSize: '14px' }}>
<strong></strong>99:00-18:00
<br />
<strong></strong>24
<br />
<br />
2025-10-27 11:46:57 +08:00
2025-10-22 14:08:03 +08:00
<br />
"工作日"
<br />
使 +1/-1 +0.5/-0.5
<br />
</Text>
</div>
{Object.keys(timelineAdjustments).length > 0 && (
<div style={{ marginTop: '8px', padding: '8px', backgroundColor: '#fff7e6', borderRadius: '4px' }}>
<Text strong style={{ color: '#fa8c16' }}></Text>
<div style={{ marginTop: '4px' }}>
{Object.entries(timelineAdjustments).map(([nodeIndex, adjustment]) => {
const nodeName = timelineResults[parseInt(nodeIndex)]?.nodeName;
return (
<Text key={nodeIndex} style={{ display: 'block', fontSize: '12px' }}>
{nodeName}: {adjustment > 0 ? '+' : ''}{adjustment.toFixed(1)}
</Text>
);
})}
</div>
</div>
)}
</div>
</div>
)}
</div>
</Modal>
2025-10-24 09:27:39 +08:00
{/* 标签选择部分,仅在生成模式显示 */}
{mode === 'generate' && labelOptions && Object.keys(labelOptions).length > 0 && (
2025-10-29 15:03:02 +08:00
<Card title="标签选择" className="card-enhanced" style={{ marginBottom: '24px' }}>
2025-10-22 14:08:03 +08:00
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px' }}>
{Array.from({ length: 10 }, (_, i) => i + 1).map(num => {
const labelKey = `标签${num}`;
const options = labelOptions[labelKey] || [];
const isMultiSelect = labelKey === '标签7' || labelKey === '标签8' || labelKey === '标签10';
return (
<div key={labelKey}>
<Text strong style={{ display: 'block', marginBottom: '8px' }}>{labelKey}</Text>
<Select
2025-10-29 15:03:02 +08:00
className="select-enhanced"
2025-10-22 14:08:03 +08:00
style={{ width: '100%' }}
placeholder={`请选择${labelKey}或输入关键字搜索`}
value={selectedLabels[labelKey]}
onChange={(value) => handleLabelChange(labelKey, value)}
multiple={isMultiSelect}
filter
showSearch
>
{options.map((option, optionIndex) => (
<Select.Option key={optionIndex} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
</div>
);
})}
</div>
{/* 客户期望日期选择 */}
2025-10-29 15:03:02 +08:00
<div style={{ marginTop: '24px' }}>
2025-10-22 14:08:03 +08:00
<Text strong style={{ display: 'block', marginBottom: '8px' }}></Text>
<DatePicker
2025-10-29 15:03:02 +08:00
className="input-enhanced"
2025-10-22 14:08:03 +08:00
style={{ width: '300px' }}
placeholder="请选择客户期望日期"
value={expectedDate}
onChange={(date) => setExpectedDate(date)}
format="yyyy-MM-dd"
disabledDate={(date) => {
// 禁用今天之前的日期
const today = new Date();
today.setHours(0, 0, 0, 0);
return date < today;
}}
/>
{expectedDate && (
<Text type="secondary" style={{ marginLeft: '12px' }}>
{formatDate(expectedDate, 'CHINESE_DATE')}
</Text>
)}
</div>
{Object.keys(selectedLabels).length > 0 && (
2025-10-29 15:03:02 +08:00
<div style={{
marginTop: '24px',
padding: '20px',
background: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
borderRadius: '12px',
border: '1px solid #bae6fd'
}}>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
2025-10-22 14:08:03 +08:00
<Button
type='primary'
2025-10-29 15:03:02 +08:00
className="btn-gradient-calculate"
size="large"
2025-10-22 14:08:03 +08:00
onClick={handleCalculateTimeline}
loading={timelineLoading}
disabled={timelineLoading}
2025-10-29 15:03:02 +08:00
style={{ minWidth: '160px' }}
2025-10-22 14:08:03 +08:00
>
</Button>
2025-10-29 15:03:02 +08:00
<Text type="secondary" style={{ fontSize: '14px' }}>
2025-10-22 14:08:03 +08:00
{Object.keys(selectedLabels).length}
</Text>
</Space>
2025-10-29 15:03:02 +08:00
<div style={{ marginTop: '16px' }}>
<Text strong style={{ color: '#0369a1' }}></Text>
<div style={{ marginTop: '8px', display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
2025-10-22 14:08:03 +08:00
{Object.entries(selectedLabels).map(([key, value]) => {
const displayValue = Array.isArray(value) ? value.join(', ') : value;
return (
2025-10-29 15:03:02 +08:00
<span
key={key}
style={{
background: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
color: 'white',
padding: '4px 12px',
borderRadius: '20px',
fontSize: '12px',
fontWeight: 500
2025-10-22 14:08:03 +08:00
}}
>
{key}: {displayValue}
2025-10-29 15:03:02 +08:00
</span>
2025-10-22 14:08:03 +08:00
);
})}
</div>
</div>
</div>
)}
</Card>
)}
2025-10-29 15:03:02 +08:00
{mode === 'batch' && (
<main className="main" style={{ padding: '20px' }}>
{/* 批量处理配置 */}
<div className="batch-config-card" style={{
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
padding: '24px',
borderRadius: '16px',
border: '1px solid #e2e8f0',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.05)',
marginTop: '16px'
}}>
<Title heading={4} style={{ marginBottom: '20px', color: '#334155' }}>
</Title>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{/* 第一行:表和视图选择 */}
<div style={{ display: 'flex', gap: '20px', flexWrap: 'wrap', alignItems: 'center' }}>
<div className="batch-input-group" style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'white',
padding: '12px 16px',
borderRadius: '12px',
border: '1px solid #e2e8f0',
minWidth: '280px'
}}>
<Text style={{ fontWeight: 500, color: '#475569', minWidth: '60px' }}>:</Text>
<Select
value={selectedBatchTableId}
onChange={handleBatchTableChange}
placeholder="请选择表"
loading={tablesLoading}
style={{ flex: 1, minWidth: '180px' }}
>
{availableTables.map(table => (
<Select.Option key={table.id} value={table.id}>
{table.name}
</Select.Option>
))}
</Select>
</div>
<div className="batch-input-group" style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'white',
padding: '12px 16px',
borderRadius: '12px',
border: '1px solid #e2e8f0',
minWidth: '280px'
}}>
<Text style={{ fontWeight: 500, color: '#475569', minWidth: '60px' }}>:</Text>
<Select
value={selectedBatchViewId}
onChange={(value) => setSelectedBatchViewId(value)}
placeholder="请选择视图(可选)"
loading={viewsLoading}
style={{ flex: 1, minWidth: '180px' }}
allowClear
>
{availableViews.map(view => (
<Select.Option key={view.id} value={view.id}>
{view.name}
</Select.Option>
))}
</Select>
</div>
</div>
{/* 第二行:处理行数和操作按钮 */}
<div style={{ display: 'flex', gap: '20px', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap' }}>
<div className="batch-input-group" style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'white',
padding: '12px 16px',
borderRadius: '12px',
border: '1px solid #e2e8f0'
}}>
<Text style={{ fontWeight: 500, color: '#475569' }}>:</Text>
<InputNumber
value={batchRowCount}
onChange={(value) => setBatchRowCount(value || 10)}
min={1}
max={100}
style={{ width: 100 }}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{batchProcessing && (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'rgba(59, 130, 246, 0.1)',
padding: '8px 16px',
borderRadius: '8px',
border: '1px solid rgba(59, 130, 246, 0.2)'
}}>
<div style={{
width: '12px',
height: '12px',
borderRadius: '50%',
background: 'conic-gradient(from 0deg, #3b82f6, transparent)',
animation: 'spin 1s linear infinite'
}}></div>
<Text style={{ color: '#3b82f6', fontWeight: 500 }}>
: {batchProgress.current}/{batchProgress.total}
</Text>
</div>
)}
<Button
type="primary"
theme="solid"
size="large"
loading={batchProcessing}
onClick={() => handleBatchProcess(batchRowCount)}
disabled={batchProcessing || !selectedBatchTableId}
style={{
background: selectedBatchTableId ? 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)' : undefined,
border: 'none',
borderRadius: '12px',
padding: '12px 24px',
fontWeight: 600,
boxShadow: selectedBatchTableId ? '0 4px 12px rgba(59, 130, 246, 0.3)' : undefined
}}
>
{batchProcessing ? '批量处理中...' : '开始批量生成'}
</Button>
</div>
</div>
</div>
</div>
</main>
)}
2025-10-22 14:08:03 +08:00
2025-10-24 09:27:39 +08:00
{mode === 'generate' && (
<main className="main" style={{ padding: '20px' }}>
2025-10-29 15:03:02 +08:00
<Card className="card-enhanced">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '24px' }}>
2025-10-24 09:27:39 +08:00
<div>
2025-10-29 15:03:02 +08:00
<Title heading={3} style={{
margin: 0,
background: 'linear-gradient(135deg, #1890ff 0%, #722ed1 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text'
}}></Title>
<Text type='secondary' style={{ fontSize: '14px', marginTop: '4px' }}>
</Text>
2025-10-24 09:27:39 +08:00
{selectedRecords.length > 0 && (
<Text type='secondary' style={{ fontSize: '12px', marginTop: '4px' }}>
{selectedRecords.length}
</Text>
)}
</div>
2025-10-22 14:08:03 +08:00
2025-10-24 09:27:39 +08:00
<Space>
2025-10-22 14:08:03 +08:00
<Button
2025-10-24 09:27:39 +08:00
type='primary'
2025-10-29 15:03:02 +08:00
className="btn-gradient-select"
size="large"
2025-10-24 09:27:39 +08:00
onClick={handleSelectRecords}
loading={loading}
disabled={loading}
2025-10-22 14:08:03 +08:00
>
2025-10-24 09:27:39 +08:00
{selectedRecords.length > 0 ? '重新选择' : '选择记录'}
2025-10-22 14:08:03 +08:00
</Button>
2025-10-24 09:27:39 +08:00
{selectedRecords.length > 0 && (
<Button
type='secondary'
onClick={handleClearRecords}
size='small'
>
</Button>
)}
</Space>
</div>
{/* 已选择记录的详细信息 */}
{selectedRecords.length > 0 && recordDetails.length > 0 && (
<div style={{
2025-10-29 15:03:02 +08:00
padding: '16px',
background: 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)',
borderRadius: '12px',
border: '1px solid #dee2e6',
marginBottom: '20px',
boxShadow: '0 2px 8px rgba(0,0,0,0.06)'
2025-10-24 09:27:39 +08:00
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
2025-10-22 14:08:03 +08:00
<div>
2025-10-29 15:03:02 +08:00
<Text strong style={{ color: '#495057' }}>:</Text>
<span style={{
marginLeft: '8px',
fontSize: '12px',
background: 'linear-gradient(135deg, #007bff 0%, #0056b3 100%)',
color: 'white',
padding: '2px 8px',
borderRadius: '12px',
fontWeight: 500
}}>
{recordDetails[0].displayValue || recordDetails[0].id}
</span>
2025-10-22 14:08:03 +08:00
</div>
2025-10-24 09:27:39 +08:00
{recordDetails.length > 1 && (
<div>
<Text type='secondary'>+ {recordDetails.length - 1} </Text>
</div>
)}
</div>
2025-10-22 14:08:03 +08:00
</div>
2025-10-24 09:27:39 +08:00
)}
{/* 加载状态 */}
{loading && (
<div style={{ textAlign: 'center', padding: '20px' }}>
<Spin size="large" />
<div style={{ marginTop: '10px' }}>
<Text>...</Text>
</div>
2025-10-22 14:08:03 +08:00
</div>
2025-10-24 09:27:39 +08:00
)}
{/* 空状态提示 */}
{selectedRecords.length === 0 && !loading && (
<div style={{
textAlign: 'center',
padding: '20px',
backgroundColor: '#fafafa',
borderRadius: '6px',
border: '1px dashed #d9d9d9'
}}>
<Text type="tertiary"></Text>
</div>
)}
</Card>
2025-10-29 15:03:02 +08:00
{/* 批量处理配置 - 移动到版单数据下方 */}
{mode === 'batch' && (
<div className="batch-config-card" style={{
background: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
padding: '24px',
borderRadius: '16px',
border: '1px solid #e2e8f0',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.05)',
marginTop: '16px'
}}>
<Title heading={4} style={{ marginBottom: '20px', color: '#334155' }}>
</Title>
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{/* 第一行:表和视图选择 */}
<div style={{ display: 'flex', gap: '20px', flexWrap: 'wrap', alignItems: 'center' }}>
<div className="batch-input-group" style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'white',
padding: '12px 16px',
borderRadius: '12px',
border: '1px solid #e2e8f0',
minWidth: '280px'
}}>
<Text style={{ fontWeight: 500, color: '#475569', minWidth: '60px' }}>:</Text>
<Select
value={selectedBatchTableId}
onChange={handleBatchTableChange}
placeholder="请选择表"
loading={tablesLoading}
style={{ flex: 1, minWidth: '180px' }}
>
{availableTables.map(table => (
<Select.Option key={table.id} value={table.id}>
{table.name}
</Select.Option>
))}
</Select>
</div>
<div className="batch-input-group" style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'white',
padding: '12px 16px',
borderRadius: '12px',
border: '1px solid #e2e8f0',
minWidth: '280px'
}}>
<Text style={{ fontWeight: 500, color: '#475569', minWidth: '60px' }}>:</Text>
<Select
value={selectedBatchViewId}
onChange={(value) => setSelectedBatchViewId(value)}
placeholder="请选择视图(可选)"
loading={viewsLoading}
style={{ flex: 1, minWidth: '180px' }}
allowClear
>
{availableViews.map(view => (
<Select.Option key={view.id} value={view.id}>
{view.name}
</Select.Option>
))}
</Select>
</div>
</div>
{/* 第二行:处理行数和操作按钮 */}
<div style={{ display: 'flex', gap: '20px', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap' }}>
<div className="batch-input-group" style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'white',
padding: '12px 16px',
borderRadius: '12px',
border: '1px solid #e2e8f0'
}}>
<Text style={{ fontWeight: 500, color: '#475569' }}>:</Text>
<InputNumber
value={batchRowCount}
onChange={(value) => setBatchRowCount(value || 10)}
min={1}
max={100}
style={{ width: 100 }}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{batchProcessing && (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
background: 'rgba(59, 130, 246, 0.1)',
padding: '8px 16px',
borderRadius: '8px',
border: '1px solid rgba(59, 130, 246, 0.2)'
}}>
<div style={{
width: '12px',
height: '12px',
borderRadius: '50%',
background: 'conic-gradient(from 0deg, #3b82f6, transparent)',
animation: 'spin 1s linear infinite'
}}></div>
<Text style={{ color: '#3b82f6', fontWeight: 500 }}>
: {batchProgress.current}/{batchProgress.total}
</Text>
</div>
)}
<Button
type="primary"
theme="solid"
size="large"
loading={batchProcessing}
onClick={() => handleBatchProcess(batchRowCount)}
disabled={batchProcessing || !selectedBatchTableId}
style={{
background: selectedBatchTableId ? 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)' : undefined,
border: 'none',
borderRadius: '12px',
padding: '12px 24px',
fontWeight: 600,
boxShadow: selectedBatchTableId ? '0 4px 12px rgba(59, 130, 246, 0.3)' : undefined
}}
>
{batchProcessing ? '批量处理中...' : '开始批量生成'}
</Button>
</div>
</div>
</div>
</div>
)}
2025-10-24 09:27:39 +08:00
</main>
2025-10-22 14:08:03 +08:00
)}
2025-10-24 09:27:39 +08:00
{mode === 'generate' && (
2025-10-22 14:08:03 +08:00
<>
2025-10-24 09:27:39 +08:00
{/* 面料数据查询结果 */}
{queryResults.length > 0 && (
<>
<Divider />
<Title heading={4}> ({queryResults.length} )</Title>
<Table
columns={columns}
dataSource={queryResults.map((item, index) => ({ ...item, key: index }))}
pagination={{ pageSize: 10 }}
style={{ marginTop: '10px' }}
/>
</>
)}
{/* 二次工艺查询结果 */}
{secondaryProcessResults.length > 0 && (
<>
<Divider />
<Title heading={4}> ({secondaryProcessResults.length} )</Title>
<Table
columns={secondaryProcessColumns}
dataSource={secondaryProcessResults.map((item, index) => ({ ...item, key: index }))}
pagination={{ pageSize: 10 }}
style={{ marginTop: '10px' }}
/>
</>
)}
{/* 工艺价格查询结果 */}
{pricingDetailsResults.length > 0 && (
<>
<Divider />
<Title heading={4}> ({pricingDetailsResults.length} )</Title>
<Table
columns={pricingDetailsColumns}
dataSource={pricingDetailsResults.map((item, index) => ({ ...item, key: index }))}
pagination={{ pageSize: 10 }}
style={{ marginTop: '10px' }}
/>
</>
)}
2025-10-22 14:08:03 +08:00
</>
)}
</div>
);
}