diff --git a/src/App.tsx b/src/App.tsx index a1d4a69..1261105 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,8 @@ import { bitable, FieldType } from '@lark-base-open/js-sdk'; -import { Button, Typography, List, Card, Space, Divider, Spin, Table, Select, Modal, DatePicker, InputNumber, Input } from '@douyinfe/semi-ui'; +import { Button, Typography, List, Card, Space, Divider, Spin, Table, Select, Modal, DatePicker, InputNumber, Input, Progress } from '@douyinfe/semi-ui'; import { useState, useEffect, useRef } from 'react'; import { addDays, format } from 'date-fns'; import { zhCN } from 'date-fns/locale'; -import { executePricingQuery, executeSecondaryProcessQuery, executePricingDetailsQuery } from './services/apiService'; const { Title, Text } = Typography; @@ -25,8 +24,10 @@ const extractText = (val: any) => { return typeof item === 'string' ? item : (item?.text || item?.name || ''); } else if (typeof val === 'string') { return val; + } else if (typeof val === 'number') { + return String(val); } else if (val && typeof val === 'object') { - return val.text || val.name || ''; + return val.text || val.name || val.value?.toString?.() || ''; } return ''; }; @@ -35,15 +36,11 @@ export default function App() { const [selectedRecords, setSelectedRecords] = useState([]); const [recordDetails, setRecordDetails] = useState([]); const [loading, setLoading] = useState(false); - const [queryResults, setQueryResults] = useState([]); - const [queryLoading, setQueryLoading] = useState(false); - const [secondaryProcessResults, setSecondaryProcessResults] = useState([]); - const [secondaryProcessLoading, setSecondaryProcessLoading] = useState(false); - const [pricingDetailsResults, setPricingDetailsResults] = useState([]); - const [pricingDetailsLoading, setPricingDetailsLoading] = useState(false); + // 标签相关状态 const [labelOptions, setLabelOptions] = useState<{[key: string]: any[]}>({}); + const [labelAdjustmentFlow, setLabelAdjustmentFlow] = useState(false); const [selectedLabels, setSelectedLabels] = useState<{[key: string]: string | string[]}>({}); const [labelLoading, setLabelLoading] = useState(false); @@ -79,6 +76,20 @@ export default function App() { const [mode, setMode] = useState<'generate' | 'adjust' | null>(null); const [modeSelectionVisible, setModeSelectionVisible] = useState(true); const [adjustLoading, setAdjustLoading] = useState(false); + const [batchModalVisible, setBatchModalVisible] = useState(false); + const [batchStartRow, setBatchStartRow] = useState(1); + const [batchEndRow, setBatchEndRow] = useState(1); + const [batchTotalRows, setBatchTotalRows] = useState(0); + const [batchLoading, setBatchLoading] = useState(false); + const [batchProcessedCount, setBatchProcessedCount] = useState(0); + const [batchProcessingTotal, setBatchProcessingTotal] = useState(0); + const [batchSuccessCount, setBatchSuccessCount] = useState(0); + const [batchFailureCount, setBatchFailureCount] = useState(0); + const [batchProgressList, setBatchProgressList] = useState<{ index: number; foreignId: string; status: 'success' | 'failed'; message?: string }[]>([]); + const [batchCurrentRowInfo, setBatchCurrentRowInfo] = useState<{ index: number; foreignId: string; style: string; color: string } | null>(null); + const batchAbortRef = useRef(false); + const [lastSavedDeliveryRecordId, setLastSavedDeliveryRecordId] = useState(null); + const [lastSavedDeliveryVersion, setLastSavedDeliveryVersion] = useState(null); // 删除未使用的 deliveryRecords 状态 const [selectedDeliveryRecordId, setSelectedDeliveryRecordId] = useState(''); // 从货期记录读取到的record_ids(用于保存回写) @@ -92,9 +103,6 @@ export default function App() { const resetGlobalState = (opts?: { resetMode?: boolean }) => { // 运行时加载状态 setLoading(false); - setQueryLoading(false); - setSecondaryProcessLoading(false); - setPricingDetailsLoading(false); setLabelLoading(false); setAdjustLoading(false); setTimelineLoading(false); @@ -130,6 +138,7 @@ export default function App() { setCurrentColorText(''); setCurrentText2(''); setCurrentVersionNumber(null); + setLabelAdjustmentFlow(false); // 可选:重置模式 if (opts?.resetMode) { @@ -205,7 +214,7 @@ export default function App() { const CALCULATION_METHOD_FIELD_ID = 'fldxfLZNUu'; // 时效计算方式字段ID // 新表ID(批量生成表) - // 已移除:批量生成表ID + const BATCH_TABLE_ID = 'tblXO7iSxBYxrqtY'; // 已移除:调整模式不再加载货期记录列表 @@ -217,6 +226,24 @@ export default function App() { setModeSelectionVisible(false); }; + const openBatchModal = async () => { + try { + const batchTable = await bitable.base.getTable(BATCH_TABLE_ID); + const res = await batchTable.getRecords({ pageSize: 5000 }); + const total = res.records?.length || 0; + setBatchTotalRows(total); + setBatchStartRow(1); + setBatchEndRow(total > 0 ? total : 1); + setBatchProcessingTotal(total); + } catch { + setBatchTotalRows(0); + setBatchStartRow(1); + setBatchEndRow(1); + setBatchProcessingTotal(0); + } + setBatchModalVisible(true); + }; + // 根据货期记录ID读取节点详情并还原流程数据 const loadProcessDataFromDeliveryRecord = async (deliveryRecordId: string) => { if (!deliveryRecordId) { @@ -346,10 +373,26 @@ export default function App() { // 完整快照直接包含timelineResults,优先使用 if (Array.isArray(snapshot.timelineResults)) { setTimelineResults(snapshot.timelineResults); - setTimelineVisible(true); - if (bitable.ui.showToast) { - await bitable.ui.showToast({ toastType: 'success', message: '已按货期记录快照还原流程数据' }); - } + Modal.confirm({ + title: '是否调整标签?', + content: '选择“是”将允许修改标签并重新生成计划(版本按V2/V3/V4递增)', + okText: '是,调整标签', + cancelText: '否,直接还原', + onOk: async () => { + setLabelAdjustmentFlow(true); + setTimelineVisible(false); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'info', message: '请在下方修改标签后点击重新生成计划' }); + } + }, + onCancel: async () => { + setLabelAdjustmentFlow(false); + setTimelineVisible(true); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'success', message: '已按货期记录快照还原流程数据' }); + } + } + }); setTimelineLoading(false); setIsRestoringSnapshot(false); return; @@ -499,15 +542,30 @@ export default function App() { } if (Array.isArray(snapshot.timelineResults)) { - // 兼容旧版本的完整快照格式 setTimelineResults(snapshot.timelineResults); - setTimelineVisible(true); - if (bitable.ui.showToast) { - await bitable.ui.showToast({ toastType: 'success', message: '已按快照一模一样还原流程数据' }); - } + Modal.confirm({ + title: '是否调整标签?', + content: '选择“是”将允许修改标签并重新生成计划(版本按V2/V3/V4递增)', + okText: '是,调整标签', + cancelText: '否,直接还原', + onOk: async () => { + setLabelAdjustmentFlow(true); + setTimelineVisible(false); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'info', message: '请在下方修改标签后点击重新生成计划' }); + } + }, + onCancel: async () => { + setLabelAdjustmentFlow(false); + setTimelineVisible(true); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'success', message: '已按快照一模一样还原流程数据' }); + } + } + }); setTimelineLoading(false); - setIsRestoringSnapshot(false); // 快照还原完成 - return; // 快照还原完成,退出函数 + setIsRestoringSnapshot(false); + return; } else if (snapshot.isCompleteSnapshot || snapshot.snapshotType === 'complete' || snapshot.isGlobalSnapshot || snapshot.isCombinedSnapshot) { // 处理完整快照格式:每个节点都包含完整数据 @@ -515,21 +573,27 @@ export default function App() { // 如果是完整快照,直接使用其中的timelineResults if (snapshot.isCompleteSnapshot && snapshot.timelineResults) { - console.log('使用完整快照中的timelineResults数据'); setTimelineResults(snapshot.timelineResults); - setTimelineVisible(true); - - // 恢复智能缓冲期状态 - if (snapshot.bufferManagement) { - console.log('恢复智能缓冲期状态:', snapshot.bufferManagement); - } - - // 恢复连锁调整系统状态 - if (snapshot.chainAdjustmentSystem) { - console.log('恢复连锁调整系统状态:', snapshot.chainAdjustmentSystem); - } - - // 恢复生成模式状态 + Modal.confirm({ + title: '是否调整标签?', + content: '选择“是”将允许修改标签并重新生成计划(版本按V2/V3/V4递增)', + okText: '是,调整标签', + cancelText: '否,直接还原', + onOk: async () => { + setLabelAdjustmentFlow(true); + setTimelineVisible(false); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'info', message: '请在下方修改标签后点击重新生成计划' }); + } + }, + onCancel: async () => { + setLabelAdjustmentFlow(false); + setTimelineVisible(true); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'success', message: '已按快照一模一样还原流程数据' }); + } + } + }); if (snapshot.generationModeState) { console.log('恢复生成模式状态:', snapshot.generationModeState); const genState = snapshot.generationModeState; @@ -1606,6 +1670,21 @@ export default function App() { console.log('currentExpectedDate:', currentExpectedDate); console.log('currentStartTime:', currentStartTime); + { + const requiredLabelKeys = Array.from({ length: 10 }, (_, i) => `标签${i + 1}`); + const missing = requiredLabelKeys.filter(key => { + const val = (currentSelectedLabels as any)[key]; + if (Array.isArray(val)) return val.length === 0; + return !(typeof val === 'string' && val.trim().length > 0); + }); + if (missing.length > 0) { + if (showUI && bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'warning', message: `请填写以下必填标签:${missing.join('、')}` }); + } + return []; + } + } + // 跳过验证(用于批量模式) if (!skipValidation) { // 检查是否选择了多条记录 @@ -1629,21 +1708,6 @@ export default function App() { } return; } - - // 检查是否选择了标签 - const hasSelectedLabels = Object.values(currentSelectedLabels).some(value => { - return Array.isArray(value) ? value.length > 0 : Boolean(value); - }); - - if (!hasSelectedLabels) { - if (bitable.ui.showToast) { - await bitable.ui.showToast({ - toastType: 'warning', - message: '请先选择至少一个标签' - }); - } - return; - } // 可选:检查是否选择了客户期望日期 if (!currentExpectedDate) { @@ -2915,7 +2979,8 @@ export default function App() { const writeToDeliveryRecordTable = async ( timelineResults: any[], processRecordIds: string[], - timelineAdjustments: {[key: number]: number} = {} + timelineAdjustments: {[key: number]: number} = {}, + overrides?: { foreignId?: string; style?: string; color?: string; expectedDate?: Date | null; startTime?: Date | null; selectedLabels?: {[key: string]: string | string[]} } ) => { try { console.log('=== 开始写入货期记录表 ==='); @@ -2975,17 +3040,17 @@ export default function App() { // 获取foreign_id:调整模式严格使用快照数据,生成模式优先使用选择记录 console.log('=== 开始获取foreign_id ==='); - let foreignId = ''; + let foreignId = overrides?.foreignId ?? ''; // 使用全局状态 const currentSelectedRecords = selectedRecords; const currentRecordDetails = recordDetails; - if (mode === 'adjust') { + if (!foreignId && mode === 'adjust') { // 调整模式:严格使用快照回填的foreign_id,即使为空也不回退 foreignId = currentForeignId; console.log('调整模式:严格使用快照恢复的foreign_id:', foreignId); - } else if (currentSelectedRecords.length > 0) { + } else if (!foreignId && currentSelectedRecords.length > 0) { // 生成模式:从选择记录获取 console.log('生成模式:从选择记录获取foreign_id'); console.log('selectedRecords[0]:', currentSelectedRecords[0]); @@ -3001,35 +3066,35 @@ export default function App() { } // 生成模式的回退逻辑:记录详情 - if (mode !== 'adjust' && !foreignId && currentRecordDetails.length > 0) { + if (!foreignId && mode !== 'adjust' && currentRecordDetails.length > 0) { const first = currentRecordDetails[0]; const val = first.fields['fldpvBfeC0']; foreignId = extractText(val); } // 生成模式的最后回退:快照状态 - if (mode !== 'adjust' && !foreignId && currentForeignId) { + if (!foreignId && mode !== 'adjust' && currentForeignId) { foreignId = currentForeignId; } // 获取款式与颜色:调整模式优先使用快照数据,生成模式优先使用记录详情 - let style = ''; - let color = ''; + let style = overrides?.style ?? ''; + let color = overrides?.color ?? ''; - if (mode === 'adjust') { + if (!style && !color && mode === 'adjust') { // 调整模式:严格使用快照回填的数据,即使为空也不回退 style = currentStyleText; color = currentColorText; console.log('调整模式:严格使用快照恢复的款式:', style, '颜色:', color); } else { // 生成模式:优先使用记录详情 - if (currentRecordDetails.length > 0) { + if (!style && !color && currentRecordDetails.length > 0) { const first = currentRecordDetails[0]; style = extractText(first.fields['fld6Uw95kt']) || currentStyleText || ''; color = extractText(first.fields['flde85ni4O']) || currentColorText || ''; } else { // 回退:使用快照回填的状态 - style = currentStyleText || ''; - color = currentColorText || ''; + style = style || currentStyleText || ''; + color = color || currentColorText || ''; // 若仍为空且有选择记录,仅做一次读取 if ((!style || !color) && currentSelectedRecords.length > 0) { const table = await bitable.base.getTable(TABLE_ID); @@ -3053,7 +3118,7 @@ export default function App() { } // 获取标签汇总:批量模式优先使用传入的labels - const selectedLabelValues = Object.values(selectedLabels).flat().filter(Boolean); + const selectedLabelValues = Object.values(overrides?.selectedLabels ?? selectedLabels).flat().filter(Boolean); // 获取预计交付日期(交期余量的日期版本:最后流程完成日期 + 基础缓冲期) let expectedDeliveryDate = null; @@ -3090,8 +3155,9 @@ export default function App() { // 获取客户期望日期:批量模式优先使用传入的expectedDate let customerExpectedDate = null; - if (expectedDate) { - customerExpectedDate = expectedDate.getTime(); // 转换为时间戳 + const expectedDateToUse = overrides?.expectedDate ?? expectedDate; + if (expectedDateToUse) { + customerExpectedDate = expectedDateToUse.getTime(); } // 创建当前时间戳 @@ -3134,10 +3200,10 @@ export default function App() { // 在创建Cell之前进行数据校验(移除冗余日志) // ===== 构建完整快照(保持与流程数据表写入时的一致内容)并写入到货期记录表 ===== - const expectedDateTimestamp = expectedDate ? expectedDate.getTime() : null; - const expectedDateString = expectedDate ? format(expectedDate, DATE_FORMATS.STORAGE_FORMAT) : null; - const currentStartTime = startTime; - const currentSelectedLabels = selectedLabels; + const expectedDateTimestamp = expectedDateToUse ? expectedDateToUse.getTime() : null; + const expectedDateString = expectedDateToUse ? format(expectedDateToUse, DATE_FORMATS.STORAGE_FORMAT) : null; + const currentStartTime = overrides?.startTime ?? startTime; + const currentSelectedLabels = overrides?.selectedLabels ?? selectedLabels; // 与快照字段保持相同的命名 const styleText = style || ''; @@ -3297,11 +3363,11 @@ export default function App() { const createTimeCell = await createTimeField.createCell(currentTime); // 调试日志:检查startTime参数 - console.log('保存 - startTime参数:', startTime); - console.log('保存 - startTime类型:', typeof startTime); + console.log('保存 - startTime参数:', currentStartTime); + console.log('保存 - startTime类型:', typeof currentStartTime); console.log('保存 - currentTime:', currentTime, '对应日期:', new Date(currentTime).toLocaleString()); - const startTimestamp = startTime ? startTime.getTime() : currentTime; + const startTimestamp = currentStartTime ? currentStartTime.getTime() : currentTime; console.log('保存 - 最终使用的startTimestamp:', startTimestamp, '对应日期:', new Date(startTimestamp).toLocaleString()); const startTimeCell = await startTimeField.createCell(startTimestamp); @@ -3333,7 +3399,17 @@ export default function App() { // 添加记录到货期记录表 const addedRecord = await deliveryRecordTable.addRecord(recordCells); - + // 保存最近一次写入的记录ID与版本号 + try { + const savedId = typeof addedRecord === 'string' + ? addedRecord + : (((addedRecord as any)?.id || (addedRecord as any)?.recordId) as string); + if (savedId) { + setLastSavedDeliveryRecordId(savedId); + setLastSavedDeliveryVersion(versionNumber); + } + } catch {} + return addedRecord; } catch (error) { console.error('写入货期记录表详细错误:', { @@ -3353,7 +3429,7 @@ export default function App() { }; // 写入流程数据表的函数 - const writeToProcessDataTable = async (timelineResults: any[]): Promise => { + const writeToProcessDataTable = async (timelineResults: any[], overrides?: { foreignId?: string; style?: string; color?: string }): Promise => { try { console.log('=== 开始写入流程数据表 ==='); console.log('当前模式:', mode); @@ -3381,7 +3457,7 @@ export default function App() { // 获取foreign_id - 支持批量模式直接传递数据 console.log('=== 开始获取foreign_id ==='); - let foreignId = null; + let foreignId = overrides?.foreignId ?? null; // 使用全局状态 const currentSelectedRecords = selectedRecords; @@ -3423,8 +3499,8 @@ export default function App() { } // 获取款式与颜色:与货期记录写入逻辑保持一致 - let style = ''; - let color = ''; + let style = overrides?.style ?? ''; + let color = overrides?.color ?? ''; if (mode === 'adjust') { // 调整模式:严格使用快照回填的数据 @@ -3693,242 +3769,171 @@ export default function App() { throw error; // 重新抛出错误 } }; - // 已移除批量处理主函数 handleBatchProcess + const handleBatchProcess = async (range?: { start: number; end: number }) => { + try { + resetGlobalState({ resetMode: false }); + setModeSelectionVisible(false); + setBatchLoading(true); + setBatchProcessedCount(0); + setBatchSuccessCount(0); + setBatchFailureCount(0); + setBatchProgressList([]); + batchAbortRef.current = false; + const batchTable = await bitable.base.getTable(BATCH_TABLE_ID); + const fieldMetaList = await batchTable.getFieldMetaList(); + const nameToId = new Map(); + for (const f of fieldMetaList) nameToId.set(f.name, f.id); + const res = await batchTable.getRecords({ pageSize: 5000 }); + const rows = res.records || []; + const total = rows.length; + const startIndex1 = range?.start && range.start > 0 ? range.start : 1; + const endIndex1 = range?.end && range.end > 0 ? range.end : total; + const minIndex = Math.max(1, Math.min(startIndex1, total)); + const maxIndex = Math.max(minIndex, Math.min(endIndex1, total)); + setBatchProcessingTotal(maxIndex - minIndex + 1); + let processed = 0; + for (let i = minIndex - 1; i < maxIndex; i++) { + if (batchAbortRef.current) { + break; + } + const row = rows[i]; + const f = row.fields || {}; + const getText = (name: string) => extractText(f[nameToId.get(name) || '']); + const foreignId = getText('foreignId'); + const styleText = getText('styleText'); + const colorText = getText('colorText'); + const rawStart = f[nameToId.get('startTimestamp') || '']; + const rawExpected = f[nameToId.get('expectedDateTimestamp') || '']; + let startDate: Date | null = null; + let expectedDateObj: Date | null = null; + if (typeof rawStart === 'number') startDate = new Date(rawStart); + else if (Array.isArray(rawStart) && rawStart.length > 0) { + const item = rawStart[0]; + if (typeof item === 'number') startDate = new Date(item); + else startDate = parseDate(extractText(rawStart)); + } else if (typeof rawStart === 'string') startDate = parseDate(rawStart); + else if (rawStart && typeof rawStart === 'object') { + const v = (rawStart as any).value || (rawStart as any).text || (rawStart as any).name || ''; + if (typeof v === 'number') startDate = new Date(v); else startDate = parseDate(v); + } + if (typeof rawExpected === 'number') expectedDateObj = new Date(rawExpected); + else if (Array.isArray(rawExpected) && rawExpected.length > 0) { + const item = rawExpected[0]; + if (typeof item === 'number') expectedDateObj = new Date(item); + else expectedDateObj = parseDate(extractText(rawExpected)); + } else if (typeof rawExpected === 'string') expectedDateObj = parseDate(rawExpected); + else if (rawExpected && typeof rawExpected === 'object') { + const v = (rawExpected as any).value || (rawExpected as any).text || (rawExpected as any).name || ''; + if (typeof v === 'number') expectedDateObj = new Date(v); else expectedDateObj = parseDate(v); + } + const splitVals = (s: string) => (s || '').split(/[,,、]+/).map(v => v.trim()).filter(Boolean); + const labels: { [key: string]: string | string[] } = {}; + for (let i2 = 1; i2 <= 10; i2++) { + const key = `标签${i2}`; + const v = getText(key); + if (v && v.trim()) { + if (i2 === 7 || i2 === 8 || i2 === 10) labels[key] = splitVals(v); + else labels[key] = v.trim(); + } + } + { + const requiredLabelKeys = Array.from({ length: 10 }, (_, k) => `标签${k + 1}`); + const missing = requiredLabelKeys.filter(k => { + const val = (labels as any)[k]; + if (Array.isArray(val)) return val.length === 0; + return !(typeof val === 'string' && val.trim().length > 0); + }); + if (missing.length > 0) { + setBatchProcessedCount(p => p + 1); + setBatchFailureCount(fCount => fCount + 1); + setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'failed', message: `标签不完整:${missing.join('、')}` }]); + continue; + } + } + setBatchCurrentRowInfo({ index: i + 1, foreignId: foreignId || '', style: styleText || '', color: colorText || '' }); + setCurrentForeignId(foreignId || ''); + setCurrentStyleText(styleText || ''); + setCurrentColorText(colorText || ''); + setExpectedDate(expectedDateObj || null); + setStartTime(startDate || null); + setSelectedLabels(labels); + try { + const results = await handleCalculateTimeline(true, { selectedLabels: labels, expectedDate: expectedDateObj || null, startTime: startDate || null }, false); + if (results && results.length > 0) { + const processRecordIds = await writeToProcessDataTable(results, { foreignId, style: styleText, color: colorText }); + const deliveryRecordId = await writeToDeliveryRecordTable(results, processRecordIds, {}, { foreignId, style: styleText, color: colorText, expectedDate: expectedDateObj || null, startTime: startDate || null, selectedLabels: labels }); + try { + const candidateNames = ['状态','record_id','记录ID','货期记录ID','deliveryRecordId']; + let statusFieldId = ''; + for (const nm of candidateNames) { const id = nameToId.get(nm) || ''; if (id) { statusFieldId = id; break; } } + if (!statusFieldId) statusFieldId = 'fldKTpPL9s'; + const rowRecordId = (row.id || (row as any).recordId || (row as any)._id || (row as any).record_id); + const deliveryRecordIdStr = typeof deliveryRecordId === 'string' + ? deliveryRecordId + : ((deliveryRecordId && ((deliveryRecordId as any).id || (deliveryRecordId as any).recordId)) ? (((deliveryRecordId as any).id || (deliveryRecordId as any).recordId) as string) : ''); + if (statusFieldId && rowRecordId && deliveryRecordIdStr) { + const statusField = await batchTable.getField(statusFieldId); + try { + await (statusField as any).setValue(rowRecordId, deliveryRecordIdStr); + } catch (eSet) { + try { + const statusCell = await statusField.createCell(deliveryRecordIdStr); + try { + await batchTable.updateRecord(rowRecordId, [statusCell]); + } catch (e1) { + const fn = (batchTable as any).setRecord || (batchTable as any).updateRecordById; + if (typeof fn === 'function') { + await fn.call(batchTable, rowRecordId, [statusCell]); + } else { + throw e1; + } + } + } catch (e2) { + throw e2; + } + } + setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'success', message: `记录ID: ${deliveryRecordIdStr}` }]); + } else { + setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'failed', message: '未找到状态字段或记录ID为空' }]); + } + } catch (statusErr) { + console.warn('回写批量状态字段失败', statusErr); + setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'failed', message: `状态写入失败: ${statusErr?.message || '未知错误'}` }]); + } + processed++; + setBatchProcessedCount(p => p + 1); + setBatchSuccessCount(s => s + 1); + } else { + setBatchProcessedCount(p => p + 1); + setBatchFailureCount(fCount => fCount + 1); + setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'failed', message: '时效结果为空' }]); + } + } catch (rowErr: any) { + setBatchProcessedCount(p => p + 1); + setBatchFailureCount(fCount => fCount + 1); + setBatchProgressList(list => [...list, { index: i + 1, foreignId: foreignId || '', status: 'failed', message: rowErr?.message || '处理失败' }]); + } + } + if (bitable.ui.showToast) { + const aborted = batchAbortRef.current; + await bitable.ui.showToast({ toastType: 'success', message: aborted ? `批量已中止,已处理 ${processed} 条记录` : `批量生成完成,共处理 ${processed} 条记录` }); + } + } catch (error) { + console.error('批量处理失败:', error); + if (bitable.ui.showToast) { + await bitable.ui.showToast({ toastType: 'error', message: '批量处理失败,请检查数据表配置' }); + } + } finally { + setBatchLoading(false); + } + }; // 为批量处理优化的时效计算函数 // 已移除批量时效计算函数 calculateTimelineForBatch - // 执行定价数据查询 - const executeQuery = async (packId: string, packType: string) => { - if (queryLoading) return; - setQueryLoading(true); - try { - // 使用 apiService 中的函数 - const data = await executePricingQuery(packId, packType, selectedLabels); - setQueryResults(data); - - // 处理品类属性填充到标签8的逻辑保持不变 - if (data && data.length > 0) { - const label8Options = labelOptions['标签8'] || []; - const matchingCategoryValues: string[] = []; - - data.forEach((record: any) => { - if (record.品类属性 && record.品类属性.trim() !== '') { - const categoryValue = record.品类属性.trim(); - const matchingOption = label8Options.find(option => - option.value === categoryValue || option.label === categoryValue - ); - - if (matchingOption && !matchingCategoryValues.includes(categoryValue)) { - matchingCategoryValues.push(categoryValue); - } - } - }); - - if (matchingCategoryValues.length > 0) { - setSelectedLabels(prev => ({ - ...prev, - '标签8': matchingCategoryValues - })); - - if (bitable.ui.showToast) { - await bitable.ui.showToast({ - toastType: 'success', - message: `已将 ${matchingCategoryValues.length} 个品类属性填充到标签8: ${matchingCategoryValues.join(', ')}` - }); - } - } - } - - if (bitable.ui.showToast) { - await bitable.ui.showToast({ - toastType: 'success', - message: '查询成功' - }); - } - - } catch (error: any) { - console.error('数据库查询出错:', error); - if (bitable.ui.showToast) { - await bitable.ui.showToast({ - toastType: 'error', - message: `数据库查询失败: ${error.message}` - }); - } - } finally { - setQueryLoading(false); - } - }; - - // 执行二次工艺查询 - const executeSecondaryProcessQueryLocal = async (packId: string, packType: string) => { - if (secondaryProcessLoading) return; - setSecondaryProcessLoading(true); - try { - const data = await executeSecondaryProcessQuery(packId, packType); - setSecondaryProcessResults(data); - - // 处理二次工艺填充到标签7的逻辑 - if (data && data.length > 0) { - const label7Options = labelOptions['标签7'] || []; - const matchingProcessValues: string[] = []; - - data.forEach((record: any) => { - if (record.costs_type && record.costs_type.trim() !== '') { - const processValue = record.costs_type.trim(); - const matchingOption = label7Options.find(option => - option.value === processValue || option.label === processValue - ); - - if (matchingOption && !matchingProcessValues.includes(processValue)) { - matchingProcessValues.push(processValue); - } - } - }); - - if (matchingProcessValues.length > 0) { - setSelectedLabels(prev => ({ - ...prev, - '标签7': matchingProcessValues - })); - - if (bitable.ui.showToast) { - await bitable.ui.showToast({ - toastType: 'success', - message: `已将 ${matchingProcessValues.length} 个二次工艺填充到标签7: ${matchingProcessValues.join(', ')}` - }); - } - } - } - - if (bitable.ui.showToast) { - await bitable.ui.showToast({ - toastType: 'success', - message: '二次工艺查询成功' - }); - } - } catch (error: any) { - console.error('二次工艺查询出错:', error); - if (bitable.ui.showToast) { - await bitable.ui.showToast({ - toastType: 'error', - message: `二次工艺查询失败: ${error.message}` - }); - } - } finally { - setSecondaryProcessLoading(false); - } - }; - - // 执行定价详情查询 - const executePricingDetailsQueryLocal = async (packId: string) => { - if (pricingDetailsLoading) return; - setPricingDetailsLoading(true); - try { - const data = await executePricingDetailsQuery(packId); - setPricingDetailsResults(data); - - if (bitable.ui.showToast) { - await bitable.ui.showToast({ - toastType: 'success', - message: '定价详情查询成功' - }); - } - } catch (error: any) { - console.error('定价详情查询出错:', error); - if (bitable.ui.showToast) { - await bitable.ui.showToast({ - toastType: 'error', - message: `定价详情查询失败: ${error.message}` - }); - } - } finally { - setPricingDetailsLoading(false); - } - }; - - // 处理数据库查询 - const handleQueryDatabase = async (record: any) => { - // 从记录字段中提取 packId 和 packType - let packId = ''; - let packType = ''; - - // 提取 pack_id (fldpvBfeC0) - if (record.fields.fldpvBfeC0 && Array.isArray(record.fields.fldpvBfeC0) && record.fields.fldpvBfeC0.length > 0) { - packId = record.fields.fldpvBfeC0[0].text; - } - - // 提取 pack_type (fldSAF9qXe) - if (record.fields.fldSAF9qXe && record.fields.fldSAF9qXe.text) { - packType = record.fields.fldSAF9qXe.text; - } - - if (!packId || !packType) { - if (bitable.ui.showToast) { - await bitable.ui.showToast({ - toastType: 'error', - message: '缺少必要的查询参数 (pack_id 或 pack_type)' - }); - } - return; - } - - await executeQuery(packId, packType); - }; - - // 处理二次工艺查询 - const handleSecondaryProcessQuery = async (record: any) => { - // 从记录字段中提取 packId 和 packType - let packId = ''; - let packType = ''; - - // 提取 pack_id (fldpvBfeC0) - if (record.fields.fldpvBfeC0 && Array.isArray(record.fields.fldpvBfeC0) && record.fields.fldpvBfeC0.length > 0) { - packId = record.fields.fldpvBfeC0[0].text; - } - - // 提取 pack_type (fldSAF9qXe) - if (record.fields.fldSAF9qXe && record.fields.fldSAF9qXe.text) { - packType = record.fields.fldSAF9qXe.text; - } - - if (!packId || !packType) { - if (bitable.ui.showToast) { - await bitable.ui.showToast({ - toastType: 'error', - message: '缺少必要的查询参数 (pack_id 或 pack_type)' - }); - } - return; - } - - await executeSecondaryProcessQueryLocal(packId, packType); - }; - - // 处理定价详情查询 - const handlePricingDetailsQuery = async (record: any) => { - // 从记录字段中提取 packId - let packId = ''; - - // 提取 pack_id (fldpvBfeC0) - if (record.fields.fldpvBfeC0 && Array.isArray(record.fields.fldpvBfeC0) && record.fields.fldpvBfeC0.length > 0) { - packId = record.fields.fldpvBfeC0[0].text; - } - - if (!packId) { - if (bitable.ui.showToast) { - await bitable.ui.showToast({ - toastType: 'error', - message: '缺少必要的查询参数 (pack_id)' - }); - } - return; - } - - await executePricingDetailsQueryLocal(packId); - }; + @@ -4077,6 +4082,32 @@ export default function App() { const label4Value = extractFieldValue('中类名称'); const label5Value = extractFieldValue('小类名称'); const label6Value = extractFieldValue('工艺难易度'); + const extractFieldValuesById = (fieldId: string): string[] => { + const v = firstRecord.fields[fieldId]; + if (!v) return []; + if (Array.isArray(v)) { + return v + .flatMap((item: any) => { + const raw = typeof item === 'string' ? item : (item?.text || item?.name || ''); + return raw.split(/[,,、]+/); + }) + .map((s: string) => (s || '').trim()) + .filter(Boolean) as string[]; + } + if (typeof v === 'string') { + return v + .split(/[,,、]+/) + .map(s => s.trim()) + .filter(Boolean); + } + if (typeof v === 'object' && v !== null) { + const s = (v.text || v.name || '').trim(); + return s + ? s.split(/[,,、]+/).map(x => x.trim()).filter(Boolean) + : []; + } + return []; + }; // 设置提取到的标签值 const newSelectedLabels: {[key: string]: string | string[]} = {}; @@ -4085,6 +4116,22 @@ export default function App() { if (label4Value) newSelectedLabels['标签4'] = label4Value; if (label5Value) newSelectedLabels['标签5'] = label5Value; if (label6Value) newSelectedLabels['标签6'] = label6Value; + const label8Vals = extractFieldValuesById('fldLeU2Qhq'); + if (label8Vals.length > 0) { + const opts = labelOptions['标签8'] || []; + const matched = label8Vals.filter(val => opts.some(o => o.value === val || o.label === val)); + if (matched.length > 0) { + newSelectedLabels['标签8'] = matched; + } + } + const label7Vals = extractFieldValuesById('fldt9v2oJM'); + if (label7Vals.length > 0) { + const opts = labelOptions['标签7'] || []; + const matched = label7Vals.filter(val => opts.some(o => o.value === val || o.label === val)); + if (matched.length > 0) { + newSelectedLabels['标签7'] = matched; + } + } // 添加标签10的自动填充 newSelectedLabels['标签10'] = ['复版', '开货版不打版']; @@ -4105,15 +4152,6 @@ export default function App() { }); } - // 自动执行所有三个查询 - 对第一条记录顺序执行查询 - // 顺序执行查询,避免 loading 状态冲突 - try { - await handleQueryDatabase(recordValList[0]); - await handleSecondaryProcessQuery(recordValList[0]); - await handlePricingDetailsQuery(recordValList[0]); - } catch (queryError) { - console.error('自动查询出错:', queryError); - } } } else { setRecordDetails([]); @@ -4135,115 +4173,12 @@ export default function App() { 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 (
@@ -4300,11 +4235,76 @@ export default function App() { background: 'linear-gradient(180deg, #fff, #f9fbff)' }} > - {/* 批量入口已移除 */} + 批量生成 + 从批量生成数据表读取并写入记录 +
+ +
+ { batchAbortRef.current = true; setBatchModalVisible(false); }} + footer={null} + maskClosable={false} + > +
+ +
+ 起始行 + setBatchStartRow(typeof v === 'number' ? v : 1)} style={{ width: 120 }} disabled={batchLoading} /> + 结束行 + setBatchEndRow(typeof v === 'number' ? v : 1)} style={{ width: 120 }} disabled={batchLoading} /> + 总行数:{batchTotalRows} + + + + + +
+
+ + {(batchLoading || batchProcessedCount > 0) && ( + +
+
+ 0 ? Math.round((batchProcessedCount / batchProcessingTotal) * 100) : 0} showInfo /> +
+
+
+ 进度:{batchProcessedCount}/{batchProcessingTotal} + 成功 {batchSuccessCount},失败 {batchFailureCount} +
+
+ {batchCurrentRowInfo && ( +
+ 当前处理行:{batchCurrentRowInfo.index} + 款号:{batchCurrentRowInfo.foreignId || '-'} + 款式:{batchCurrentRowInfo.style || '-'} + 颜色:{batchCurrentRowInfo.color || '-'} +
+ )} + ( + + + 行 {item.index} + 款号 {item.foreignId || '-'} + {item.status === 'success' ? '成功' : '失败'} + {item.message && {item.message}} + + + )} + /> + + )} +
+ + {mode === 'generate' && (
<Text type="tertiary" style={{ fontSize: '14px' }}>读取货期记录,精确还原时间线</Text> + {lastSavedDeliveryRecordId && lastSavedDeliveryVersion !== null && ( + <Card style={{ marginTop: '12px', padding: '12px', backgroundColor: '#f6f8fa', borderRadius: 8 }}> + <Text strong>最近保存的货期记录</Text> + <div style={{ marginTop: 8, display: 'flex', gap: 12, flexWrap: 'wrap' }}> + <span style={{ + fontSize: 12, + color: '#1f2937', + background: 'rgba(245, 158, 11, 0.12)', + border: '1px solid rgba(245, 158, 11, 0.32)', + borderRadius: 6, + padding: '4px 8px', + fontWeight: 600 + }}>版本:V{lastSavedDeliveryVersion}</span> + <span style={{ + fontSize: 12, + color: '#111827', + background: 'rgba(17, 24, 39, 0.08)', + border: '1px solid rgba(17, 24, 39, 0.18)', + borderRadius: 6, + padding: '4px 8px' + }}>record_id:{lastSavedDeliveryRecordId}</span> + </div> + </Card> + )} </div> )} {/* 功能入口切换与调整入口 */} @@ -5280,22 +5304,24 @@ export default function App() { - {/* 标签选择部分,仅在生成模式显示 */} - {mode === 'generate' && labelOptions && Object.keys(labelOptions).length > 0 && ( + {/* 标签选择部分,生成/调整模式均可使用 */} + {(mode === 'generate' || labelAdjustmentFlow) && labelOptions && Object.keys(labelOptions).length > 0 && ( <Card title="标签选择" className="card-enhanced" style={{ marginBottom: '24px' }}> <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'; + const val = selectedLabels[labelKey]; + const isMissing = Array.isArray(val) ? (val as string[]).length === 0 : !(typeof val === 'string' && (val as string).trim().length > 0); return ( <div key={labelKey}> - <Text strong style={{ display: 'block', marginBottom: '8px' }}>{labelKey}</Text> + <Text strong style={{ display: 'block', marginBottom: '8px' }}>{labelKey}(必填)</Text> <Select className="select-enhanced" - style={{ width: '100%' }} - placeholder={`请选择${labelKey}或输入关键字搜索`} + style={{ width: '100%', borderColor: isMissing ? '#ff4d4f' : undefined }} + placeholder={`请选择${labelKey}(必填)`} value={selectedLabels[labelKey]} onChange={(value) => handleLabelChange(labelKey, value)} multiple={isMultiSelect} @@ -5308,6 +5334,9 @@ export default function App() { </Select.Option> ))} </Select> + {isMissing && ( + <Text type="danger" style={{ marginTop: '6px', display: 'block' }}>该标签为必填</Text> + )} </div> ); })} @@ -5350,12 +5379,18 @@ export default function App() { type='primary' className="btn-gradient-calculate" size="large" - onClick={handleCalculateTimeline} + onClick={() => labelAdjustmentFlow + ? handleCalculateTimeline(true, { selectedLabels, expectedDate, startTime }, true) + : handleCalculateTimeline()} loading={timelineLoading} - disabled={timelineLoading} + disabled={timelineLoading || Array.from({ length: 10 }, (_, i) => `标签${i + 1}`).some(key => { + const val = (selectedLabels as any)[key]; + if (Array.isArray(val)) return val.length === 0; + return !(typeof val === 'string' && val.trim().length > 0); + })} style={{ minWidth: '160px' }} > - 计算预计时间 + {labelAdjustmentFlow ? '重新生成计划' : '计算预计时间'} </Button> <Text type="secondary" style={{ fontSize: '14px' }}> 已选择 {Object.keys(selectedLabels).length} 个标签 @@ -5499,49 +5534,7 @@ export default function App() { {/* 批量处理配置已移除 */} </main> )} - {mode === 'generate' && ( - <> - {/* 面料数据查询结果 */} - {queryResults.length > 0 && ( - <> - <Divider /> - <Title heading={4}>面料数据查询结果 ({queryResults.length} 条) - ({ ...item, key: index }))} - pagination={{ pageSize: 10 }} - style={{ marginTop: '10px' }} - /> - - )} - {/* 二次工艺查询结果 */} - {secondaryProcessResults.length > 0 && ( - <> - - 二次工艺查询结果 ({secondaryProcessResults.length} 条) -
({ ...item, key: index }))} - pagination={{ pageSize: 10 }} - style={{ marginTop: '10px' }} - /> - - )} - {/* 工艺价格查询结果 */} - {pricingDetailsResults.length > 0 && ( - <> - - 工艺价格查询结果 ({pricingDetailsResults.length} 条) -
({ ...item, key: index }))} - pagination={{ pageSize: 10 }} - style={{ marginTop: '10px' }} - /> - - )} - - )} + ); -} \ No newline at end of file +}