2025-10-22 14:08:03 +08:00
|
|
|
|
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';
|
2025-10-24 09:27:39 +08:00
|
|
|
|
import { addDays, format } from 'date-fns';
|
2025-10-22 14:08:03 +08:00
|
|
|
|
import { zhCN } from 'date-fns/locale';
|
2025-10-22 16:19:28 +08:00
|
|
|
|
import { executePricingQuery, executeSecondaryProcessQuery, executePricingDetailsQuery } from './services/apiService';
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
const { Title, Text } = Typography;
|
|
|
|
|
|
|
|
|
|
|
|
// 统一的日期格式常量
|
|
|
|
|
|
const DATE_FORMATS = {
|
|
|
|
|
|
DISPLAY_WITH_TIME: 'yyyy-MM-dd HH:mm', // 显示格式:2026-02-20 16:54
|
|
|
|
|
|
DISPLAY_DATE_ONLY: 'yyyy-MM-dd', // 日期格式:2026-02-20
|
|
|
|
|
|
STORAGE_FORMAT: 'yyyy-MM-dd HH:mm:ss', // 存储格式:2026-02-20 16:54:00
|
|
|
|
|
|
CHINESE_DATE: 'yyyy年MM月dd日', // 中文格式:2026年02月20日
|
|
|
|
|
|
} 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);
|
|
|
|
|
|
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 预览相关状态(已移除未使用的 previewLoading 状态)
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 时效计算相关状态
|
|
|
|
|
|
const [timelineVisible, setTimelineVisible] = useState(false);
|
|
|
|
|
|
const [timelineLoading, setTimelineLoading] = useState(false);
|
|
|
|
|
|
const [timelineResults, setTimelineResults] = useState<any[]>([]);
|
|
|
|
|
|
const [timelineAdjustments, setTimelineAdjustments] = useState<{[key: number]: number}>({});
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 快照回填来源(foreign_id、款式、颜色)
|
|
|
|
|
|
const [currentForeignId, setCurrentForeignId] = useState<string | null>(null);
|
|
|
|
|
|
const [currentStyleText, setCurrentStyleText] = useState<string>('');
|
|
|
|
|
|
const [currentColorText, setCurrentColorText] = useState<string>('');
|
|
|
|
|
|
// 功能入口模式与调整相关状态
|
|
|
|
|
|
const [mode, setMode] = useState<'generate' | 'adjust' | null>(null);
|
|
|
|
|
|
const [modeSelectionVisible, setModeSelectionVisible] = useState(true);
|
|
|
|
|
|
const [adjustLoading, setAdjustLoading] = useState(false);
|
|
|
|
|
|
// 删除未使用的 deliveryRecords 状态
|
|
|
|
|
|
const [selectedDeliveryRecordId, setSelectedDeliveryRecordId] = useState<string>('');
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 指定的数据表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
|
|
|
|
|
|
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 已移除:调整模式不再加载货期记录列表
|
|
|
|
|
|
|
|
|
|
|
|
// 入口选择处理
|
|
|
|
|
|
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 (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.timelineAdjustments) setTimelineAdjustments(snapshot.timelineAdjustments);
|
|
|
|
|
|
if (snapshot.expectedDateTimestamp) {
|
|
|
|
|
|
setExpectedDate(new Date(snapshot.expectedDateTimestamp));
|
|
|
|
|
|
} else if (snapshot.expectedDateString) {
|
|
|
|
|
|
setExpectedDate(new Date(snapshot.expectedDateString));
|
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-22 14:08:03 +08:00
|
|
|
|
// 流程数据表相关常量
|
|
|
|
|
|
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'; // 预计完成日期字段
|
2025-10-24 09:27:39 +08:00
|
|
|
|
const PROCESS_SNAPSHOT_JSON_FIELD_ID = 'fldSHTxfnC'; // 文本2:用于保存计算页面快照(JSON)
|
|
|
|
|
|
const PROCESS_VERSION_FIELD_ID = 'fldwk5X7Yw'; // 版本字段
|
|
|
|
|
|
const PROCESS_TIMELINESS_FIELD_ID = 'fldEYCXnWt'; // 时效字段(天)
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 货期记录表相关常量
|
|
|
|
|
|
const DELIVERY_RECORD_TABLE_ID = 'tblwiA49gksQrnfg'; // 货期记录表ID
|
|
|
|
|
|
const DELIVERY_FOREIGN_ID_FIELD_ID = 'fld0gAIcHS'; // foreign_id字段(需要替换为实际字段ID)
|
|
|
|
|
|
const DELIVERY_LABELS_FIELD_ID = 'fldp0cDP2T'; // 标签汇总字段(需要替换为实际字段ID)
|
|
|
|
|
|
const DELIVERY_STYLE_FIELD_ID = 'fldJRFxwB1'; // 款式字段(需要替换为实际字段ID)
|
|
|
|
|
|
const DELIVERY_COLOR_FIELD_ID = 'fldhA1uBMy'; // 颜色字段(需要替换为实际字段ID)
|
|
|
|
|
|
const DELIVERY_CREATE_TIME_FIELD_ID = 'fldP4w79LQ'; // 生成时间字段(需要替换为实际字段ID)
|
|
|
|
|
|
const DELIVERY_EXPECTED_DATE_FIELD_ID = 'fldrjlzsxn'; // 预计交付日期字段(需要替换为实际字段ID)
|
|
|
|
|
|
const DELIVERY_NODE_DETAILS_FIELD_ID = 'fldu1KL9yC'; // 节点详情字段(需要替换为实际字段ID)
|
|
|
|
|
|
const DELIVERY_CUSTOMER_EXPECTED_DATE_FIELD_ID = 'fldYNluU8D'; // 客户期望日期字段
|
|
|
|
|
|
const DELIVERY_ADJUSTMENT_INFO_FIELD_ID = 'fldNc6nNsz'; // 货期调整信息字段(需要替换为实际字段ID)
|
2025-10-24 09:27:39 +08:00
|
|
|
|
const DELIVERY_VERSION_FIELD_ID = 'fld5OmvZrn'; // 版本字段(新增)
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 中国法定节假日配置(需要手动维护或使用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', // 国庆节
|
|
|
|
|
|
// ... 其他节假日
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 已移除未使用的 fetchHolidays 函数
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 这个变量声明也不需要了
|
|
|
|
|
|
// const nodeNameToOptionId = new Map();
|
|
|
|
|
|
|
|
|
|
|
|
// 统一的日期格式化函数
|
|
|
|
|
|
const formatDate = (date: Date | string, formatType: keyof typeof DATE_FORMATS = 'DISPLAY_WITH_TIME'): string => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!date) return '';
|
|
|
|
|
|
|
|
|
|
|
|
const dateObj = typeof date === 'string' ? parseDate(date) : date;
|
|
|
|
|
|
if (!dateObj || isNaN(dateObj.getTime())) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return format(dateObj, DATE_FORMATS[formatType], { locale: zhCN });
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('日期格式化失败:', error, { date, formatType });
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 统一的日期解析函数
|
|
|
|
|
|
const parseDate = (dateStr: string): Date | null => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!dateStr || dateStr.includes('未找到') || dateStr.includes('时效值为0')) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 移除所有星期信息(支持"星期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: '建议选择客户期望日期以便更好地进行时效计算'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 移除冗余日志:客户期望日期输出
|
2025-10-22 14:08:03 +08:00
|
|
|
|
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());
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 已移除冗余日志:业务选择标签值
|
2025-10-22 14:08:03 +08:00
|
|
|
|
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 = 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 originalValue = timelineResults[nodeIndex]?.timelineValue || 0;
|
|
|
|
|
|
if (originalValue + newAdjustment < 0) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
newAdjustments[nodeIndex] = newAdjustment;
|
|
|
|
|
|
setTimelineAdjustments(newAdjustments);
|
|
|
|
|
|
|
|
|
|
|
|
// 重新计算所有节点的时间
|
|
|
|
|
|
recalculateTimeline(newAdjustments);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 重新计算时间线的函数
|
|
|
|
|
|
const recalculateTimeline = (adjustments: {[key: number]: number}) => {
|
|
|
|
|
|
const updatedResults = [...timelineResults];
|
|
|
|
|
|
let cumulativeStartTime = new Date(); // 从当前时间开始
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < updatedResults.length; i++) {
|
|
|
|
|
|
const result = updatedResults[i];
|
|
|
|
|
|
const originalTimelineValue = result.timelineValue || 0;
|
|
|
|
|
|
const adjustment = adjustments[i] || 0;
|
|
|
|
|
|
const adjustedTimelineValue = originalTimelineValue + 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);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 重置调整的函数
|
|
|
|
|
|
const resetTimelineAdjustments = () => {
|
|
|
|
|
|
setTimelineAdjustments({});
|
|
|
|
|
|
recalculateTimeline({});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 已移除未使用的 getTimelineLabelFieldId 辅助函数
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 写入货期记录表的函数
|
|
|
|
|
|
const writeToDeliveryRecordTable = async (timelineResults: any[], processRecordIds: string[], timelineAdjustments: {[key: number]: number} = {}) => {
|
|
|
|
|
|
try {
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 写入货期记录表
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取货期记录表
|
|
|
|
|
|
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 // 添加货期调整信息字段
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 获取各个字段
|
|
|
|
|
|
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);
|
2025-10-24 09:27:39 +08:00
|
|
|
|
const versionField = await deliveryRecordTable.getField(DELIVERY_VERSION_FIELD_ID);
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 获取foreign_id:优先使用选择记录,其次记录详情,最后快照回填
|
2025-10-22 14:08:03 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 回退:记录详情
|
|
|
|
|
|
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 || '';
|
2025-10-22 14:08:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 回退:快照状态
|
|
|
|
|
|
if (!foreignId && currentForeignId) {
|
|
|
|
|
|
foreignId = currentForeignId;
|
|
|
|
|
|
}
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 获取款式与颜色:优先使用记录详情或快照回填,避免重复请求
|
|
|
|
|
|
let style = '';
|
2025-10-22 14:08:03 +08:00
|
|
|
|
let color = '';
|
2025-10-24 09:27:39 +08:00
|
|
|
|
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']);
|
2025-10-22 14:08:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取标签汇总(从业务选择的标签中获取)
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 计算版本号(数字)并格式化货期调整信息
|
|
|
|
|
|
let versionNumber = 1;
|
|
|
|
|
|
try {
|
|
|
|
|
|
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}`;
|
2025-10-22 14:08:03 +08:00
|
|
|
|
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)} 天`;
|
|
|
|
|
|
});
|
2025-10-24 09:27:39 +08:00
|
|
|
|
adjustmentInfo += `\n当前调整:\n${adjustmentTexts.join('\n')}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 在创建Cell之前进行数据校验(移除冗余日志)
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 使用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 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;
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 创建版本号Cell(数字)
|
|
|
|
|
|
const versionCell = await versionField.createCell(versionNumber);
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 组合所有Cell到一个记录中
|
2025-10-24 09:27:39 +08:00
|
|
|
|
const recordCells = [foreignIdCell, styleCell, colorCell, createTimeCell, versionCell];
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 只有当数据存在时才添加对应的Cell
|
|
|
|
|
|
if (labelsCell) recordCells.push(labelsCell);
|
|
|
|
|
|
if (expectedDateCell) recordCells.push(expectedDateCell);
|
|
|
|
|
|
if (customerExpectedDateCell) recordCells.push(customerExpectedDateCell);
|
|
|
|
|
|
if (nodeDetailsCell) recordCells.push(nodeDetailsCell);
|
|
|
|
|
|
if (adjustmentInfoCell) recordCells.push(adjustmentInfoCell);
|
|
|
|
|
|
|
|
|
|
|
|
// 添加记录到货期记录表
|
|
|
|
|
|
const addedRecord = await deliveryRecordTable.addRecord(recordCells);
|
|
|
|
|
|
|
|
|
|
|
|
return addedRecord;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('写入货期记录表详细错误:', {
|
|
|
|
|
|
error: error,
|
|
|
|
|
|
message: error.message,
|
|
|
|
|
|
stack: error.stack,
|
|
|
|
|
|
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);
|
2025-10-24 09:27:39 +08:00
|
|
|
|
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);
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 快照回填:在调整模式通过快照还原时使用当前foreign_id状态
|
|
|
|
|
|
if (!foreignId && currentForeignId) {
|
|
|
|
|
|
foreignId = currentForeignId;
|
|
|
|
|
|
console.log('使用快照恢复的foreign_id:', foreignId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-22 14:08:03 +08:00
|
|
|
|
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);
|
|
|
|
|
|
// 继续执行,不中断流程
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-24 09:27:39 +08:00
|
|
|
|
// 构建页面快照JSON(确保可一模一样还原)
|
|
|
|
|
|
// 计算版本号:数字。默认按 foreign_id 在货期记录表的记录数量 + 1
|
|
|
|
|
|
let versionNumber = 1;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const deliveryRecordTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID);
|
|
|
|
|
|
if (mode === 'adjust' && selectedDeliveryRecordId) {
|
|
|
|
|
|
// 调整模式:基于选中货期记录版本 +1
|
|
|
|
|
|
const srcRecord = await deliveryRecordTable.getRecordById(selectedDeliveryRecordId);
|
|
|
|
|
|
const vVal = srcRecord?.fields?.[DELIVERY_VERSION_FIELD_ID];
|
|
|
|
|
|
let baseVersion = 0;
|
|
|
|
|
|
if (typeof vVal === 'number') baseVersion = vVal;
|
|
|
|
|
|
else if (typeof vVal === 'string') baseVersion = parseInt(vVal) || 0;
|
|
|
|
|
|
else if (vVal && typeof vVal === 'object' && vVal.value !== undefined) baseVersion = vVal.value || 0;
|
|
|
|
|
|
versionNumber = baseVersion + 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,
|
|
|
|
|
|
timelineAdjustments,
|
|
|
|
|
|
timelineResults
|
|
|
|
|
|
};
|
|
|
|
|
|
const snapshotJson = JSON.stringify(pageSnapshot);
|
|
|
|
|
|
|
2025-10-22 14:08:03 +08:00
|
|
|
|
// 使用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到一个记录中
|
2025-10-24 09:27:39 +08:00
|
|
|
|
const snapshotCell = await snapshotField.createCell(snapshotJson);
|
|
|
|
|
|
const versionCell = await versionField.createCell(versionNumber);
|
|
|
|
|
|
const timelinessCell = (typeof result.timelineValue === 'number')
|
|
|
|
|
|
? await timelinessField.createCell(result.timelineValue)
|
|
|
|
|
|
: null;
|
2025-10-22 14:08:03 +08:00
|
|
|
|
const recordCells = [
|
|
|
|
|
|
foreignIdCell,
|
|
|
|
|
|
processNameCell,
|
2025-10-24 09:27:39 +08:00
|
|
|
|
processOrderCell,
|
|
|
|
|
|
snapshotCell,
|
|
|
|
|
|
versionCell
|
2025-10-22 14:08:03 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 只有当时间戳存在时才添加日期Cell
|
|
|
|
|
|
if (startDateCell) recordCells.push(startDateCell);
|
|
|
|
|
|
if (endDateCell) recordCells.push(endDateCell);
|
2025-10-24 09:27:39 +08:00
|
|
|
|
if (timelinessCell) recordCells.push(timelinessCell);
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
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 {
|
2025-10-22 16:19:28 +08:00
|
|
|
|
// 使用 apiService 中的函数
|
|
|
|
|
|
const data = await executePricingQuery(packId, packType, selectedLabels);
|
|
|
|
|
|
setQueryResults(data);
|
|
|
|
|
|
|
|
|
|
|
|
// 处理品类属性填充到标签8的逻辑保持不变
|
|
|
|
|
|
if (data && data.length > 0) {
|
|
|
|
|
|
const label8Options = labelOptions['标签8'] || [];
|
|
|
|
|
|
const matchingCategoryValues: string[] = [];
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
2025-10-22 16:19:28 +08:00
|
|
|
|
data.forEach((record: any) => {
|
|
|
|
|
|
if (record.品类属性 && record.品类属性.trim() !== '') {
|
|
|
|
|
|
const categoryValue = record.品类属性.trim();
|
|
|
|
|
|
const matchingOption = label8Options.find(option =>
|
|
|
|
|
|
option.value === categoryValue || option.label === categoryValue
|
|
|
|
|
|
);
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
2025-10-22 16:19:28 +08:00
|
|
|
|
if (matchingOption && !matchingCategoryValues.includes(categoryValue)) {
|
|
|
|
|
|
matchingCategoryValues.push(categoryValue);
|
2025-10-22 14:08:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-22 16:19:28 +08:00
|
|
|
|
});
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
2025-10-22 16:19:28 +08:00
|
|
|
|
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(', ')}`
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-10-22 14:08:03 +08:00
|
|
|
|
}
|
2025-10-22 16:19:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (bitable.ui.showToast) {
|
|
|
|
|
|
await bitable.ui.showToast({
|
|
|
|
|
|
toastType: 'success',
|
|
|
|
|
|
message: '查询成功'
|
|
|
|
|
|
});
|
2025-10-22 14:08:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('数据库查询出错:', error);
|
|
|
|
|
|
if (bitable.ui.showToast) {
|
|
|
|
|
|
await bitable.ui.showToast({
|
|
|
|
|
|
toastType: 'error',
|
|
|
|
|
|
message: `数据库查询失败: ${error.message}`
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setQueryLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 执行二次工艺查询
|
2025-10-22 16:19:28 +08:00
|
|
|
|
const executeSecondaryProcessQueryLocal = async (packId: string, packType: string) => {
|
2025-10-22 14:08:03 +08:00
|
|
|
|
setSecondaryProcessLoading(true);
|
|
|
|
|
|
try {
|
2025-10-22 16:19:28 +08:00
|
|
|
|
const data = await executeSecondaryProcessQuery(packId, packType);
|
|
|
|
|
|
setSecondaryProcessResults(data);
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
2025-10-22 16:19:28 +08:00
|
|
|
|
if (bitable.ui.showToast) {
|
|
|
|
|
|
await bitable.ui.showToast({
|
|
|
|
|
|
toastType: 'success',
|
|
|
|
|
|
message: '二次工艺查询成功'
|
|
|
|
|
|
});
|
2025-10-22 14:08:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('二次工艺查询出错:', error);
|
|
|
|
|
|
if (bitable.ui.showToast) {
|
|
|
|
|
|
await bitable.ui.showToast({
|
|
|
|
|
|
toastType: 'error',
|
|
|
|
|
|
message: `二次工艺查询失败: ${error.message}`
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSecondaryProcessLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 执行定价详情查询
|
2025-10-22 16:19:28 +08:00
|
|
|
|
const executePricingDetailsQueryLocal = async (packId: string) => {
|
2025-10-22 14:08:03 +08:00
|
|
|
|
setPricingDetailsLoading(true);
|
|
|
|
|
|
try {
|
2025-10-22 16:19:28 +08:00
|
|
|
|
const data = await executePricingDetailsQuery(packId);
|
|
|
|
|
|
setPricingDetailsResults(data);
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
2025-10-22 16:19:28 +08:00
|
|
|
|
if (bitable.ui.showToast) {
|
|
|
|
|
|
await bitable.ui.showToast({
|
|
|
|
|
|
toastType: 'success',
|
|
|
|
|
|
message: '定价详情查询成功'
|
|
|
|
|
|
});
|
2025-10-22 14:08:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
} 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-22 16:19:28 +08:00
|
|
|
|
await executeSecondaryProcessQueryLocal(packId, packType);
|
2025-10-22 14:08:03 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理定价详情查询
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-22 16:19:28 +08:00
|
|
|
|
await executePricingDetailsQueryLocal(packId);
|
2025-10-22 14:08:03 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 获取记录详情的函数
|
|
|
|
|
|
const fetchRecordDetails = async (recordIdList: string[]) => {
|
2025-10-24 09:27:39 +08:00
|
|
|
|
|
2025-10-22 14:08:03 +08:00
|
|
|
|
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 () => {
|
2025-10-24 09:27:39 +08:00
|
|
|
|
|
2025-10-22 14:08:03 +08:00
|
|
|
|
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' }}>
|
2025-10-24 09:27:39 +08:00
|
|
|
|
{/* 入口选择弹窗 */}
|
|
|
|
|
|
<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>
|
|
|
|
|
|
)}
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 时效计算结果模态框 */}
|
|
|
|
|
|
<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>
|
|
|
|
|
|
{timelineResults.map((result, index) => {
|
|
|
|
|
|
const adjustment = timelineAdjustments[index] || 0;
|
|
|
|
|
|
const originalValue = result.timelineValue || 0;
|
|
|
|
|
|
const adjustedValue = originalValue + 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>:按9小时工作制(9: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>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-10-24 09:27:39 +08:00
|
|
|
|
{/* 标签选择部分,仅在生成模式显示 */}
|
|
|
|
|
|
{mode === 'generate' && labelOptions && Object.keys(labelOptions).length > 0 && (
|
2025-10-22 14:08:03 +08:00
|
|
|
|
<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 ... */}
|
|
|
|
|
|
|
2025-10-24 09:27:39 +08:00
|
|
|
|
{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>
|
2025-10-22 14:08:03 +08:00
|
|
|
|
|
2025-10-24 09:27:39 +08:00
|
|
|
|
<Space>
|
2025-10-22 14:08:03 +08:00
|
|
|
|
<Button
|
2025-10-24 09:27:39 +08:00
|
|
|
|
type='primary'
|
|
|
|
|
|
onClick={handleSelectRecords}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
disabled={loading}
|
2025-10-22 14:08:03 +08:00
|
|
|
|
>
|
2025-10-24 09:27:39 +08:00
|
|
|
|
{selectedRecords.length > 0 ? '重新选择' : '选择记录'}
|
2025-10-22 14:08:03 +08:00
|
|
|
|
</Button>
|
2025-10-24 09:27:39 +08:00
|
|
|
|
|
|
|
|
|
|
{selectedRecords.length > 0 && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type='secondary'
|
|
|
|
|
|
onClick={handleClearRecords}
|
|
|
|
|
|
size='small'
|
|
|
|
|
|
>
|
|
|
|
|
|
清空选择
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 已选择记录的详细信息 */}
|
|
|
|
|
|
{selectedRecords.length > 0 && recordDetails.length > 0 && (
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
padding: '12px',
|
|
|
|
|
|
backgroundColor: '#f8f9fa',
|
|
|
|
|
|
borderRadius: '6px',
|
|
|
|
|
|
border: '1px solid #e9ecef',
|
|
|
|
|
|
marginBottom: '16px'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
2025-10-22 14:08:03 +08:00
|
|
|
|
<div>
|
2025-10-24 09:27:39 +08:00
|
|
|
|
<Text strong>主记录:</Text>
|
|
|
|
|
|
<Text code style={{ marginLeft: '8px', fontSize: '12px' }}>{recordDetails[0].displayValue || recordDetails[0].id}</Text>
|
2025-10-22 14:08:03 +08:00
|
|
|
|
</div>
|
2025-10-24 09:27:39 +08:00
|
|
|
|
{recordDetails.length > 1 && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Text type='secondary'>+ 其他 {recordDetails.length - 1} 条</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-10-22 14:08:03 +08:00
|
|
|
|
</div>
|
2025-10-24 09:27:39 +08:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 加载状态 */}
|
|
|
|
|
|
{loading && (
|
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '20px' }}>
|
|
|
|
|
|
<Spin size="large" />
|
|
|
|
|
|
<div style={{ marginTop: '10px' }}>
|
|
|
|
|
|
<Text>正在加载版单数据...</Text>
|
|
|
|
|
|
</div>
|
2025-10-22 14:08:03 +08:00
|
|
|
|
</div>
|
2025-10-24 09:27:39 +08:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 空状态提示 */}
|
|
|
|
|
|
{selectedRecords.length === 0 && !loading && (
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
padding: '20px',
|
|
|
|
|
|
backgroundColor: '#fafafa',
|
|
|
|
|
|
borderRadius: '6px',
|
|
|
|
|
|
border: '1px dashed #d9d9d9'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<Text type="tertiary">请选择版单记录开始操作</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</main>
|
2025-10-22 14:08:03 +08:00
|
|
|
|
)}
|
2025-10-24 09:27:39 +08:00
|
|
|
|
{mode === 'generate' && (
|
2025-10-22 14:08:03 +08:00
|
|
|
|
<>
|
2025-10-24 09:27:39 +08:00
|
|
|
|
{/* 面料数据查询结果 */}
|
|
|
|
|
|
{queryResults.length > 0 && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Divider />
|
|
|
|
|
|
<Title heading={4}>面料数据查询结果 ({queryResults.length} 条)</Title>
|
|
|
|
|
|
<Table
|
|
|
|
|
|
columns={columns}
|
|
|
|
|
|
dataSource={queryResults.map((item, index) => ({ ...item, key: index }))}
|
|
|
|
|
|
pagination={{ pageSize: 10 }}
|
|
|
|
|
|
style={{ marginTop: '10px' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{/* 二次工艺查询结果 */}
|
|
|
|
|
|
{secondaryProcessResults.length > 0 && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Divider />
|
|
|
|
|
|
<Title heading={4}>二次工艺查询结果 ({secondaryProcessResults.length} 条)</Title>
|
|
|
|
|
|
<Table
|
|
|
|
|
|
columns={secondaryProcessColumns}
|
|
|
|
|
|
dataSource={secondaryProcessResults.map((item, index) => ({ ...item, key: index }))}
|
|
|
|
|
|
pagination={{ pageSize: 10 }}
|
|
|
|
|
|
style={{ marginTop: '10px' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{/* 工艺价格查询结果 */}
|
|
|
|
|
|
{pricingDetailsResults.length > 0 && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Divider />
|
|
|
|
|
|
<Title heading={4}>工艺价格查询结果 ({pricingDetailsResults.length} 条)</Title>
|
|
|
|
|
|
<Table
|
|
|
|
|
|
columns={pricingDetailsColumns}
|
|
|
|
|
|
dataSource={pricingDetailsResults.map((item, index) => ({ ...item, key: index }))}
|
|
|
|
|
|
pagination={{ pageSize: 10 }}
|
|
|
|
|
|
style={{ marginTop: '10px' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2025-10-22 14:08:03 +08:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|