mairuiming 4df5e808a3 增加起始时间
增加起始时间
2025-10-24 14:42:28 +08:00

3270 lines
134 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<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>(new Date());
// 预览相关状态(已移除未使用的 previewLoading 状态)
// 时效计算相关状态
const [timelineVisible, setTimelineVisible] = useState(false);
const [timelineLoading, setTimelineLoading] = useState(false);
const [timelineResults, setTimelineResults] = useState<any[]>([]);
const [timelineAdjustments, setTimelineAdjustments] = useState<{[key: number]: number}>({});
// 快照回填来源foreign_id、款式、颜色
const [currentForeignId, setCurrentForeignId] = useState<string | null>(null);
const [currentStyleText, setCurrentStyleText] = useState<string>('');
const [currentColorText, setCurrentColorText] = 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>('');
// 指定的数据表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<string>();
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<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 = 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<string[]> => {
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 (
<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>
</div>
</Modal>
{mode === 'generate' && <Title heading={2}></Title>}
{/* 功能入口切换与调整入口 */}
{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: '调整流程日期' }]} />
{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;
}
if (tableId && tableId !== DELIVERY_RECORD_TABLE_ID) {
if (bitable.ui.showToast) {
await bitable.ui.showToast({ toastType: 'warning', message: '请在货期记录表中选择记录' });
}
return;
}
setSelectedDeliveryRecordId(recordId);
await loadProcessDataFromDeliveryRecord(recordId);
} 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({}); // 关闭时重置调整
}}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Button onClick={resetTimelineAdjustments}>
</Button>
<Button
type="primary"
onClick={async () => {
try {
// 写入流程数据表并获取记录ID
if (timelineResults.length > 0) {
const processRecordIds = await writeToProcessDataTable(timelineResults);
// 写入货期记录表
await writeToDeliveryRecordTable(timelineResults, processRecordIds, timelineAdjustments);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'success',
message: Object.keys(timelineAdjustments).length > 0
? '已保存调整后的时效数据到流程数据表和货期记录表'
: '已保存计算的时效数据到流程数据表和货期记录表'
});
}
}
} catch (error) {
console.error('保存数据时出错:', error);
if (bitable.ui.showToast) {
await bitable.ui.showToast({
toastType: 'error',
message: '保存数据失败,请重试'
});
}
}
setTimelineVisible(false);
}}
>
</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;
return (
<Card key={index} style={{ marginBottom: '8px', padding: '12px', position: 'relative' }}>
<div style={{ position: 'absolute', top: '12px', left: '12px', zIndex: 1 }}>
<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>
<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' }}>
<Button
size="small"
onClick={() => handleTimelineAdjustment(index, -0.5)}
disabled={adjustedValue <= 0}
style={{ minWidth: '28px', height: '24px', fontSize: '13px' }}
>
-0.5
</Button>
<Button
size="small"
onClick={() => handleTimelineAdjustment(index, -1)}
disabled={adjustedValue <= 0}
style={{ minWidth: '24px', height: '24px', fontSize: '13px' }}
>
-1
</Button>
<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' }}>
: {originalValue.toFixed(1)}
<br />
: {adjustment > 0 ? '+' : ''}{adjustment.toFixed(1)}
</div>
)}
</div>
<Button
size="small"
onClick={() => handleTimelineAdjustment(index, 1)}
style={{ minWidth: '24px', height: '24px', fontSize: '13px' }}
>
+1
</Button>
<Button
size="small"
onClick={() => handleTimelineAdjustment(index, 0.5)}
style={{ minWidth: '28px', height: '24px', fontSize: '13px' }}
>
+0.5
</Button>
</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.skippedHolidays || 0}\n${result.weekendDaysConfig && result.weekendDaysConfig.length > 0 ? `休息日配置:${result.weekendDaysConfig.map(day => ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][day]).join(', ')}` : '休息日配置:无固定休息日'}${result.calculationMethod === '内部' ? '\n工作时间9:00-18:00 (9小时制)' : ''}`}
>
?
</span>
</Text>
</div>
</div>
</Card>
);
})}
<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 />
<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>
{/* 标签选择部分,仅在生成模式显示 */}
{mode === 'generate' && labelOptions && Object.keys(labelOptions).length > 0 && (
<Card title="标签选择" style={{ marginBottom: '20px' }}>
<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
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>
{/* 客户期望日期选择 */}
<div style={{ marginTop: '20px' }}>
<Text strong style={{ display: 'block', marginBottom: '8px' }}></Text>
<DatePicker
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 && (
<div style={{ marginTop: '16px' }}>
<Space>
<Button
type='primary'
onClick={handleCalculateTimeline}
loading={timelineLoading}
disabled={timelineLoading}
style={{ backgroundColor: '#52c41a', borderColor: '#52c41a' }}
>
</Button>
<Text type="secondary">
{Object.keys(selectedLabels).length}
</Text>
</Space>
<div style={{ marginTop: '10px' }}>
<Text strong></Text>
<div style={{ marginTop: '5px' }}>
{Object.entries(selectedLabels).map(([key, value]) => {
const displayValue = Array.isArray(value) ? value.join(', ') : value;
return (
<Text
key={key}
code
style={{
marginRight: '8px',
marginBottom: '4px',
display: 'inline-block'
}}
>
{key}: {displayValue}
</Text>
);
})}
</div>
</div>
</div>
)}
</Card>
)}
{/* ... existing code ... */}
{mode === 'generate' && (
<main className="main" style={{ padding: '20px' }}>
<Card>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '20px' }}>
<div>
<Title heading={3} style={{ margin: 0 }}></Title>
{selectedRecords.length > 0 && (
<Text type='secondary' style={{ fontSize: '12px', marginTop: '4px' }}>
{selectedRecords.length}
</Text>
)}
</div>
<Space>
<Button
type='primary'
onClick={handleSelectRecords}
loading={loading}
disabled={loading}
>
{selectedRecords.length > 0 ? '重新选择' : '选择记录'}
</Button>
{selectedRecords.length > 0 && (
<Button
type='secondary'
onClick={handleClearRecords}
size='small'
>
</Button>
)}
</Space>
</div>
{/* 已选择记录的详细信息 */}
{selectedRecords.length > 0 && recordDetails.length > 0 && (
<div style={{
padding: '12px',
backgroundColor: '#f8f9fa',
borderRadius: '6px',
border: '1px solid #e9ecef',
marginBottom: '16px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div>
<Text strong>:</Text>
<Text code style={{ marginLeft: '8px', fontSize: '12px' }}>{recordDetails[0].displayValue || recordDetails[0].id}</Text>
</div>
{recordDetails.length > 1 && (
<div>
<Text type='secondary'>+ {recordDetails.length - 1} </Text>
</div>
)}
</div>
</div>
)}
{/* 加载状态 */}
{loading && (
<div style={{ textAlign: 'center', padding: '20px' }}>
<Spin size="large" />
<div style={{ marginTop: '10px' }}>
<Text>...</Text>
</div>
</div>
)}
{/* 空状态提示 */}
{selectedRecords.length === 0 && !loading && (
<div style={{
textAlign: 'center',
padding: '20px',
backgroundColor: '#fafafa',
borderRadius: '6px',
border: '1px dashed #d9d9d9'
}}>
<Text type="tertiary"></Text>
</div>
)}
</Card>
</main>
)}
{mode === 'generate' && (
<>
{/* 面料数据查询结果 */}
{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' }}
/>
</>
)}
</>
)}
</div>
);
}