diff --git a/src/App.tsx b/src/App.tsx index 5c3fbeb..9b5b1b4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import { bitable, FieldType, ToastType } from '@lark-base-open/js-sdk'; -import { Button, Typography, List, Card, Space, Divider, Spin, Table, Select, Modal, DatePicker, InputNumber, Input, Progress, Switch } from '@douyinfe/semi-ui'; +import { Button, Typography, List, Card, Space, Divider, Spin, Table, Select, Modal, DatePicker, InputNumber, Input, Progress, Switch, Tag } from '@douyinfe/semi-ui'; import { useState, useEffect, useRef } from 'react'; import { addDays, format, differenceInCalendarDays } from 'date-fns'; import { zhCN } from 'date-fns/locale'; @@ -49,6 +49,14 @@ export default function App() { const [labelAdjustmentFlow, setLabelAdjustmentFlow] = useState(false); const [selectedLabels, setSelectedLabels] = useState<{[key: string]: string | string[]}>({}); const [labelLoading, setLabelLoading] = useState(false); + const [excludedDatesOverride, setExcludedDatesOverride] = useState([]); + const [excludedDatesOverrideText, setExcludedDatesOverrideText] = useState(''); + const [excludedDatesByNodeOverride, setExcludedDatesByNodeOverride] = useState>({}); + const excludedDatesByNodeOverrideRef = useRef>({}); + const pendingRecalculateAfterExcludedDatesRef = useRef(false); + const [excludedDatesAdjustVisible, setExcludedDatesAdjustVisible] = useState(false); + const [excludedDatesByNodeDraft, setExcludedDatesByNodeDraft] = useState>({}); + const [excludedDatesAddDraft, setExcludedDatesAddDraft] = useState>({}); // 客户期望日期状态 const [expectedDate, setExpectedDate] = useState(null); @@ -130,6 +138,13 @@ export default function App() { setSelectedRecords([]); setRecordDetails([]); setSelectedLabels({}); + setExcludedDatesOverride([]); + setExcludedDatesOverrideText(''); + setExcludedDatesByNodeOverride({}); + excludedDatesByNodeOverrideRef.current = {}; + setExcludedDatesAdjustVisible(false); + setExcludedDatesByNodeDraft({}); + setExcludedDatesAddDraft({}); setExpectedDate(null); setStartTime(null); setCalculatedRequiredStartTime(null); @@ -223,6 +238,10 @@ export default function App() { ensureStyleColorDefaults(); } }, [timelineVisible]); + + useEffect(() => { + excludedDatesByNodeOverrideRef.current = excludedDatesByNodeOverride || {}; + }, [excludedDatesByNodeOverride]); const VIEW_ID = 'vewb28sjuX'; // 标签表ID @@ -350,6 +369,212 @@ export default function App() { } }; + const normalizeExcludedDatesOverride = (raw: any): string[] => { + const parts: string[] = []; + if (Array.isArray(raw)) { + for (const el of raw) { + const s = typeof el === 'string' ? el : extractText(el); + if (s) parts.push(s); + } + } else if (typeof raw === 'string') { + parts.push(...raw.split(/[\,\n\r\s]+/)); + } else if (raw && typeof raw === 'object') { + const s = extractText(raw); + if (s) parts.push(...s.split(/[\,\n\r\s]+/)); + } + + const out: string[] = []; + const seen = new Set(); + for (const p of parts) { + const t = (p || '').trim(); + if (!t) continue; + const d = parseDate(t); + if (!d || isNaN(d.getTime())) continue; + const normalized = formatDate(d, 'DISPLAY_DATE_ONLY'); + if (!normalized) continue; + if (!seen.has(normalized)) { + seen.add(normalized); + out.push(normalized); + } + } + out.sort(); + return out; + }; + + const restoreExcludedDatesOverrideFromSnapshot = (snapshot: any, timelineResultsCandidate?: any[]) => { + const candidates: any[] = [ + snapshot?.excludedDatesOverride, + snapshot?.timelineCalculationState?.excludedDatesOverride, + snapshot?.timelineCalculationState?.excludedDates, + ]; + for (const c of candidates) { + const normalized = normalizeExcludedDatesOverride(c); + if (normalized.length > 0) { + setExcludedDatesOverride(normalized); + setExcludedDatesOverrideText(normalized.join('\n')); + return; + } + } + + const results = Array.isArray(timelineResultsCandidate) + ? timelineResultsCandidate + : (Array.isArray(snapshot?.timelineResults) ? snapshot.timelineResults : []); + + if (results.length > 0) { + const union: string[] = []; + for (const r of results) { + const arr = Array.isArray(r?.excludedDates) ? r.excludedDates : []; + union.push(...arr); + } + const normalized = normalizeExcludedDatesOverride(union); + if (normalized.length > 0) { + setExcludedDatesOverride(normalized); + setExcludedDatesOverrideText(normalized.join('\n')); + return; + } + } + + setExcludedDatesOverride([]); + setExcludedDatesOverrideText(''); + }; + + const handleExcludedDatesOverrideTextChange = (next: any) => { + const text = typeof next === 'string' + ? next + : (next?.target?.value ?? ''); + setExcludedDatesOverrideText(text); + setExcludedDatesOverride(normalizeExcludedDatesOverride(text)); + }; + + const buildExcludedDatesNodeKey = (nodeName: any, processOrder: any, indexFallback?: number) => { + const name = (typeof nodeName === 'string' ? nodeName : extractText(nodeName)).trim(); + const order = (typeof processOrder === 'number' || typeof processOrder === 'string') + ? String(processOrder) + : ''; + if (name && order) return `${order}::${name}`; + if (name) return name; + if (indexFallback !== undefined) return `#${indexFallback + 1}`; + return '#unknown'; + }; + + const buildExcludedDatesByNodeFromTimeline = (results: any[]): Record => { + const map: Record = {}; + for (let i = 0; i < results.length; i++) { + const r = results[i]; + const key = buildExcludedDatesNodeKey(r?.nodeName, r?.processOrder, i); + const arr = Array.isArray(r?.excludedDates) ? r.excludedDates : []; + map[key] = normalizeExcludedDatesOverride(arr); + } + return map; + }; + + const normalizeExcludedDatesByNodeMap = (raw: any): Record => { + const out: Record = {}; + if (!raw) return out; + if (typeof raw !== 'object') return out; + + for (const [k, v] of Object.entries(raw)) { + const key = (k || '').trim(); + if (!key) continue; + const normalized = normalizeExcludedDatesOverride(v); + out[key] = normalized; + } + return out; + }; + + const restoreExcludedDatesByNodeOverrideFromSnapshot = (snapshot: any, timelineResultsCandidate?: any[]) => { + const candidates: any[] = [ + snapshot?.excludedDatesByNodeOverride, + snapshot?.excludedDatesByNode, + snapshot?.timelineCalculationState?.excludedDatesByNodeOverride, + snapshot?.timelineCalculationState?.excludedDatesByNode, + ]; + + for (const c of candidates) { + const normalized = normalizeExcludedDatesByNodeMap(c); + if (Object.keys(normalized).length > 0) { + excludedDatesByNodeOverrideRef.current = normalized; + setExcludedDatesByNodeOverride(normalized); + return; + } + } + + const results = Array.isArray(timelineResultsCandidate) + ? timelineResultsCandidate + : (Array.isArray(snapshot?.timelineResults) ? snapshot.timelineResults : []); + if (results.length > 0) { + const derived = buildExcludedDatesByNodeFromTimeline(results); + excludedDatesByNodeOverrideRef.current = derived; + setExcludedDatesByNodeOverride(derived); + return; + } + + excludedDatesByNodeOverrideRef.current = {}; + setExcludedDatesByNodeOverride({}); + }; + + const groupDatesByMonth = (dates: string[]) => { + const groups = new Map(); + for (const d of dates) { + const ds = (d || '').trim(); + if (!ds) continue; + const month = ds.slice(0, 7); + if (!groups.has(month)) groups.set(month, []); + groups.get(month)!.push(ds); + } + const out = Array.from(groups.entries()) + .map(([month, ds]) => ({ + month, + dates: Array.from(new Set(ds)).sort(), + })) + .sort((a, b) => a.month.localeCompare(b.month)); + return out; + }; + + const openExcludedDatesAdjustModal = () => { + const baseResults = Array.isArray(timelineResults) ? timelineResults : []; + const draft: Record = {}; + for (let i = 0; i < baseResults.length; i++) { + const r = baseResults[i]; + const key = buildExcludedDatesNodeKey(r?.nodeName, r?.processOrder, i); + const existing = excludedDatesByNodeOverrideRef.current?.[key]; + const fromResult = Array.isArray(r?.excludedDates) ? r.excludedDates : []; + draft[key] = normalizeExcludedDatesOverride(existing ?? fromResult); + } + setExcludedDatesByNodeDraft(draft); + setExcludedDatesAddDraft({}); + setExcludedDatesAdjustVisible(true); + }; + + const closeExcludedDatesAdjustModal = () => { + setExcludedDatesAdjustVisible(false); + setExcludedDatesByNodeDraft({}); + setExcludedDatesAddDraft({}); + }; + + const removeExcludedDateFromDraft = (nodeKey: string, date: string) => { + setExcludedDatesByNodeDraft(prev => { + const current = Array.isArray(prev?.[nodeKey]) ? prev[nodeKey] : []; + const next = current.filter(d => d !== date); + return { ...(prev || {}), [nodeKey]: next }; + }); + }; + + const addExcludedDateToDraft = (nodeKey: string, date: Date | null) => { + if (!(date instanceof Date) || isNaN(date.getTime())) return; + const normalized = formatDate(date, 'DISPLAY_DATE_ONLY'); + if (!normalized) return; + setExcludedDatesByNodeDraft(prev => { + const current = Array.isArray(prev?.[nodeKey]) ? prev[nodeKey] : []; + const next = Array.from(new Set([...current, normalized])).sort(); + return { ...(prev || {}), [nodeKey]: next }; + }); + }; + + const clearExcludedDatesDraftForNode = (nodeKey: string) => { + setExcludedDatesByNodeDraft(prev => ({ ...(prev || {}), [nodeKey]: [] })); + }; + // 根据货期记录ID读取节点详情并还原流程数据 const loadProcessDataFromDeliveryRecord = async (deliveryRecordId: string) => { if (!deliveryRecordId) { @@ -434,6 +659,8 @@ export default function App() { const snapshot = JSON.parse(deliverySnapStr); restoreTimelineDirectionFromSnapshot(snapshot); restoreBaseBufferDaysFromSnapshot(snapshot); + restoreExcludedDatesOverrideFromSnapshot(snapshot, snapshot?.timelineResults); + restoreExcludedDatesByNodeOverrideFromSnapshot(snapshot, snapshot?.timelineResults); // 恢复页面与全局状态 if (snapshot.selectedLabels) setSelectedLabels(snapshot.selectedLabels); @@ -487,6 +714,8 @@ export default function App() { setIsExpectedDeliveryDateLocked(false); } } + restoreExcludedDatesOverrideFromSnapshot(snapshot, snapshot.timelineResults); + restoreExcludedDatesByNodeOverrideFromSnapshot(snapshot, snapshot.timelineResults); if (snapshot.expectedDateTimestamp) { setExpectedDate(new Date(snapshot.expectedDateTimestamp)); } else if (snapshot.expectedDateString) { @@ -594,6 +823,8 @@ export default function App() { setIsRestoringSnapshot(true); restoreTimelineDirectionFromSnapshot(snapshot); restoreBaseBufferDaysFromSnapshot(snapshot); + restoreExcludedDatesOverrideFromSnapshot(snapshot, snapshot?.timelineResults); + restoreExcludedDatesByNodeOverrideFromSnapshot(snapshot, snapshot?.timelineResults); if (snapshot.selectedLabels) setSelectedLabels(snapshot.selectedLabels); if (!mode && snapshot.mode) setMode(snapshot.mode); @@ -782,6 +1013,8 @@ export default function App() { if (vNum !== null && !isNaN(vNum)) setCurrentVersionNumber(vNum); } if (snapshot.timelineAdjustments) setTimelineAdjustments(snapshot.timelineAdjustments); + restoreExcludedDatesOverrideFromSnapshot(snapshot, snapshot.timelineResults); + restoreExcludedDatesByNodeOverrideFromSnapshot(snapshot, snapshot.timelineResults); if (snapshot.expectedDateTimestamp) { setExpectedDate(new Date(snapshot.expectedDateTimestamp)); } else if (snapshot.expectedDateString) { @@ -1084,6 +1317,8 @@ export default function App() { // 重组完整的 timelineResults restoreTimelineDirectionFromSnapshot(globalSnapshotData); restoreBaseBufferDaysFromSnapshot(globalSnapshotData); + restoreExcludedDatesOverrideFromSnapshot(globalSnapshotData, nodeSnapshots); + restoreExcludedDatesByNodeOverrideFromSnapshot(globalSnapshotData, nodeSnapshots); setTimelineResults(nodeSnapshots); setTimelineVisible(true); @@ -1902,7 +2137,7 @@ export default function App() { try { const deliveryTable = await bitable.base.getTable(DELIVERY_RECORD_TABLE_ID); const versionField: any = await deliveryTable.getField(DELIVERY_VERSION_FIELD_ID); - const planFilter = { + const planFilter: any = { conjunction: 'and', conditions: [{ fieldId: DELIVERY_RECORD_IDS_FIELD_ID, operator: 'is', value: planText }] }; @@ -1910,38 +2145,115 @@ export default function App() { ? [{ fieldId: DELIVERY_CREATE_TIME_FIELD_ID, desc: true }] : undefined; - let token: any = undefined; const eps = 1e-9; - for (let i = 0; i < 10000; i++) { - const res: any = await versionField.getFieldValueListByPage({ - pageSize: 200, - pageToken: token, - filter: planFilter, - sort, - stringValue: true - }); - const fieldValues: any[] = Array.isArray(res?.fieldValues) ? res.fieldValues : []; + const normalizePlanText = (s: string) => s.replace(/\s+/g, '').trim(); + const parseVersionFromRaw = (raw: any): number | null => { + if (typeof raw === 'number' && Number.isFinite(raw)) return raw; + const s = typeof raw === 'string' ? raw : extractText(raw); + const m = (s || '').match(/\d+(?:\.\d+)?/); + return m ? parseFloat(m[0]) : null; + }; - for (const fv of fieldValues) { - const recordId = fv?.recordId; - const raw = fv?.value; - let v: number | null = null; - if (typeof raw === 'number') { - v = raw; - } else { - const s = typeof raw === 'string' ? raw : extractText(raw); - const m = (s || '').match(/\d+(?:\.\d+)?/); - if (m) v = parseFloat(m[0]); - } - if (v !== null && Math.abs(v - planVersion) < eps) { - return recordId || null; - } - } + { + try { + let token: any = undefined; + for (let i = 0; i < 10000; i++) { + const res: any = await deliveryTable.getRecordsByPage({ + pageSize: 200, + pageToken: token, + filter: planFilter, + sort, + }); + const recs: any[] = Array.isArray(res?.records) ? res.records : []; - const nextToken = res?.pageToken; - const hasMore = !!res?.hasMore; - token = nextToken; - if (!hasMore && !nextToken) break; + for (const r of recs) { + const recordId = r?.recordId || r?.id || null; + if (!recordId) continue; + + const recordPlanRaw = (r?.fields || {})[DELIVERY_RECORD_IDS_FIELD_ID]; + const recordPlanText = typeof recordPlanRaw === 'string' ? recordPlanRaw : extractText(recordPlanRaw); + if (!recordPlanText) continue; + + if (normalizePlanText(recordPlanText) !== normalizePlanText(planText)) continue; + + const recordVersionRaw = (r?.fields || {})[DELIVERY_VERSION_FIELD_ID]; + const recordVersion = parseVersionFromRaw(recordVersionRaw); + if (recordVersion !== null && Math.abs(recordVersion - planVersion) < eps) { + return recordId; + } + } + + const nextToken = res?.pageToken; + const hasMore = !!res?.hasMore; + token = nextToken; + if (!hasMore && !nextToken) break; + } + } catch {} + } + + { + try { + let token: any = undefined; + for (let i = 0; i < 10000; i++) { + const res: any = await versionField.getFieldValueListByPage({ + pageSize: 200, + pageToken: token, + filter: planFilter, + sort, + stringValue: true + }); + const fieldValues: any[] = Array.isArray(res?.fieldValues) ? res.fieldValues : []; + + for (const fv of fieldValues) { + const recordId = fv?.recordId; + const v = parseVersionFromRaw(fv?.value); + if (v !== null && Math.abs(v - planVersion) < eps) { + return recordId || null; + } + } + + const nextToken = res?.pageToken; + const hasMore = !!res?.hasMore; + token = nextToken; + if (!hasMore && !nextToken) break; + } + } catch {} + } + + { + try { + let token: any = undefined; + for (let i = 0; i < 10000; i++) { + const res: any = await deliveryTable.getRecordsByPage({ + pageSize: 200, + pageToken: token, + sort, + }); + const recs: any[] = Array.isArray(res?.records) ? res.records : []; + + for (const r of recs) { + const recordId = r?.recordId || r?.id || null; + if (!recordId) continue; + + const recordPlanRaw = (r?.fields || {})[DELIVERY_RECORD_IDS_FIELD_ID]; + const recordPlanText = typeof recordPlanRaw === 'string' ? recordPlanRaw : extractText(recordPlanRaw); + if (!recordPlanText) continue; + + if (normalizePlanText(recordPlanText) !== normalizePlanText(planText)) continue; + + const recordVersionRaw = (r?.fields || {})[DELIVERY_VERSION_FIELD_ID]; + const recordVersion = parseVersionFromRaw(recordVersionRaw); + if (recordVersion !== null && Math.abs(recordVersion - planVersion) < eps) { + return recordId; + } + } + + const nextToken = res?.pageToken; + const hasMore = !!res?.hasMore; + token = nextToken; + if (!hasMore && !nextToken) break; + } + } catch {} } return null; } catch (error) { @@ -1986,6 +2298,8 @@ export default function App() { const currentExpectedDate = overrideData?.expectedDate || expectedDate; const currentStartTime = overrideData?.startTime || startTime; const currentExcludedDates = Array.isArray(overrideData?.excludedDates) ? overrideData!.excludedDates : []; + const globalExcludedDates = normalizeExcludedDatesOverride(currentExcludedDates); + const excludedByNode = excludedDatesByNodeOverrideRef.current || {}; const isBackward = timelineDirection === 'backward'; console.log('=== handleCalculateTimeline - 使用的数据 ==='); @@ -2381,7 +2695,16 @@ export default function App() { for (let i = 0; i < nodesToProcess.length; i++) { const processNode = nodesToProcess[i]; - const nodeExcludedDates = Array.from(new Set([...(processNode.excludedDates || []), ...currentExcludedDates])).filter(Boolean); + const nodeKey = buildExcludedDatesNodeKey(processNode?.nodeName, processNode?.processOrder, i); + const overrideList = Object.prototype.hasOwnProperty.call(excludedByNode, nodeKey) + ? excludedByNode[nodeKey] + : undefined; + const baseList = Array.isArray(processNode?.excludedDates) ? processNode.excludedDates : []; + const selectedList = Array.isArray(overrideList) ? overrideList : baseList; + const nodeExcludedDates = Array.from(new Set([ + ...normalizeExcludedDatesOverride(selectedList), + ...globalExcludedDates, + ])).filter(Boolean); let timelineValue = null; let matchedTimelineRecord = null; @@ -3479,6 +3802,15 @@ export default function App() { } }, [actualCompletionDates, isRestoringSnapshot]); + useEffect(() => { + if (!pendingRecalculateAfterExcludedDatesRef.current) return; + if (isRestoringSnapshot) return; + pendingRecalculateAfterExcludedDatesRef.current = false; + if (timelineResults.length > 0) { + recalculateTimeline(timelineAdjustments, true); + } + }, [timelineResults, isRestoringSnapshot]); + // 捕获初始状态快照(在首次生成/还原出完整时间线后) useEffect(() => { if (!hasCapturedInitialSnapshotRef.current && timelineResults.length > 0) { @@ -3821,6 +4153,8 @@ export default function App() { text2, mode, timelineDirection, + excludedDatesOverride, + excludedDatesByNodeOverride: excludedDatesByNodeOverrideRef.current || excludedDatesByNodeOverride, lockedExpectedDeliveryDateTs, isExpectedDeliveryDateLocked, selectedLabels: currentSelectedLabels, @@ -3863,7 +4197,9 @@ export default function App() { totalNodes: timelineResults.length, hasValidResults: timelineResults.length > 0, lastCalculationMode: mode, - timelineDirection + timelineDirection, + excludedDatesOverride, + excludedDatesByNodeOverride: excludedDatesByNodeOverrideRef.current || excludedDatesByNodeOverride }, totalNodes: timelineResults.length, isGlobalSnapshot: true @@ -5362,6 +5698,8 @@ export default function App() { setMode(next); setIsExpectedDeliveryDateLocked(false); setLockedExpectedDeliveryDateTs(null); + setExcludedDatesOverride([]); + setExcludedDatesOverrideText(''); }} optionList={[ { value: 'generate', label: '生成流程日期' }, @@ -5645,6 +5983,120 @@ export default function App() { })()} + + + 共 {Array.isArray(timelineResults) ? timelineResults.length : 0} 个节点, + 已配置 {Object.values(excludedDatesByNodeDraft || {}).filter(v => Array.isArray(v) && v.length > 0).length} 个节点 + + + + + + + } + style={{ width: 980 }} + bodyStyle={{ maxHeight: '70vh', overflowY: 'auto' }} + > +
+ {(Array.isArray(timelineResults) ? timelineResults : []).map((r: any, idx: number) => { + const nodeKey = buildExcludedDatesNodeKey(r?.nodeName, r?.processOrder, idx); + const dates = Array.isArray(excludedDatesByNodeDraft?.[nodeKey]) ? excludedDatesByNodeDraft[nodeKey] : []; + const groups = groupDatesByMonth(dates); + const pickerValue = excludedDatesAddDraft?.[nodeKey] ?? null; + return ( + +
+ + {typeof r?.processOrder !== 'undefined' ? `#${r.processOrder} ` : ''}{r?.nodeName || nodeKey} + + + { + let d: Date | null = null; + if (v instanceof Date) d = v; + else if (typeof v === 'string') { + const parsed = parseDate(v); + d = parsed && !isNaN(parsed.getTime()) ? parsed : null; + } + setExcludedDatesAddDraft(prev => ({ ...(prev || {}), [nodeKey]: d })); + }} + /> + + + +
+ +
+ {groups.length === 0 ? ( + 无跳过日期 + ) : ( + groups.map(g => ( +
+ {g.month} +
+ {g.dates.map(d => ( + removeExcludedDateFromDraft(nodeKey, d)} + style={{ backgroundColor: '#fff7e6', borderColor: '#ffd591', color: '#ad4e00' }} + > + {d} + + ))} +
+
+ )) + )} +
+
+ ); + })} +
+
+ {/* 时效计算结果模态框 */} +
+ + 跳过日期 + + + 已配置 {Object.values(excludedDatesByNodeOverride || {}).filter(v => Array.isArray(v) && v.length > 0).length} 个节点 + + + + +
{Object.keys(timelineAdjustments).length > 0 && (
当前调整: