import { bitable, FieldType } from '@lark-base-open/js-sdk'; import { Button, Typography, List, Card, Space, Divider, Spin, Table, Select, Modal, DatePicker } from '@douyinfe/semi-ui'; import { useState, useEffect } from 'react'; import { addDays, format } from 'date-fns'; import { zhCN } from 'date-fns/locale'; import { executePricingQuery, executeSecondaryProcessQuery, executePricingDetailsQuery } from './services/apiService'; 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日 } as const; // 统一的星期显示 const WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] as const; export default function App() { const [selectedRecords, setSelectedRecords] = useState([]); const [recordDetails, setRecordDetails] = useState([]); const [loading, setLoading] = useState(false); const [queryResults, setQueryResults] = useState([]); const [queryLoading, setQueryLoading] = useState(false); const [secondaryProcessResults, setSecondaryProcessResults] = useState([]); const [secondaryProcessLoading, setSecondaryProcessLoading] = useState(false); const [pricingDetailsResults, setPricingDetailsResults] = useState([]); 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(null); // 起始时间状态(默认当前时间,用于驱动所有节点时间) const [startTime, setStartTime] = useState(new Date()); // 预览相关状态(已移除未使用的 previewLoading 状态) // 时效计算相关状态 const [timelineVisible, setTimelineVisible] = useState(false); const [timelineLoading, setTimelineLoading] = useState(false); const [timelineResults, setTimelineResults] = useState([]); const [timelineAdjustments, setTimelineAdjustments] = useState<{[key: number]: number}>({}); // 快照回填来源(foreign_id、款式、颜色) const [currentForeignId, setCurrentForeignId] = useState(null); const [currentStyleText, setCurrentStyleText] = useState(''); const [currentColorText, setCurrentColorText] = useState(''); const [currentVersionNumber, setCurrentVersionNumber] = useState(null); // 功能入口模式与调整相关状态 const [mode, setMode] = useState<'generate' | 'adjust' | null>(null); const [modeSelectionVisible, setModeSelectionVisible] = useState(true); const [adjustLoading, setAdjustLoading] = useState(false); // 删除未使用的 deliveryRecords 状态 const [selectedDeliveryRecordId, setSelectedDeliveryRecordId] = useState(''); // 指定的数据表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 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 // 已移除:调整模式不再加载货期记录列表 // 入口选择处理 const chooseMode = (m: 'generate' | 'adjust') => { 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]; 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); } 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() !== '') { 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.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)); } // 回填起始时间(优先时间戳,其次字符串) if (snapshot.startTimestamp) { setStartTime(new Date(snapshot.startTimestamp)); } else if (snapshot.startString) { const parsed = new Date(snapshot.startString); if (!isNaN(parsed.getTime())) setStartTime(parsed); } if (Array.isArray(snapshot.timelineResults)) { setTimelineResults(snapshot.timelineResults); setTimelineVisible(true); if (bitable.ui.showToast) { await bitable.ui.showToast({ toastType: 'success', message: '已按快照一模一样还原流程数据' }); } setTimelineLoading(false); return; // 快照还原完成,退出函数 } } } catch (snapError) { console.warn('解析快照失败,降级为基于字段的还原:', snapError); } 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, skippedHolidays: 0, actualDays: undefined, startDateRule: undefined, dateAdjustmentRule: undefined, ruleDescription: undefined, adjustmentDescription: undefined }; }); 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'; // 时效字段(天) // 货期记录表相关常量 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_START_TIME_FIELD_ID = 'fld727qCAv'; // 中国法定节假日配置(需要手动维护或使用API) const CHINESE_HOLIDAYS = [ '2024-01-01', // 元旦 '2024-02-10', '2024-02-11', '2024-02-12', // 春节 '2024-04-04', '2024-04-05', '2024-04-06', // 清明节 '2024-05-01', '2024-05-02', '2024-05-03', // 劳动节 '2024-06-10', // 端午节 '2024-09-15', '2024-09-16', '2024-09-17', // 中秋节 '2024-10-01', '2024-10-02', '2024-10-03', // 国庆节 // ... 其他节假日 ]; // 已移除未使用的 fetchHolidays 函数 // 这个变量声明也不需要了 // 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; } // 移除所有星期信息(支持"星期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('日期解析失败:', error, { dateStr }); 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 calculateSkippedHolidays = (startDateStr: string | Date, endDateStr: string | Date): number => { try { const startDate = typeof startDateStr === 'string' ? new Date(startDateStr) : startDateStr; const endDate = typeof endDateStr === 'string' ? new Date(endDateStr) : endDateStr; if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { return 0; } if (typeof startDateStr === 'string' && (startDateStr.includes('未找到') || startDateStr.includes('时效值为0'))) { return 0; } if (typeof endDateStr === 'string' && (endDateStr.includes('未找到') || endDateStr.includes('时效值为0'))) { return 0; } let count = 0; let currentDate = new Date(startDate); while (currentDate <= endDate) { if (isChineseHoliday(currentDate)) { count++; } currentDate = addDays(currentDate, 1); } return count; } catch (error) { return 0; } }; // 判断是否为中国节假日 const isChineseHoliday = (date: Date, holidays: string[] = CHINESE_HOLIDAYS): boolean => { try { if (isNaN(date.getTime())) { return false; } const dateStr = format(date, 'yyyy-MM-dd'); return holidays.includes(dateStr); } catch (error) { return false; } }; // 判断是否为自定义周末 - 支持空的休息日配置 const isCustomWeekend = (date: Date, weekendDays: number[] = []): boolean => { if (weekendDays.length === 0) return false; // 如果没有配置休息日,则不认为是周末 return weekendDays.includes(date.getDay()); }; // 判断是否为工作日 - 只排除法定节假日和表格配置的休息日 const isBusinessDay = (date: Date, weekendDays: number[] = []): boolean => { return !isCustomWeekend(date, weekendDays) && !isChineseHoliday(date); }; // 日期调整函数 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): 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, [])) { result.setDate(result.getDate() + 1); } // 设置为9:00:00 result.setHours(9, 0, 0, 0); } } return result; }; // 内部工作时间计算函数 const addInternalBusinessTime = (startDate: Date, businessDays: number, weekendDays: number[] = []): Date => { const result = new Date(startDate); let remainingDays = businessDays; // 处理整数天 const wholeDays = Math.floor(remainingDays); let addedDays = 0; while (addedDays < wholeDays) { result.setDate(result.getDate() + 1); if (isBusinessDay(result, weekendDays)) { addedDays++; } } // 处理小数部分(按9小时工作制) const fractionalDays = remainingDays - wholeDays; if (fractionalDays > 0) { const workingHoursToAdd = fractionalDays * 9; // 内部按9小时工作制 let currentHour = result.getHours(); let currentMinute = result.getMinutes(); // 确保在工作时间内开始 if (currentHour < 9) { currentHour = 9; currentMinute = 0; } else if (currentHour >= 18) { // 跳到下一个工作日的9:00 result.setDate(result.getDate() + 1); while (!isBusinessDay(result, weekendDays)) { result.setDate(result.getDate() + 1); } currentHour = 9; currentMinute = 0; } // 添加工作小时 const totalMinutes = currentHour * 60 + currentMinute + workingHoursToAdd * 60; const finalHour = Math.floor(totalMinutes / 60); const finalMinute = totalMinutes % 60; // 如果超过18:00,需要跨到下一个工作日 if (finalHour >= 18) { const overflowHours = finalHour - 18; const overflowMinutes = finalMinute; // 跳到下一个工作日 result.setDate(result.getDate() + 1); while (!isBusinessDay(result, weekendDays)) { result.setDate(result.getDate() + 1); } result.setHours(9 + overflowHours, overflowMinutes, 0, 0); } else { result.setHours(finalHour, finalMinute, 0, 0); } } return result; }; // 添加工作日 - 使用表格配置的休息日 const addBusinessDaysWithHolidays = (startDate: Date, businessDays: number, weekendDays: number[] = []): Date => { const result = new Date(startDate); let addedDays = 0; // 处理小数天数:先添加整数天,再处理小数部分 const wholeDays = Math.floor(businessDays); const fractionalDays = businessDays - wholeDays; // 添加整数工作日 while (addedDays < wholeDays) { result.setDate(result.getDate() + 1); if (isBusinessDay(result, weekendDays)) { addedDays++; } } // 处理小数部分(转换为小时,按24小时制) if (fractionalDays > 0) { const hoursToAdd = fractionalDays * 24; // 1天=24小时 result.setHours(result.getHours() + hoursToAdd); } 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(); }; initializeData(); }, []); // 处理标签选择变化 const handleLabelChange = (labelKey: string, value: string | string[]) => { setSelectedLabels(prev => ({ ...prev, [labelKey]: value })); }; // 清空标签选择 const handleClearLabels = () => { setSelectedLabels({}); setExpectedDate(null); // 清空客户期望日期 }; // 计算预计开始和完成时间 const handleCalculateTimeline = async () => { // 检查是否选择了多条记录 if (selectedRecords.length > 1) { if (bitable.ui.showToast) { await bitable.ui.showToast({ toastType: 'warning', message: '计算时效功能仅支持单条记录,请重新选择单条记录后再试' }); } return; } // 检查是否选择了记录 if (selectedRecords.length === 0) { if (bitable.ui.showToast) { await bitable.ui.showToast({ toastType: 'warning', message: '请先选择一条记录' }); } return; } // 检查是否选择了标签 const hasSelectedLabels = Object.values(selectedLabels).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 (!expectedDate) { if (bitable.ui.showToast) { await bitable.ui.showToast({ toastType: 'info', message: '建议选择客户期望日期以便更好地进行时效计算' }); } } // 移除冗余日志:客户期望日期输出 setTimelineLoading(true); try { // 构建业务选择的所有标签值集合(用于快速查找) const businessLabelValues = new Set(); for (const [labelKey, selectedValue] of Object.entries(selectedLabels)) { 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(selectedLabels)) { 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); } // 如果表格中没有配置休息日或配置为空,则该节点没有固定休息日 // 这样就完全依赖表格数据,不会有任何硬编码的默认值 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, // 添加休息日配置 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(); 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 = startTime ? new Date(startTime) : 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); let endDate: Date; if (calculationMethod === '内部') { // 使用内部工作时间计算 endDate = addInternalBusinessTime(adjustedStartDate, timelineValue, processNode.weekendDays); } else { // 使用原有的24小时制计算 endDate = addBusinessDaysWithHolidays(adjustedStartDate, timelineValue, processNode.weekendDays); } 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); if (nodeCalculationMethod === '内部') { nodeEndTime = addInternalBusinessTime(adjustedStartTime, timelineValue, processNode.weekendDays); } else { nodeEndTime = addBusinessDaysWithHolidays(adjustedStartTime, timelineValue, processNode.weekendDays); } } else { nodeEndTime = new Date(nodeStartTime); } // 计算跳过的天数 const skippedWeekends = calculateSkippedWeekends(nodeStartTime, nodeEndTime, processNode.weekendDays); const skippedHolidays = calculateSkippedHolidays(timelineResult.startDate, timelineResult.endDate); const actualDays = calculateActualDays(timelineResult.startDate, timelineResult.endDate); 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, // 新增:保存休息日配置用于显示 calculationMethod: nodeCalculationMethod, // 新增:保存计算方式 ruleDescription: ruleDescription, // 新增:保存规则描述 skippedWeekends: skippedWeekends, skippedHolidays: skippedHolidays, 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); setTimelineVisible(true); console.log('按流程顺序计算的时效结果:', results); } catch (error) { console.error('计算时效失败:', error); if (bitable.ui.showToast) { await bitable.ui.showToast({ toastType: 'error', message: '计算时效失败,请检查表格配置' }); } } finally { setTimelineLoading(false); } }; // 调整时效值的函数 const handleTimelineAdjustment = (nodeIndex: number, adjustment: number) => { const newAdjustments = { ...timelineAdjustments }; const currentAdjustment = newAdjustments[nodeIndex] || 0; const newAdjustment = currentAdjustment + adjustment; // 防止调整后的时效值小于0(优先使用原始时效值,其次使用上次重算后的值) const baseValue = (typeof timelineResults[nodeIndex]?.timelineValue === 'number') ? timelineResults[nodeIndex]!.timelineValue : (typeof timelineResults[nodeIndex]?.adjustedTimelineValue === 'number') ? timelineResults[nodeIndex]!.adjustedTimelineValue : 0; if (baseValue + newAdjustment < 0) { return; } newAdjustments[nodeIndex] = newAdjustment; setTimelineAdjustments(newAdjustments); // 重新计算所有节点的时间 recalculateTimeline(newAdjustments); }; // 重新计算时间线的函数 const recalculateTimeline = (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); // 重新应用起始日期调整规则 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; if (adjustedTimelineValue > 0) { const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod); if (nodeCalculationMethod === '内部') { nodeEndTime = addInternalBusinessTime(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays); } else { nodeEndTime = addBusinessDaysWithHolidays(adjustedStartTime, adjustedTimelineValue, nodeWeekendDays); } } else { nodeEndTime = new Date(nodeStartTime); } // 计算跳过的天数 const adjustedStartTime = adjustedTimelineValue > 0 ? adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod) : nodeStartTime; const skippedWeekends = calculateSkippedWeekends(adjustedStartTime, nodeEndTime, nodeWeekendDays); const estimatedStartStr = formatDate(adjustedStartTime); const estimatedEndStr = adjustedTimelineValue > 0 ? formatDate(nodeEndTime) : '时效值为0'; const skippedHolidays = calculateSkippedHolidays(estimatedStartStr, estimatedEndStr); const actualDays = calculateActualDays(estimatedStartStr, estimatedEndStr); // 更新结果 updatedResults[i] = { ...result, adjustedTimelineValue: adjustedTimelineValue, estimatedStart: estimatedStartStr, estimatedEnd: estimatedEndStr, adjustment: adjustment, calculationMethod: nodeCalculationMethod, // 保持计算方式 skippedWeekends: skippedWeekends, skippedHolidays: skippedHolidays, actualDays: actualDays, adjustmentDescription: result.adjustmentDescription, // 保持调整规则描述 ruleDescription: ruleDescription // 添加更新后的规则描述 }; // 更新累积时间:当前节点的完成时间成为下一个节点的开始时间 if (adjustedTimelineValue > 0) { cumulativeStartTime = new Date(nodeEndTime); } } setTimelineResults(updatedResults); }; // 当起始时间变更时,重新以最新起始时间为基准重算全流程 useEffect(() => { if (timelineResults.length > 0) { recalculateTimeline(timelineAdjustments); } }, [startTime]); // 重置调整的函数 const resetTimelineAdjustments = () => { setTimelineAdjustments({}); recalculateTimeline({}); }; // 已移除未使用的 getTimelineLabelFieldId 辅助函数 // 写入货期记录表的函数 const writeToDeliveryRecordTable = async (timelineResults: any[], processRecordIds: string[], timelineAdjustments: {[key: number]: number} = {}) => { try { // 写入货期记录表 // 获取货期记录表 const deliveryRecordTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID); // 检查字段是否存在 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 // 新增:起始时间字段 ]; // 获取各个字段 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 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); // 获取foreign_id:优先使用选择记录,其次记录详情,最后快照回填 let foreignId = ''; if (selectedRecords.length > 0) { const table = await bitable.base.getTable(TABLE_ID); const firstRecord = await table.getRecordById(selectedRecords[0]); const fieldValue = firstRecord.fields['fldpvBfeC0']; if (Array.isArray(fieldValue) && fieldValue.length > 0) { const firstItem = fieldValue[0]; if (firstItem && firstItem.text) { foreignId = firstItem.text; } else if (typeof firstItem === 'string') { foreignId = firstItem; } } else if (typeof fieldValue === 'string') { foreignId = fieldValue; } else if (fieldValue && fieldValue.text) { foreignId = fieldValue.text; } } // 回退:记录详情 if (!foreignId && recordDetails.length > 0) { const first = recordDetails[0]; const val = first.fields['fldpvBfeC0']; if (Array.isArray(val) && val.length > 0) { const item = val[0]; foreignId = typeof item === 'string' ? item : (item?.text || item?.name || ''); } else if (typeof val === 'string') { foreignId = val; } else if (val && typeof val === 'object') { foreignId = val.text || val.name || ''; } } // 回退:快照状态 if (!foreignId && currentForeignId) { foreignId = currentForeignId; } // 获取款式与颜色:优先使用记录详情或快照回填,避免重复请求 let style = ''; let color = ''; 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 ''; }; if (recordDetails.length > 0) { const first = recordDetails[0]; style = extractText(first.fields['fld6Uw95kt']) || currentStyleText || ''; color = extractText(first.fields['flde85ni4O']) || currentColorText || ''; } else { // 回退:使用快照回填的状态 style = currentStyleText || ''; color = currentColorText || ''; // 若仍为空且有选择记录,仅做一次读取 if ((!style || !color) && selectedRecords.length > 0) { const table = await bitable.base.getTable(TABLE_ID); const firstRecord = await table.getRecordById(selectedRecords[0]); style = style || extractText(firstRecord.fields['fld6Uw95kt']); color = color || extractText(firstRecord.fields['flde85ni4O']); } } // 获取标签汇总(从业务选择的标签中获取) const selectedLabelValues = Object.values(selectedLabels).flat().filter(Boolean); // 获取预计交付日期(最后节点的预计完成时间) let expectedDeliveryDate = null; if (timelineResults.length > 0) { const lastNode = timelineResults[timelineResults.length - 1]; if (lastNode.estimatedEnd && !lastNode.estimatedEnd.includes('未找到')) { try { expectedDeliveryDate = new Date(lastNode.estimatedEnd).getTime(); } catch (error) { console.error('转换预计交付日期失败:', error); } } } // 获取客户期望日期 let customerExpectedDate = null; 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之前进行数据校验(移除冗余日志) // 使用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 createTimeCell = await createTimeField.createCell(currentTime); const startTimestamp = startTime ? startTime.getTime() : currentTime; const startTimeCell = await startTimeField.createCell(startTimestamp); 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); // 组合所有Cell到一个记录中 const recordCells = [foreignIdCell, styleCell, colorCell, createTimeCell, startTimeCell, versionCell]; // 只有当数据存在时才添加对应的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, recordCells: recordCells.length }); if (bitable.ui.showToast) { await bitable.ui.showToast({ toastType: 'error', message: '写入货期记录表失败: ' + (error as Error).message }); } throw error; } }; // 写入流程数据表的函数 const writeToProcessDataTable = async (timelineResults: any[]): Promise => { try { console.log('开始写入流程数据表...'); console.log('timelineResults:', timelineResults); // 获取流程数据表和流程配置表 const processDataTable = await bitable.base.getTable(PROCESS_DATA_TABLE_ID); const processConfigTable = await bitable.base.getTable(PROCESS_CONFIG_TABLE_ID); // 获取所有需要的字段 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 snapshotField = await processDataTable.getField(PROCESS_SNAPSHOT_JSON_FIELD_ID); const versionField = await processDataTable.getField(PROCESS_VERSION_FIELD_ID); const timelinessField = await processDataTable.getField(PROCESS_TIMELINESS_FIELD_ID); // 获取foreign_id - 修改这部分逻辑 let foreignId = null; console.log('selectedRecords:', selectedRecords); console.log('recordDetails:', recordDetails); if (selectedRecords.length > 0 && recordDetails.length > 0) { // 从第一个选择的记录的详情中获取fldpvBfeC0字段的值 const firstRecord = recordDetails[0]; if (firstRecord && firstRecord.fields && firstRecord.fields['fldpvBfeC0']) { const fieldValue = firstRecord.fields['fldpvBfeC0']; // 处理数组格式的字段值 if (Array.isArray(fieldValue) && fieldValue.length > 0) { const firstItem = fieldValue[0]; if (firstItem && firstItem.text) { foreignId = firstItem.text; } else if (typeof firstItem === 'string') { foreignId = firstItem; } } else if (typeof fieldValue === 'string') { foreignId = fieldValue; } else if (fieldValue && fieldValue.text) { foreignId = fieldValue.text; } 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 []; } // 先删除该foreign_id的所有现有记录 try { const existingRecords = await processDataTable.getRecords({ pageSize: 5000, filter: { conjunction: 'And', conditions: [{ fieldId: FOREIGN_ID_FIELD_ID, operator: 'is', value: [foreignId] }] } }); if (existingRecords.records && existingRecords.records.length > 0) { const recordIdsToDelete = existingRecords.records.map(record => record.id); await processDataTable.deleteRecords(recordIdsToDelete); console.log(`已删除 ${recordIdsToDelete.length} 条现有记录`); if (bitable.ui.showToast) { await bitable.ui.showToast({ toastType: 'info', message: `已删除 ${recordIdsToDelete.length} 条现有流程数据` }); } } } catch (deleteError) { console.warn('删除现有记录时出错:', 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); } const expectedDateTimestamp = expectedDate ? expectedDate.getTime() : null; const expectedDateString = expectedDate ? format(expectedDate, DATE_FORMATS.STORAGE_FORMAT) : null; // 从记录详情提取款式和颜色,用于快照存档(回填写入来源) let styleText = ''; let colorText = ''; if (recordDetails.length > 0) { const firstRecord = recordDetails[0]; const styleFieldValue = firstRecord.fields['fld6Uw95kt']; if (Array.isArray(styleFieldValue) && styleFieldValue.length > 0) { const firstItem = styleFieldValue[0]; styleText = typeof firstItem === 'string' ? firstItem : (firstItem?.text || firstItem?.name || ''); } else if (typeof styleFieldValue === 'string') { styleText = styleFieldValue; } else if (styleFieldValue && typeof styleFieldValue === 'object') { styleText = (styleFieldValue.text || styleFieldValue.name || ''); } const colorFieldValue = firstRecord.fields['flde85ni4O']; if (Array.isArray(colorFieldValue) && colorFieldValue.length > 0) { const firstItemC = colorFieldValue[0]; colorText = typeof firstItemC === 'string' ? firstItemC : (firstItemC?.text || firstItemC?.name || ''); } else if (typeof colorFieldValue === 'string') { colorText = colorFieldValue; } else if (colorFieldValue && typeof colorFieldValue === 'object') { colorText = (colorFieldValue.text || colorFieldValue.name || ''); } } // 快照回填的备用值 if (!styleText && currentStyleText) styleText = currentStyleText; if (!colorText && currentColorText) colorText = currentColorText; const pageSnapshot = { version: versionNumber, foreignId, styleText, colorText, mode, selectedLabels, expectedDateTimestamp, expectedDateString, startTimestamp: startTime ? startTime.getTime() : undefined, startString: startTime ? formatDate(startTime, 'STORAGE_FORMAT') : undefined, timelineAdjustments, timelineResults }; const snapshotJson = JSON.stringify(pageSnapshot); // 使用createCell方法准备要写入的记录数据 const recordCellsToAdd = []; for (const result of timelineResults) { 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; // 组合所有Cell到一个记录中 const snapshotCell = await snapshotField.createCell(snapshotJson); const versionCell = await versionField.createCell(versionNumber); const timelinessCell = (typeof result.timelineValue === 'number') ? await timelinessField.createCell(result.timelineValue) : null; const recordCells = [ foreignIdCell, processNameCell, processOrderCell, snapshotCell, 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 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); 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 () => { 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 (
{/* 入口选择弹窗 */} setModeSelectionVisible(false)} maskClosable={false} >
chooseMode('generate')} > 生成流程日期 基于业务数据计算并生成节点时间线
chooseMode('adjust')} > 调整流程日期 读取货期记录,精确还原时间线
{mode === 'generate' && 数据查询工具} {/* 功能入口切换与调整入口 */} {mode !== null && (
handleLabelChange(labelKey, value)} multiple={isMultiSelect} filter showSearch > {options.map((option, optionIndex) => ( {option.label} ))}
); })}
{/* 客户期望日期选择 */}
客户期望日期 setExpectedDate(date)} format="yyyy-MM-dd" disabledDate={(date) => { // 禁用今天之前的日期 const today = new Date(); today.setHours(0, 0, 0, 0); return date < today; }} /> {expectedDate && ( 已选择:{formatDate(expectedDate, 'CHINESE_DATE')} )}
{Object.keys(selectedLabels).length > 0 && (
已选择 {Object.keys(selectedLabels).length} 个标签
当前选择的标签:
{Object.entries(selectedLabels).map(([key, value]) => { const displayValue = Array.isArray(value) ? value.join(', ') : value; return ( {key}: {displayValue} ); })}
)} )} {/* ... existing code ... */} {mode === 'generate' && (
版单数据 {selectedRecords.length > 0 && ( 已选择 {selectedRecords.length} 条记录 )}
{selectedRecords.length > 0 && ( )}
{/* 已选择记录的详细信息 */} {selectedRecords.length > 0 && recordDetails.length > 0 && (
主记录: {recordDetails[0].displayValue || recordDetails[0].id}
{recordDetails.length > 1 && (
+ 其他 {recordDetails.length - 1} 条
)}
)} {/* 加载状态 */} {loading && (
正在加载版单数据...
)} {/* 空状态提示 */} {selectedRecords.length === 0 && !loading && (
请选择版单记录开始操作
)}
)} {mode === 'generate' && ( <> {/* 面料数据查询结果 */} {queryResults.length > 0 && ( <> 面料数据查询结果 ({queryResults.length} 条) ({ ...item, key: index }))} pagination={{ pageSize: 10 }} style={{ marginTop: '10px' }} /> )} {/* 二次工艺查询结果 */} {secondaryProcessResults.length > 0 && ( <> 二次工艺查询结果 ({secondaryProcessResults.length} 条)
({ ...item, key: index }))} pagination={{ pageSize: 10 }} style={{ marginTop: '10px' }} /> )} {/* 工艺价格查询结果 */} {pricingDetailsResults.length > 0 && ( <> 工艺价格查询结果 ({pricingDetailsResults.length} 条)
({ ...item, key: index }))} pagination={{ pageSize: 10 }} style={{ marginTop: '10px' }} /> )} )} ); }