From b81b4afae43b0d920f7b14d84aea7b57f32263d5 Mon Sep 17 00:00:00 2001 From: mairuiming Date: Thu, 29 Jan 2026 10:27:20 +0800 Subject: [PATCH] 1 1 --- src/App.tsx | 971 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 871 insertions(+), 100 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e8d7842..06b2e68 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -34,6 +34,74 @@ const extractText = (val: any) => { }; export default function App() { + type ProcessGroupInstance = { + id: string; + groupName: string; + displayName: string; + }; + + const nextProcessGroupInstanceIdRef = useRef(1); + const createProcessGroupInstance = (groupName: string, displayName?: string): ProcessGroupInstance => { + const name = (groupName || '').trim(); + const id = `${Date.now()}_${nextProcessGroupInstanceIdRef.current++}`; + return { id, groupName: name, displayName: (displayName || name).trim() || name }; + }; + + const deriveGroupOrderDraftFromTimelineResults = (results: any[]): ProcessGroupInstance[] => { + const arr = Array.isArray(results) ? results : []; + const seen = new Set(); + const out: ProcessGroupInstance[] = []; + for (const r of arr) { + const g = (typeof r?.processGroup === 'string' ? r.processGroup : extractText(r?.processGroup)).trim(); + if (!g || seen.has(g)) continue; + seen.add(g); + out.push(createProcessGroupInstance(g)); + } + return out; + }; + + const applyGroupOrderConfigToTimelineResults = (results: any[], config: ProcessGroupInstance[]) => { + const base = Array.isArray(results) ? results : []; + const order = Array.isArray(config) ? config : []; + if (base.length === 0 || order.length === 0) return base; + + const configuredBaseNames = new Set(); + for (const inst of order) { + const bn = (inst?.groupName || '').trim(); + if (bn) configuredBaseNames.add(bn); + } + + const baseNameToNodes = new Map(); + for (const r of base) { + const bn = (typeof r?.processGroup === 'string' ? r.processGroup : extractText(r?.processGroup)).trim(); + if (!bn) continue; + if (!baseNameToNodes.has(bn)) baseNameToNodes.set(bn, []); + baseNameToNodes.get(bn)!.push(r); + } + + const expanded: any[] = []; + for (const inst of order) { + const bn = (inst?.groupName || '').trim(); + if (!bn) continue; + const nodes = baseNameToNodes.get(bn) || []; + for (const n of nodes) { + expanded.push({ + ...n, + processGroupBase: bn, + processGroupInstanceId: inst.id, + processGroupInstanceName: inst.displayName, + }); + } + } + + const leftovers = base.filter(r => { + const bn = (typeof r?.processGroup === 'string' ? r.processGroup : extractText(r?.processGroup)).trim(); + return bn ? !configuredBaseNames.has(bn) : true; + }); + + return expanded.concat(leftovers); + }; + const [selectedRecords, setSelectedRecords] = useState([]); const [recordDetails, setRecordDetails] = useState([]); const [loading, setLoading] = useState(false); @@ -54,9 +122,22 @@ export default function App() { const [excludedDatesByNodeOverride, setExcludedDatesByNodeOverride] = useState>({}); const excludedDatesByNodeOverrideRef = useRef>({}); const pendingRecalculateAfterExcludedDatesRef = useRef(false); + const pendingRecalculateAfterCalculateRef = useRef(false); + const pendingRecalculateAfterCalculateAdjustmentsRef = useRef | null>(null); const [excludedDatesAdjustVisible, setExcludedDatesAdjustVisible] = useState(false); const [excludedDatesByNodeDraft, setExcludedDatesByNodeDraft] = useState>({}); const [excludedDatesAddDraft, setExcludedDatesAddDraft] = useState>({}); + const pendingGroupConfigCalcRef = useRef<{ + overrideData?: { + selectedRecords?: string[], + recordDetails?: any[], + selectedLabels?: { [key: string]: string | string[] }, + expectedDate?: Date | null, + startTime?: Date | null, + excludedDates?: string[] + }, + showUI: boolean + } | null>(null); // 客户期望日期状态 const [expectedDate, setExpectedDate] = useState(null); @@ -70,6 +151,10 @@ export default function App() { const [allocationDraft, setAllocationDraft] = useState<{[key: number]: number}>({}); const [allocationNodesSnapshot, setAllocationNodesSnapshot] = useState([]); const [allocationExcluded, setAllocationExcluded] = useState<{[key: number]: boolean}>({}); + const [groupOrderConfig, setGroupOrderConfig] = useState([]); + const [groupConfigVisible, setGroupConfigVisible] = useState(false); + const [groupOrderDraft, setGroupOrderDraft] = useState([]); + const [draggingGroupIndex, setDraggingGroupIndex] = useState(null); // 预览相关状态(已移除未使用的 previewLoading 状态) @@ -77,7 +162,7 @@ export default function App() { const [timelineVisible, setTimelineVisible] = useState(false); const [timelineLoading, setTimelineLoading] = useState(false); const [timelineResults, setTimelineResults] = useState([]); - const [timelineAdjustments, setTimelineAdjustments] = useState<{[key: number]: number}>({}); + const [timelineAdjustments, setTimelineAdjustments] = useState>({}); // 交期余量扣减状态:记录从交期余量中扣减的天数 const [deliveryMarginDeductions, setDeliveryMarginDeductions] = useState(0); // 最后流程完成日期调整状态:记录最后流程完成日期增加的天数 @@ -186,6 +271,12 @@ export default function App() { setCurrentDeliveryRecordId(null); setLabelAdjustmentFlow(false); + setGroupOrderConfig([]); + setGroupConfigVisible(false); + setGroupOrderDraft([]); + setDraggingGroupIndex(null); + pendingGroupConfigCalcRef.current = null; + // 可选:重置模式 if (opts?.resetMode) { setMode(null); @@ -242,26 +333,36 @@ export default function App() { useEffect(() => { excludedDatesByNodeOverrideRef.current = excludedDatesByNodeOverride || {}; }, [excludedDatesByNodeOverride]); + + useEffect(() => { + if (!groupConfigVisible && groupOrderConfig.length > 0 && pendingGroupConfigCalcRef.current) { + const payload = pendingGroupConfigCalcRef.current; + pendingGroupConfigCalcRef.current = null; + setTimelineLoading(true); + handleCalculateTimeline(true, payload?.overrideData, payload?.showUI ?? true); + } + }, [groupConfigVisible, groupOrderConfig]); + 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 EXCLUDED_DATES_FIELD_ID = 'fldGxzC5uG'; // 不参与计算日期(多选,格式:yyyy-MM-dd) + const NODE_NAME_FIELD_ID = 'fld0g9L9Fw'; + const PROCESS_LABEL_FIELD_ID = 'fldrVTa23X'; + const PROCESS_ORDER_FIELD_ID = 'fldbfJQ4Zs'; + const WEEKEND_DAYS_FIELD_ID = 'fld2BvjbIN'; + const START_DATE_RULE_FIELD_ID = 'fld0KsQ2j3'; + const DATE_ADJUSTMENT_RULE_FIELD_ID = 'fld0KsQ2j3'; + const EXCLUDED_DATES_FIELD_ID = 'fldGxzC5uG'; + const PROCESS_GROUP_FIELD_ID = 'fldtjxS4oO'; - // 时效数据表相关常量 const TIMELINE_TABLE_ID = 'tblPnQscqwqopJ8V'; // 时效数据表ID const TIMELINE_FIELD_ID = 'fldniJmZe3'; // 时效值字段ID const TIMELINE_NODE_FIELD_ID = 'fldeIZzokl'; // 时效表中的节点名称字段ID const CALCULATION_METHOD_FIELD_ID = 'fldxfLZNUu'; // 时效计算方式字段ID + const TIMELINE_LABEL12_FIELD_ID = 'fldZZmb2LV'; // 标签12(时效数据表) // 新表ID(批量生成表) const BATCH_TABLE_ID = 'tblXO7iSxBYxrqtY'; @@ -446,22 +547,210 @@ export default function App() { setExcludedDatesOverride(normalizeExcludedDatesOverride(text)); }; - const buildExcludedDatesNodeKey = (nodeName: any, processOrder: any, indexFallback?: number) => { + const buildExcludedDatesNodeKey = (nodeName: any, processOrder: any, indexFallback?: number, instanceKey?: any) => { 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; + const inst = (typeof instanceKey === 'string' ? instanceKey : extractText(instanceKey)).trim(); + if (name && order) return inst ? `${order}::${name}::${inst}` : `${order}::${name}`; + if (name) return inst ? `${name}::${inst}` : name; if (indexFallback !== undefined) return `#${indexFallback + 1}`; return '#unknown'; }; + const buildTimelineAdjustmentKey = (node: any, indexFallback?: number) => { + return buildExcludedDatesNodeKey( + node?.nodeName, + node?.processOrder, + indexFallback, + node?.processGroupInstanceId || node?.processGroupInstanceName + ); + }; + + const normalizeTimelineAdjustmentsFromSnapshot = (raw: any, resultsFromSnapshot?: any[]): Record => { + if (!raw || typeof raw !== 'object') return {}; + + const entries = Object.entries(raw) + .map(([k, v]) => [String(k).trim(), Number(v)] as const) + .filter(([k, v]) => k && Number.isFinite(v) && v !== 0); + + if (entries.length === 0) return {}; + + const snapshotResults = Array.isArray(resultsFromSnapshot) ? resultsFromSnapshot : []; + const numericKeys: number[] = []; + for (const [k] of entries) { + const n = Number(k); + if (Number.isInteger(n)) numericKeys.push(n); + } + + if (snapshotResults.length > 0 && numericKeys.length > 0) { + const byProcessOrder = new Map(); + for (let i = 0; i < snapshotResults.length; i++) { + const r = snapshotResults[i]; + const orderRaw = r?.processOrder; + const order = typeof orderRaw === 'number' + ? orderRaw + : (typeof orderRaw === 'string' ? Number(orderRaw) : NaN); + if (!Number.isFinite(order)) continue; + if (!byProcessOrder.has(order)) byProcessOrder.set(order, { node: r, idx: i }); + } + + const eps = 1e-6; + const indexMatchHits = (() => { + let hits = 0; + for (const [k, v] of entries) { + const n = Number(k); + if (!Number.isInteger(n)) continue; + if (n < 0 || n >= snapshotResults.length) continue; + const r = snapshotResults[n]; + const adj = Number(r?.adjustment); + if (Number.isFinite(adj) && Math.abs(adj - v) < eps) { + hits++; + continue; + } + const base = Number(r?.timelineValue); + const adjusted = Number(r?.adjustedTimelineValue); + if (Number.isFinite(base) && Number.isFinite(adjusted) && Math.abs((adjusted - base) - v) < eps) { + hits++; + continue; + } + } + return hits; + })(); + + const processOrderHits = numericKeys.filter(k => k !== 0 && byProcessOrder.has(k)).length; + const shouldTreatAsIndex = indexMatchHits >= Math.ceil(numericKeys.length / 2); + const shouldTreatAsProcessOrder = !shouldTreatAsIndex && processOrderHits >= Math.ceil(numericKeys.length / 2); + + const out: Record = {}; + for (const [k, v] of entries) { + const n = Number(k); + if (!Number.isInteger(n)) { + out[k] = Math.round(v * 100) / 100; + continue; + } + + if (shouldTreatAsIndex && n >= 0 && n < snapshotResults.length) { + out[buildTimelineAdjustmentKey(snapshotResults[n], n)] = Math.round(v * 100) / 100; + continue; + } + + if (shouldTreatAsProcessOrder) { + const hit = byProcessOrder.get(n); + if (hit) { + out[buildTimelineAdjustmentKey(hit.node, hit.idx)] = Math.round(v * 100) / 100; + continue; + } + } + + if (n >= 0 && n < snapshotResults.length) { + out[buildTimelineAdjustmentKey(snapshotResults[n], n)] = Math.round(v * 100) / 100; + continue; + } + + out[k] = Math.round(v * 100) / 100; + } + + return out; + } + + const out: Record = {}; + for (const [k, v] of entries) { + out[k] = Math.round((v as number) * 100) / 100; + } + return out; + }; + + const deriveTimelineAdjustmentsFromResults = (resultsFromSnapshot: any[]): Record => { + const results = Array.isArray(resultsFromSnapshot) ? resultsFromSnapshot : []; + const out: Record = {}; + for (let i = 0; i < results.length; i++) { + const r = results[i]; + const v = Number(r?.adjustment); + if (!Number.isFinite(v) || v === 0) continue; + const key = buildTimelineAdjustmentKey(r, i); + out[key] = Math.round(v * 100) / 100; + } + return out; + }; + + const tryParseProcessOrderFromKey = (nodeKey: string): number | null => { + const key = String(nodeKey || '').trim(); + const idx = key.indexOf('::'); + if (idx <= 0) return null; + const left = key.slice(0, idx).trim(); + const order = Number(left); + if (!Number.isFinite(order)) return null; + return order; + }; + + const extractNodeNameFromKey = (nodeKey: string): string => { + const key = String(nodeKey || '').trim(); + const idx = key.indexOf('::'); + if (idx < 0) return key; + return key.slice(idx + 2).trim(); + }; + + const normalizeNodeNameForMatch = (name: any): string => { + const raw = (typeof name === 'string' ? name : extractText(name)).trim(); + if (!raw) return ''; + return raw + .replace(/[\s::\-_.()()【】\[\]<>《》]/g, '') + .toLowerCase(); + }; + + const remapTimelineAdjustmentsToNewResults = ( + prevResults: any[], + nextResults: any[] + ): Record => { + const out: Record = {}; + const prevArr = Array.isArray(prevResults) ? prevResults : []; + const prevAdjustmentByName = new Map(); + for (let i = 0; i < prevArr.length; i++) { + const r = prevArr[i]; + const name = (typeof r?.nodeName === 'string' ? r.nodeName : extractText(r?.nodeName)).trim(); + if (!name) continue; + const rowAdj = Number(r?.adjustment); + if (Number.isFinite(rowAdj) && rowAdj !== 0) { + const existed = prevAdjustmentByName.get(name); + if (existed === undefined || existed === 0) prevAdjustmentByName.set(name, Math.round(rowAdj * 100) / 100); + continue; + } + const base = Number(r?.timelineValue); + const adjusted = Number(r?.adjustedTimelineValue); + if (Number.isFinite(base) && Number.isFinite(adjusted)) { + const d = adjusted - base; + if (Number.isFinite(d) && d !== 0) { + const existed = prevAdjustmentByName.get(name); + if (existed === undefined || existed === 0) prevAdjustmentByName.set(name, Math.round(d * 100) / 100); + } + } + } + + const nextArr = Array.isArray(nextResults) ? nextResults : []; + for (let i = 0; i < nextArr.length; i++) { + const node = nextArr[i]; + const name = (typeof node?.nodeName === 'string' ? node.nodeName : extractText(node?.nodeName)).trim(); + if (!name) continue; + const adj = prevAdjustmentByName.get(name); + if (adj === undefined || adj === 0) continue; + out[buildTimelineAdjustmentKey(node, i)] = Math.round(adj * 100) / 100; + } + + return out; + }; + 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 key = buildExcludedDatesNodeKey( + r?.nodeName, + r?.processOrder, + i, + r?.processGroupInstanceId || r?.processGroupInstanceName + ); const arr = Array.isArray(r?.excludedDates) ? r.excludedDates : []; map[key] = normalizeExcludedDatesOverride(arr); } @@ -536,7 +825,12 @@ export default function App() { const draft: Record = {}; for (let i = 0; i < baseResults.length; i++) { const r = baseResults[i]; - const key = buildExcludedDatesNodeKey(r?.nodeName, r?.processOrder, i); + const key = buildExcludedDatesNodeKey( + r?.nodeName, + r?.processOrder, + i, + r?.processGroupInstanceId || r?.processGroupInstanceName + ); const existing = excludedDatesByNodeOverrideRef.current?.[key]; const fromResult = Array.isArray(r?.excludedDates) ? r.excludedDates : []; draft[key] = normalizeExcludedDatesOverride(existing ?? fromResult); @@ -690,7 +984,12 @@ export default function App() { if (vNum !== null && !isNaN(vNum)) setCurrentVersionNumber(vNum); } - if (snapshot.timelineAdjustments) setTimelineAdjustments(snapshot.timelineAdjustments); + { + const normalized = snapshot.timelineAdjustments + ? normalizeTimelineAdjustmentsFromSnapshot(snapshot.timelineAdjustments, snapshot?.timelineResults) + : deriveTimelineAdjustmentsFromResults(snapshot?.timelineResults); + setTimelineAdjustments(normalized); + } { const snapLockedFlag = snapshot?.isExpectedDeliveryDateLocked; const snapLockedTs = snapshot?.lockedExpectedDeliveryDateTs; @@ -855,7 +1154,12 @@ export default function App() { if (vNum !== null && !isNaN(vNum)) setCurrentVersionNumber(vNum); } - if (snapshot.timelineAdjustments) setTimelineAdjustments(snapshot.timelineAdjustments); + { + const normalized = snapshot.timelineAdjustments + ? normalizeTimelineAdjustmentsFromSnapshot(snapshot.timelineAdjustments, snapshot?.timelineResults) + : deriveTimelineAdjustmentsFromResults(snapshot?.timelineResults); + setTimelineAdjustments(normalized); + } { const snapLockedFlag = snapshot?.isExpectedDeliveryDateLocked; const snapLockedTs = snapshot?.lockedExpectedDeliveryDateTs; @@ -1009,7 +1313,12 @@ export default function App() { } if (vNum !== null && !isNaN(vNum)) setCurrentVersionNumber(vNum); } - if (snapshot.timelineAdjustments) setTimelineAdjustments(snapshot.timelineAdjustments); + { + const normalized = snapshot.timelineAdjustments + ? normalizeTimelineAdjustmentsFromSnapshot(snapshot.timelineAdjustments, snapshot?.timelineResults) + : deriveTimelineAdjustmentsFromResults(snapshot?.timelineResults); + setTimelineAdjustments(normalized); + } restoreExcludedDatesOverrideFromSnapshot(snapshot, snapshot.timelineResults); restoreExcludedDatesByNodeOverrideFromSnapshot(snapshot, snapshot.timelineResults); if (snapshot.expectedDateTimestamp) { @@ -1316,6 +1625,7 @@ export default function App() { restoreBaseBufferDaysFromSnapshot(globalSnapshotData); restoreExcludedDatesOverrideFromSnapshot(globalSnapshotData, nodeSnapshots); restoreExcludedDatesByNodeOverrideFromSnapshot(globalSnapshotData, nodeSnapshots); + setTimelineAdjustments(deriveTimelineAdjustmentsFromResults(nodeSnapshots)); setTimelineResults(nodeSnapshots); setTimelineVisible(true); @@ -1473,6 +1783,7 @@ export default function App() { const PROCESS_TIMELINESS_FIELD_ID = 'fldEYCXnWt'; // 时效字段(天) const PROCESS_STYLE_FIELD_ID = 'fld8xVqHJW'; // 款式字段(流程数据表) const PROCESS_COLOR_FIELD_ID = 'fld3F1zGYe'; // 颜色字段(流程数据表) + const PROCESS_GROUP_FIELD_ID_DATA = 'fldq6PTX5F'; // 流程组字段(流程数据表,单选) // 货期记录表相关常量 const DELIVERY_RECORD_TABLE_ID = 'tblwiA49gksQrnfg'; // 货期记录表ID @@ -1825,6 +2136,31 @@ export default function App() { return result; }; + const hasNonEmptyRuleValue = (value: any): boolean => { + if (value === null || value === undefined) return false; + if (typeof value === 'string') return value.trim().length > 0; + if (typeof value === 'number') return true; + if (Array.isArray(value)) return value.some(item => hasNonEmptyRuleValue(item)); + if (typeof value === 'object') { + const v: any = value; + if (typeof v.text === 'string') return v.text.trim().length > 0; + if (typeof v.name === 'string') return v.name.trim().length > 0; + if (v.value !== undefined) return hasNonEmptyRuleValue(v.value); + return Object.keys(v).length > 0; + } + return Boolean(value); + }; + + const setTimeOfDay = (date: Date, hour: number, minute: number): Date => { + const result = new Date(date); + result.setHours(hour, minute, 0, 0); + return result; + }; + + const shouldForceEndTimeTo18 = (nodeLike: any): boolean => { + return hasNonEmptyRuleValue(nodeLike?.startDateRule) || hasNonEmptyRuleValue(nodeLike?.dateAdjustmentRule); + }; + // 内部工作时间计算函数 const addInternalBusinessTime = (startDate: Date, businessDays: number, weekendDays: number[] = [], excludedDates: string[] = []): Date => { const result = new Date(startDate); @@ -1979,35 +2315,60 @@ export default function App() { const options: {[key: string]: any[]} = {}; // 初始化标签的选项数组 - for (let i = 1; i <= 11; i++) { + for (let i = 1; i <= 12; i++) { options[`标签${i}`] = []; } + + const getSelectFieldOptions = async (table: any, fieldId: string): Promise => { + const field = await table.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(); + return (fieldOptions || []) + .filter((option: any) => option && typeof option.name === 'string') + .map((option: any) => ({ label: option.name, value: option.name })); + } + const records = await fetchAllRecordsByPage(table); + const values = new Set(); + for (const r of records) { + const raw = (r?.fields || {})[fieldId]; + const tokens = Array.isArray(raw) ? raw : [raw]; + for (const t of tokens) { + const s = extractText(t)?.trim(); + if (s) values.add(s); + } + } + return Array.from(values) + .sort((a, b) => a.localeCompare(b, 'zh-Hans-CN')) + .map(v => ({ label: v, value: v })); + }; // 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 || []) - .filter((option: any) => option && typeof option.name === 'string') - .map((option: any) => ({ - label: option.name, - value: option.name - })); - } + options[labelKey] = await getSelectFieldOptions(labelTable, fieldId); } catch (error) { console.warn(`获取${labelKey}字段选项失败:`, error); // 如果获取选项失败,保持空数组 options[labelKey] = []; } } + + try { + const timelineTable = await bitable.base.getTable(TIMELINE_TABLE_ID); + const label12Options = await getSelectFieldOptions(timelineTable, TIMELINE_LABEL12_FIELD_ID); + const unique = new Map(); + for (const opt of label12Options) { + if (opt && typeof opt.value === 'string' && opt.value.trim()) { + unique.set(opt.value.trim(), { label: opt.value.trim(), value: opt.value.trim() }); + } + } + options['标签12'] = Array.from(unique.values()).sort((a, b) => a.value.localeCompare(b.value, 'zh-Hans-CN')); + } catch (error) { + console.warn('获取标签12字段选项失败:', error); + options['标签12'] = []; + } console.log('处理后的标签选项:', options); @@ -2340,6 +2701,12 @@ export default function App() { { const requiredLabelKeys = Array.from({ length: 10 }, (_, i) => `标签${i + 1}`); + const label7Val = (currentSelectedLabels as any)['标签7']; + const label7Values = Array.isArray(label7Val) + ? label7Val.map((s: any) => String(s ?? '').trim()).filter(Boolean) + : (typeof label7Val === 'string' ? [label7Val.trim()].filter(Boolean) : []); + const shouldRequireLabel12 = label7Values.length > 0 && !(label7Values.length === 1 && label7Values[0] === '无(二次工艺)'); + if (shouldRequireLabel12) requiredLabelKeys.push('标签12'); const missing = requiredLabelKeys.filter(key => { const val = (currentSelectedLabels as any)[key]; if (Array.isArray(val)) return val.length === 0; @@ -2473,7 +2840,7 @@ export default function App() { if (!fieldMeta || typeof (fieldMeta as any).name !== 'string' || typeof (fieldMeta as any).id !== 'string') { continue; } - const match = (fieldMeta as any).name.match(/^标签([1-9]|10)$/); + const match = (fieldMeta as any).name.match(/^标签([1-9]|10|12)$/); if (match) { const labelKey = `标签${match[1]}`; timelineLabelFields[labelKey] = (fieldMeta as any).id; @@ -2500,7 +2867,15 @@ export default function App() { const processLabels = fields[PROCESS_LABEL_FIELD_ID]; const nodeName = fields[NODE_NAME_FIELD_ID]; const processOrder = fields[PROCESS_ORDER_FIELD_ID]; // 获取流程顺序 - + const processGroup = fields[PROCESS_GROUP_FIELD_ID]; + let processGroupText = ''; + if (typeof processGroup === 'string') { + processGroupText = processGroup; + } else { + processGroupText = extractText(processGroup); + } + processGroupText = (processGroupText || '').trim(); + if (!processLabels || !nodeName) continue; // 处理流程配置表中的标签数据 - 修复多选字段处理 @@ -2555,15 +2930,7 @@ export default function App() { for (const value of valuesToCheck) { // 检查用户选择的值是否在流程配置的任何一个标签选项中 const isValueMatched = processLabelTexts.some(processLabelText => { - // 直接匹配 - if (processLabelText === value) { - return true; - } - // 包含匹配(用于处理可能的格式差异) - if (processLabelText.includes(value)) { - return true; - } - return false; + return String(processLabelText ?? '').trim() === String(value ?? '').trim(); }); if (isValueMatched) { @@ -2662,15 +3029,87 @@ export default function App() { weekendDays: weekendDays, // 添加休息日配置 excludedDates: excludedDates, // 添加自定义跳过日期 startDateRule: startDateRule, // 添加起始日期调整规则 - dateAdjustmentRule: dateAdjustmentRule // 添加JSON格式日期调整规则 + dateAdjustmentRule: dateAdjustmentRule, // 添加JSON格式日期调整规则 + processGroup: processGroupText }); } } - // 按流程顺序排序 - matchedProcessNodes.sort((a, b) => a.processOrder - b.processOrder); - - console.log('按顺序排列的流程节点:', matchedProcessNodes); + const allGroups = Array.from( + new Set( + matchedProcessNodes + .map(n => (n.processGroup || '').trim()) + .filter(g => g && g.length > 0) + ) + ); + + if (allGroups.length > 1 && groupOrderConfig.length === 0 && showUI) { + const initial = allGroups.map(g => createProcessGroupInstance(g)); + setGroupOrderDraft(initial); + setGroupConfigVisible(true); + setTimelineLoading(false); + pendingGroupConfigCalcRef.current = { + overrideData: { + selectedRecords: currentSelectedRecords, + recordDetails: currentRecordDetails, + selectedLabels: currentSelectedLabels, + expectedDate: currentExpectedDate, + startTime: currentStartTime, + excludedDates: currentExcludedDates, + }, + showUI, + }; + return []; + } + + let orderedProcessNodes: any[] = matchedProcessNodes; + + if (groupOrderConfig.length > 0) { + const configuredBaseNames = new Set(); + for (const inst of groupOrderConfig) { + const base = (inst?.groupName || '').trim(); + if (base) configuredBaseNames.add(base); + } + + const baseNameToNodes = new Map(); + for (const n of matchedProcessNodes) { + const base = (n?.processGroup || '').trim(); + if (!base) continue; + if (!baseNameToNodes.has(base)) baseNameToNodes.set(base, []); + baseNameToNodes.get(base)!.push(n); + } + for (const [base, list] of baseNameToNodes.entries()) { + list.sort((a, b) => (Number(a?.processOrder) || 0) - (Number(b?.processOrder) || 0)); + baseNameToNodes.set(base, list); + } + + const expanded: any[] = []; + for (const inst of groupOrderConfig) { + const base = (inst?.groupName || '').trim(); + if (!base) continue; + const nodesInGroup = baseNameToNodes.get(base) || []; + for (const n of nodesInGroup) { + expanded.push({ + ...n, + processGroupBase: base, + processGroupInstanceId: inst.id, + processGroupInstanceName: inst.displayName, + }); + } + } + + const leftovers = matchedProcessNodes.filter(n => { + const base = (n?.processGroup || '').trim(); + return base ? !configuredBaseNames.has(base) : true; + }); + leftovers.sort((a, b) => (Number(a?.processOrder) || 0) - (Number(b?.processOrder) || 0)); + + orderedProcessNodes = expanded.concat(leftovers); + } else { + orderedProcessNodes = [...matchedProcessNodes].sort((a, b) => (Number(a?.processOrder) || 0) - (Number(b?.processOrder) || 0)); + } + + console.log('按顺序排列的流程节点:', orderedProcessNodes); // 2. 优化:预先获取所有时效数据并建立索引 const timelineRecords = await fetchAllRecordsByPage(timelineTable); @@ -2721,11 +3160,16 @@ export default function App() { let cumulativeTime = isBackward ? new Date(currentExpectedDate as Date) : (currentStartTime ? new Date(currentStartTime) : new Date()); - const nodesToProcess = isBackward ? [...matchedProcessNodes].reverse() : matchedProcessNodes; + const nodesToProcess = isBackward ? [...orderedProcessNodes].reverse() : orderedProcessNodes; for (let i = 0; i < nodesToProcess.length; i++) { const processNode = nodesToProcess[i]; - const nodeKey = buildExcludedDatesNodeKey(processNode?.nodeName, processNode?.processOrder, i); + const nodeKey = buildExcludedDatesNodeKey( + processNode?.nodeName, + processNode?.processOrder, + i, + processNode?.processGroupInstanceId || processNode?.processGroupInstanceName + ); const overrideList = Object.prototype.hasOwnProperty.call(excludedByNode, nodeKey) ? excludedByNode[nodeKey] : undefined; @@ -2977,6 +3421,7 @@ export default function App() { let nodeEndTime: Date; let ruleDescription = ''; let timelineResult: { startDate: string; endDate: string }; + const forceEndTimeTo18 = shouldForceEndTimeTo18(processNode); if (!isBackward) { nodeStartTime = new Date(cumulativeTime); @@ -3037,8 +3482,19 @@ export default function App() { } else { nodeEndTime = new Date(nodeStartTime); } + + if (forceEndTimeTo18 && nodeEndTime && !isNaN(nodeEndTime.getTime())) { + nodeEndTime = setTimeOfDay(nodeEndTime, 18, 0); + timelineResult = { + ...timelineResult, + endDate: timelineResult.endDate.includes('未找到') ? timelineResult.endDate : formatDate(nodeEndTime, 'STORAGE_FORMAT') + }; + } } else { nodeEndTime = new Date(cumulativeTime); + if (forceEndTimeTo18 && nodeEndTime && !isNaN(nodeEndTime.getTime())) { + nodeEndTime = setTimeOfDay(nodeEndTime, 18, 0); + } if (timelineValue) { if (nodeCalculationMethod === '内部') { nodeStartTime = addInternalBusinessTime(nodeEndTime, -timelineValue, processNode.weekendDays, nodeExcludedDates); @@ -3070,6 +3526,9 @@ export default function App() { (isBackward ? results.unshift.bind(results) : results.push.bind(results))({ processOrder: processNode.processOrder, nodeName: processNode.nodeName, + processGroup: processNode.processGroup, + processGroupInstanceId: processNode.processGroupInstanceId, + processGroupInstanceName: processNode.processGroupInstanceName, matchedLabels: processNode.matchedLabels, timelineValue: timelineValue, estimatedStart: timelineResult.startDate, @@ -3145,6 +3604,10 @@ export default function App() { } } + const nextAdjustments = remapTimelineAdjustmentsToNewResults(timelineResults, results); + setTimelineAdjustments(nextAdjustments); + pendingRecalculateAfterCalculateAdjustmentsRef.current = nextAdjustments; + pendingRecalculateAfterCalculateRef.current = true; setTimelineResults(results); if (!isBackward && mode === 'generate' && showUI && currentExpectedDate && results.length > 0) { let lastCompletionDate: Date | null = null; @@ -3208,8 +3671,8 @@ export default function App() { }; // 复合调整处理函数:根据缓冲期和交期余量状态决定调整方式 - const handleComplexAdjustment = (nodeIndex: number, adjustment: number) => { - const nextAdjustments = handleTimelineAdjustment(nodeIndex, adjustment); + const handleComplexAdjustment = (nodeKey: string, nodeIndex: number, adjustment: number) => { + const nextAdjustments = handleTimelineAdjustment(nodeKey, nodeIndex, adjustment); if (!nextAdjustments) return; const deficit = computeBufferDeficitDaysUsingEndDelta(nextAdjustments); const prevDeficit = lastBufferDeficitRef.current; @@ -3223,9 +3686,9 @@ export default function App() { }; // 调整时效值的函数 - const handleTimelineAdjustment = (nodeIndex: number, adjustment: number) => { + const handleTimelineAdjustment = (nodeKey: string, nodeIndex: number, adjustment: number) => { const newAdjustments = { ...timelineAdjustments }; - const currentAdjustment = newAdjustments[nodeIndex] || 0; + const currentAdjustment = newAdjustments[nodeKey] || 0; const newAdjustment = currentAdjustment + adjustment; // 允许调整后的时效值为负数,用于向前回退结束时间 @@ -3244,18 +3707,19 @@ export default function App() { return null; } - newAdjustments[nodeIndex] = newAdjustment; + newAdjustments[nodeKey] = newAdjustment; setTimelineAdjustments(newAdjustments); // 使用智能重算逻辑,只重算被调整的节点及其后续节点 - recalculateTimeline(newAdjustments); + const hasAnyNonZeroAdjustment = Object.values(newAdjustments).some(v => v !== 0); + recalculateTimeline(newAdjustments, !hasAnyNonZeroAdjustment); return newAdjustments; }; // 获取重新计算后的时间线结果(不更新状态,逻辑对齐页面的重算口径) const getRecalculatedTimeline = ( - adjustments: { [key: number]: number }, + adjustments: Record, opts?: { ignoreActualCompletionDates?: boolean; actualCompletionDatesOverride?: { [key: number]: Date | null } } ) => { const updatedResults = [...timelineResults]; @@ -3268,7 +3732,8 @@ export default function App() { : (typeof result.adjustedTimelineValue === 'number') ? result.adjustedTimelineValue : 0; - const adjustment = adjustments[i] || 0; + const nodeKey = buildTimelineAdjustmentKey(result, i); + const adjustment = adjustments[nodeKey] || 0; const adjustedTimelineValue = baseTimelineValue + adjustment; // 计算当前节点的开始时间 @@ -3328,6 +3793,10 @@ export default function App() { nodeEndTime = new Date(nodeStartTime); } + if (shouldForceEndTimeTo18(result) && nodeEndTime && !isNaN(nodeEndTime.getTime())) { + nodeEndTime = setTimeOfDay(nodeEndTime, 18, 0); + } + const actualEnd = opts?.ignoreActualCompletionDates ? null : (opts?.actualCompletionDatesOverride ?? actualCompletionDates)?.[i] ?? null; @@ -3372,7 +3841,7 @@ export default function App() { return updatedResults; }; - const getRecalculatedTimelineBackward = (adjustments: { [key: number]: number }) => { + const getRecalculatedTimelineBackward = (adjustments: Record) => { const updatedResults = [...timelineResults]; const fallbackEnd = (() => { if (expectedDate && !isNaN(expectedDate.getTime())) return new Date(expectedDate); @@ -3390,14 +3859,18 @@ export default function App() { : (typeof result.adjustedTimelineValue === 'number') ? result.adjustedTimelineValue : 0; - const adjustment = adjustments[i] || 0; + const nodeKey = buildTimelineAdjustmentKey(result, i); + const adjustment = adjustments[nodeKey] || 0; const adjustedTimelineValue = baseTimelineValue + adjustment; const nodeWeekendDays = result.weekendDaysConfig || []; const nodeCalculationMethod = result.calculationMethod || '外部'; const nodeExcludedDates = Array.isArray(result.excludedDates) ? result.excludedDates : []; - const nodeEndTime = new Date(cumulativeEndTime); + let nodeEndTime = new Date(cumulativeEndTime); + if (shouldForceEndTimeTo18(result) && nodeEndTime && !isNaN(nodeEndTime.getTime())) { + nodeEndTime = setTimeOfDay(nodeEndTime, 18, 0); + } let nodeStartTime: Date; if (adjustedTimelineValue !== 0) { @@ -3498,11 +3971,14 @@ export default function App() { return; } - const merged: {[key: number]: number} = { ...(timelineAdjustments || {}) }; + const merged: Record = { ...(timelineAdjustments || {}) }; + const nodes = allocationNodesSnapshot.length > 0 ? allocationNodesSnapshot : timelineResults; for (const [k, v] of Object.entries(allocationDraft || {})) { const idx = parseInt(k, 10); const val = Math.max(0, Math.round((Number(v) || 0) * 100) / 100); - merged[idx] = Math.round(((Number(merged[idx]) || 0) + val) * 100) / 100; + const node = nodes?.[idx]; + const nodeKey = buildTimelineAdjustmentKey(node, idx); + merged[nodeKey] = Math.round(((Number(merged[nodeKey]) || 0) + val) * 100) / 100; } setTimelineAdjustments(merged); @@ -3530,7 +4006,7 @@ export default function App() { return fallback && !isNaN(fallback.getTime()) ? fallback : null; }; - const computeLastNodeEndDeltaDays = (adjustments: { [key: number]: number }): number => { + const computeLastNodeEndDeltaDays = (adjustments: Record): number => { try { if (timelineDirection === 'backward') return 0; const baseline = getRecalculatedTimeline({}, { ignoreActualCompletionDates: true }); @@ -3557,7 +4033,7 @@ export default function App() { } }; - const computeBufferDeficitDaysUsingEndDelta = (adjustments: { [key: number]: number }): number => { + const computeBufferDeficitDaysUsingEndDelta = (adjustments: Record): number => { const deltaDays = computeLastNodeEndDeltaDays(adjustments); const base = Math.max(0, Math.ceil(baseBufferDays)); return Math.max(0, deltaDays - base); @@ -3565,7 +4041,7 @@ export default function App() { const computeExpectedDeliveryDateTsFromResults = ( results: any[], - adjustments: { [key: number]: number }, + adjustments: Record, baseBufferDaysOverride?: number ): number | null => { if (timelineDirection === 'backward') return null; @@ -3596,7 +4072,7 @@ export default function App() { setLockedExpectedDeliveryDateTs(expectedDate.getTime()); }; - const computeDynamicBufferDaysUsingEndDelta = (adjustments: { [key: number]: number }, baseBufferDaysOverride?: number): number => { + const computeDynamicBufferDaysUsingEndDelta = (adjustments: Record, baseBufferDaysOverride?: number): number => { try { if (timelineDirection === 'backward') return 0; const deltaDays = computeLastNodeEndDeltaDays(adjustments); @@ -3610,7 +4086,7 @@ export default function App() { }; // 重新计算时间线的函数 - const recalculateTimeline = (adjustments: {[key: number]: number}, forceRecalculateAll: boolean = false) => { + const recalculateTimeline = (adjustments: Record, forceRecalculateAll: boolean = false) => { if (timelineDirection === 'backward') { const updated = getRecalculatedTimelineBackward(adjustments); setTimelineResults(updated); @@ -3627,7 +4103,13 @@ export default function App() { const updatedResults = [...timelineResults]; // 找到第一个被调整的节点索引 - const adjustedIndices = Object.keys(adjustments).map(k => parseInt(k)).filter(i => adjustments[i] !== 0); + const adjustedIndices = updatedResults + .map((r, i) => { + const key = buildTimelineAdjustmentKey(r, i); + const v = adjustments[key] || 0; + return v !== 0 ? i : -1; + }) + .filter(i => i >= 0); // 找到有实际完成时间的节点 const actualCompletionIndices = Object.keys(actualCompletionDates) @@ -3692,7 +4174,8 @@ export default function App() { : (typeof result.adjustedTimelineValue === 'number') ? result.adjustedTimelineValue : 0; - const adjustment = adjustments[i] || 0; + const nodeKey = buildTimelineAdjustmentKey(result, i); + const adjustment = adjustments[nodeKey] || 0; const adjustedTimelineValue = baseTimelineValue + adjustment; // 计算当前节点的开始时间 @@ -3752,6 +4235,10 @@ export default function App() { } else { nodeEndTime = new Date(nodeStartTime); } + + if (shouldForceEndTimeTo18(result) && nodeEndTime && !isNaN(nodeEndTime.getTime())) { + nodeEndTime = setTimeOfDay(nodeEndTime, 18, 0); + } // 计算跳过的天数 const adjustedStartTime = adjustToNextWorkingHour(nodeStartTime, nodeCalculationMethod, nodeWeekendDays, nodeExcludedDates); @@ -3841,6 +4328,17 @@ export default function App() { } }, [timelineResults, isRestoringSnapshot]); + useEffect(() => { + if (!pendingRecalculateAfterCalculateRef.current) return; + if (isRestoringSnapshot) return; + pendingRecalculateAfterCalculateRef.current = false; + const adjustments = pendingRecalculateAfterCalculateAdjustmentsRef.current || timelineAdjustments; + pendingRecalculateAfterCalculateAdjustmentsRef.current = null; + if (timelineResults.length > 0) { + recalculateTimeline(adjustments, true); + } + }, [timelineResults, isRestoringSnapshot]); + // 捕获初始状态快照(在首次生成/还原出完整时间线后) useEffect(() => { if (!hasCapturedInitialSnapshotRef.current && timelineResults.length > 0) { @@ -3898,7 +4396,8 @@ export default function App() { setStartTime(s.startTime || null); setExpectedDate(s.expectedDate || null); setSelectedLabels(s.selectedLabels || {}); - setTimelineAdjustments(s.timelineAdjustments || {}); + const normalizedAdjustments = normalizeTimelineAdjustmentsFromSnapshot(s.timelineAdjustments || {}, s.timelineResults || []); + setTimelineAdjustments(normalizedAdjustments); setBaseBufferDays(s.baseBufferDays ?? 14); const shouldLock = s.isExpectedDeliveryDateLocked === true || (s.isExpectedDeliveryDateLocked === undefined && s.lockedExpectedDeliveryDateTs !== null && s.lockedExpectedDeliveryDateTs !== undefined); if (shouldLock && typeof s.lockedExpectedDeliveryDateTs === 'number' && Number.isFinite(s.lockedExpectedDeliveryDateTs)) { @@ -3914,7 +4413,7 @@ export default function App() { setLastSuggestedApplied(s.lastSuggestedApplied ?? null); setDeliveryMarginDeductions(s.deliveryMarginDeductions || 0); setTimelineResults(Array.isArray(s.timelineResults) ? s.timelineResults : []); - recalculateTimeline(s.timelineAdjustments || {}, true); + recalculateTimeline(normalizedAdjustments, true); setIsRestoringSnapshot(false); if (bitable.ui.showToast) { await bitable.ui.showToast({ toastType: ToastType.success, message: '已恢复至最初状态' }); @@ -3931,7 +4430,7 @@ export default function App() { const writeToDeliveryRecordTable = async ( timelineResults: any[], processRecordIds: string[], - timelineAdjustments: {[key: number]: number} = {}, + timelineAdjustments: Record = {}, overrides?: { foreignId?: string; style?: string; color?: string; expectedDate?: Date | null; startTime?: Date | null; selectedLabels?: {[key: string]: string | string[]}; baseBufferDays?: number } ) => { let recordCells: any[] | undefined; @@ -4134,8 +4633,14 @@ export default function App() { let adjustmentInfo = `版本:V${versionNumber}`; if (Object.keys(timelineAdjustments).length > 0) { - const adjustmentTexts = Object.entries(timelineAdjustments).map(([nodeIndex, adjustment]) => { - const nodeName = timelineResults[parseInt(nodeIndex)]?.nodeName || `节点${parseInt(nodeIndex) + 1}`; + const nodeNameByKey = new Map(); + for (let i = 0; i < timelineResults.length; i++) { + const r = timelineResults[i]; + const k = buildTimelineAdjustmentKey(r, i); + if (!nodeNameByKey.has(k)) nodeNameByKey.set(k, r?.nodeName || k); + } + const adjustmentTexts = Object.entries(timelineAdjustments).map(([nodeKey, adjustment]) => { + const nodeName = nodeNameByKey.get(nodeKey) || nodeKey; return `${nodeName}: ${adjustment > 0 ? '+' : ''}${adjustment.toFixed(1)} 天`; }); adjustmentInfo += `\n当前调整:\n${adjustmentTexts.join('\n')}`; @@ -4408,7 +4913,8 @@ export default function App() { versionField, timelinessField, processStyleField, - processColorField + processColorField, + processGroupField ] = await Promise.all([ processDataTable.getField(FOREIGN_ID_FIELD_ID), processDataTable.getField(PROCESS_NAME_FIELD_ID), @@ -4418,7 +4924,8 @@ export default function App() { processDataTable.getField(PROCESS_VERSION_FIELD_ID), processDataTable.getField(PROCESS_TIMELINESS_FIELD_ID), processDataTable.getField(PROCESS_STYLE_FIELD_ID), - processDataTable.getField(PROCESS_COLOR_FIELD_ID) + processDataTable.getField(PROCESS_COLOR_FIELD_ID), + processDataTable.getField(PROCESS_GROUP_FIELD_ID_DATA) ]); console.log('成功获取所有字段'); @@ -4532,6 +5039,8 @@ export default function App() { optionNameToId.set((opt as any).name, (opt as any).id); } } + + const _processGroupField = processGroupField; for (let index = 0; index < timelineResults.length; index++) { const result = timelineResults[index]; @@ -4579,6 +5088,14 @@ export default function App() { const nodeName = result.nodeName; const optionId = typeof nodeName === 'string' ? optionNameToId.get(nodeName) : undefined; + const rawGroupName = + (typeof result.processGroupInstanceName === 'string' && result.processGroupInstanceName.trim() !== '') + ? result.processGroupInstanceName + : (typeof result.processGroup === 'string' + ? result.processGroup + : extractText(result.processGroup)); + const groupName = (rawGroupName || '').trim(); + if (optionId) { const fields: Record = { [FOREIGN_ID_FIELD_ID]: foreignId, @@ -4593,6 +5110,10 @@ export default function App() { if (startTimestamp) fields[ESTIMATED_START_DATE_FIELD_ID] = startTimestamp; if (endTimestamp) fields[ESTIMATED_END_DATE_FIELD_ID] = endTimestamp; + if (groupName) { + (fields as any)[PROCESS_GROUP_FIELD_ID_DATA] = groupName; + } + recordValueList.push({ fields }); } else { const foreignIdCell = await foreignIdField.createCell(foreignId); @@ -4604,6 +5125,7 @@ export default function App() { const colorCell = await processColorField.createCell(color); const versionCell = await versionField.createCell(versionNumber); const timelinessCell = await timelinessField.createCell(result.timelineValue); + const processGroupCell = groupName ? await _processGroupField.createCell(groupName) : null; const recordCells = [ foreignIdCell, @@ -4616,6 +5138,7 @@ export default function App() { ]; if (startDateCell) recordCells.push(startDateCell); if (endDateCell) recordCells.push(endDateCell); + if (processGroupCell) recordCells.push(processGroupCell); fallbackCellRows.push(recordCells); } } @@ -5416,9 +5939,26 @@ export default function App() { newSelectedLabels['标签7'] = matched; } } + + const label12Vals = extractFieldValuesById('fldWuCeQDS'); + if (label12Vals.length > 0) { + const uniqVals = Array.from(new Set(label12Vals.map(v => (v || '').trim()).filter(Boolean))); + const opts = labelOptions['标签12'] || []; + const optSet = new Set(); + for (const o of opts) { + const v = typeof o?.value === 'string' ? o.value.trim() : ''; + const l = typeof o?.label === 'string' ? o.label.trim() : ''; + if (v) optSet.add(v); + if (l) optSet.add(l); + } + const matched = optSet.size > 0 ? uniqVals.filter(v => optSet.has(v)) : uniqVals; + if (matched.length > 0) { + newSelectedLabels['标签12'] = matched; + } + } // 添加标签10的自动填充 - newSelectedLabels['标签10'] = ['复版', '开货版不打版']; + newSelectedLabels['标签10'] = ['开货版不打版']; // 保留用户手动选择的标签1、7、8、9 setSelectedLabels(prev => ({ @@ -5817,6 +6357,43 @@ export default function App() { } } }}>读取当前选中记录并还原流程数据 + {currentDeliveryRecordId && ( 款号 @@ -6025,6 +6602,119 @@ export default function App() { })()} + { + setGroupConfigVisible(false); + setGroupOrderDraft([]); + }} + footer={ +
+ + 拖拽条目调整流程组顺序,上方的流程组将优先执行 + + + + + +
+ } + > +
+ {groupOrderDraft.length === 0 ? ( + 未检测到可配置的流程组 + ) : ( +
+ {groupOrderDraft.map((inst, index) => ( +
setDraggingGroupIndex(index)} + onDragOver={(e) => { + e.preventDefault(); + if (draggingGroupIndex === null || draggingGroupIndex === index) return; + setGroupOrderDraft(prev => { + const next = [...prev]; + const [removed] = next.splice(draggingGroupIndex, 1); + next.splice(index, 0, removed); + return next; + }); + setDraggingGroupIndex(index); + }} + onDragEnd={() => setDraggingGroupIndex(null)} + > +
+ + {index + 1} + + {inst.displayName} + +
+ +
+ ))} +
+ )} +
+
+ { const base = Array.isArray(prev) ? prev : []; return base.map((r: any, idx: number) => { - const nodeKey = buildExcludedDatesNodeKey(r?.nodeName, r?.processOrder, idx); + const nodeKey = buildExcludedDatesNodeKey( + r?.nodeName, + r?.processOrder, + idx, + r?.processGroupInstanceId || r?.processGroupInstanceName + ); if (!Object.prototype.hasOwnProperty.call(next, nodeKey)) return r; const list = Array.isArray(next[nodeKey]) ? next[nodeKey] : []; return { ...r, excludedDates: list }; @@ -6067,7 +6762,12 @@ export default function App() { >
{(Array.isArray(timelineResults) ? timelineResults : []).map((r: any, idx: number) => { - const nodeKey = buildExcludedDatesNodeKey(r?.nodeName, r?.processOrder, idx); + const nodeKey = buildExcludedDatesNodeKey( + r?.nodeName, + r?.processOrder, + idx, + r?.processGroupInstanceId || r?.processGroupInstanceName + ); const dates = Array.isArray(excludedDatesByNodeDraft?.[nodeKey]) ? excludedDatesByNodeDraft[nodeKey] : []; const groups = groupDatesByMonth(dates); const pickerValue = excludedDatesAddDraft?.[nodeKey] ?? null; @@ -6079,7 +6779,7 @@ export default function App() { >
- {typeof r?.processOrder !== 'undefined' ? `#${r.processOrder} ` : ''}{r?.nodeName || nodeKey} + {typeof r?.processOrder !== 'undefined' ? `#${r.processOrder} ` : ''}{r?.nodeName || nodeKey}{r?.processGroupInstanceName ? `(${r.processGroupInstanceName})` : ''}
{timelineResults.map((result, index) => { - const adjustment = timelineAdjustments[index] || 0; + const nodeKey = buildTimelineAdjustmentKey(result, index); + const adjustment = timelineAdjustments[nodeKey] || 0; const baseValue = (typeof result.timelineValue === 'number') ? result.timelineValue : (typeof result.adjustedTimelineValue === 'number') @@ -6279,7 +6980,8 @@ export default function App() { // 检查是否存在周转周期为零的情况 const hasTurnoverNodeWithZero = timelineResults.some((r, i) => { if (r.nodeName === '周转周期') { - const adj = timelineAdjustments[i] || 0; + const k = buildTimelineAdjustmentKey(r, i); + const adj = timelineAdjustments[k] || 0; const baseVal = (typeof r.timelineValue === 'number') ? r.timelineValue : (typeof r.adjustedTimelineValue === 'number') @@ -6300,7 +7002,7 @@ export default function App() {
- {result.processOrder && `${result.processOrder}. `}{result.nodeName} + {result.processOrder && `${result.processOrder}. `}{result.nodeName}{result.processGroupInstanceName ? `(${result.processGroupInstanceName})` : ''} {result.isAccumulated && result.allMatchedRecords ? ( <span style={{ marginLeft: '8px', @@ -6452,10 +7154,10 @@ export default function App() { : 0; const desiredAdjustmentAbs = desiredBusinessDays - baseValue; - const currentAdj = timelineAdjustments[index] || 0; + const currentAdj = timelineAdjustments[nodeKey] || 0; const deltaToApply = desiredAdjustmentAbs - currentAdj; if (deltaToApply !== 0) { - const updated = handleTimelineAdjustment(index, deltaToApply); + const updated = handleTimelineAdjustment(nodeKey, index, deltaToApply); // 若该节点不允许调整,交由实际完成日期的useEffect联动重算 if (!updated) { return; @@ -6479,6 +7181,52 @@ export default function App() { // 重算由依赖actualCompletionDates的useEffect触发,避免使用旧状态 }} /> + <Button + size="small" + disabled={isTurnoverNode} + onClick={() => { + try { + const baseTimelineValue = (typeof result.timelineValue === 'number') + ? result.timelineValue + : (typeof result.adjustedTimelineValue === 'number' ? result.adjustedTimelineValue : 0); + const currentAdj = timelineAdjustments[nodeKey] || 0; + const targetAdj = -baseTimelineValue; + const deltaToApply = targetAdj - currentAdj; + if (deltaToApply !== 0) { + handleTimelineAdjustment(nodeKey, index, deltaToApply); + } + setActualCompletionDates(prev => { + const next = { ...(prev || {}) }; + next[index] = null; + return next; + }); + } catch (e) { + console.warn('节点置零失败:', e); + } + }} + style={{ height: '24px', padding: '0 8px' }} + title={isTurnoverNode ? '周转周期节点不支持置零' : '将该节点时效置为0天'} + > + 置零 + </Button> + <Button + size="small" + disabled={isTurnoverNode || (timelineAdjustments[nodeKey] || 0) === 0} + onClick={() => { + try { + const currentAdj = timelineAdjustments[nodeKey] || 0; + if (currentAdj !== 0) { + handleTimelineAdjustment(nodeKey, index, -currentAdj); + } + } catch (e) { + console.warn('恢复节点失败:', e); + } + }} + style={{ height: '24px', padding: '0 8px' }} + title={isTurnoverNode ? '周转周期节点不支持恢复' : '恢复该节点的时效调整为0'} + > + 恢复 + </Button> </div> </div> @@ -6488,7 +7236,7 @@ export default function App() { <div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}> <Button size="small" - onClick={() => handleComplexAdjustment(index, -0.5)} + onClick={() => handleComplexAdjustment(nodeKey, index, -0.5)} disabled={adjustedValue <= 0 || isCurrentTurnoverZero} style={{ minWidth: '28px', height: '24px', fontSize: '13px' }} title={isCurrentTurnoverZero ? '周转周期为零,无法调整' : ''} @@ -6497,7 +7245,7 @@ export default function App() { </Button> <Button size="small" - onClick={() => handleComplexAdjustment(index, -1)} + onClick={() => handleComplexAdjustment(nodeKey, index, -1)} disabled={adjustedValue <= 0 || isCurrentTurnoverZero} style={{ minWidth: '24px', height: '24px', fontSize: '13px' }} title={isCurrentTurnoverZero ? '周转周期为零,无法调整' : ''} @@ -6530,7 +7278,7 @@ export default function App() { </div> <Button size="small" - onClick={() => handleComplexAdjustment(index, 1)} + onClick={() => handleComplexAdjustment(nodeKey, index, 1)} disabled={isCurrentTurnoverZero || (hasTurnoverNodeWithZero && !isTurnoverNode) || (isTurnoverNode && !isCurrentTurnoverZero)} style={{ minWidth: '24px', height: '24px', fontSize: '13px' }} title={ @@ -6544,7 +7292,7 @@ export default function App() { </Button> <Button size="small" - onClick={() => handleComplexAdjustment(index, 0.5)} + onClick={() => handleComplexAdjustment(nodeKey, index, 0.5)} disabled={isCurrentTurnoverZero || (hasTurnoverNodeWithZero && !isTurnoverNode) || (isTurnoverNode && !isCurrentTurnoverZero)} style={{ minWidth: '28px', height: '24px', fontSize: '13px' }} title={ @@ -6855,14 +7603,22 @@ export default function App() { <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; + {(() => { + const nodeNameByKey = new Map<string, string>(); + for (let i = 0; i < timelineResults.length; i++) { + const r = timelineResults[i]; + const k = buildTimelineAdjustmentKey(r, i); + if (!nodeNameByKey.has(k)) nodeNameByKey.set(k, r?.nodeName || k); + } + return Object.entries(timelineAdjustments).map(([nodeKey, adjustment]) => { + const nodeName = nodeNameByKey.get(nodeKey) || nodeKey; return ( - <Text key={nodeIndex} style={{ display: 'block', fontSize: '12px' }}> + <Text key={nodeKey} style={{ display: 'block', fontSize: '12px' }}> {nodeName}: {adjustment > 0 ? '+' : ''}{adjustment.toFixed(1)} 天 </Text> ); - })} + }); + })()} </div> </div> )} @@ -6878,13 +7634,18 @@ export default function App() { {(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: 11 }, (_, i) => i + 1).map(num => { + {Array.from({ length: 12 }, (_, i) => i + 1).map(num => { const labelKey = `标签${num}`; const options = labelOptions[labelKey] || []; - const isMultiSelect = labelKey === '标签7' || labelKey === '标签8' || labelKey === '标签10'; + const isMultiSelect = labelKey === '标签7' || labelKey === '标签8' || labelKey === '标签10' || labelKey === '标签12'; const val = selectedLabels[labelKey]; const isMissing = Array.isArray(val) ? (val as string[]).length === 0 : !(typeof val === 'string' && (val as string).trim().length > 0); - const isRequired = labelKey !== '标签11'; + const label7Val = selectedLabels['标签7']; + const label7Values = Array.isArray(label7Val) + ? (label7Val as any[]).map(s => String(s ?? '').trim()).filter(Boolean) + : (typeof label7Val === 'string' ? [label7Val.trim()].filter(Boolean) : []); + const shouldRequireLabel12 = label7Values.length > 0 && !(label7Values.length === 1 && label7Values[0] === '无(二次工艺)'); + const isRequired = labelKey !== '标签11' && (labelKey === '标签12' ? shouldRequireLabel12 : true); return ( <div key={labelKey}> @@ -6932,6 +7693,16 @@ export default function App() { </Select.Option> ))} </Select> + {labelKey === '标签7' && ( + <Text type="tertiary" style={{ marginTop: '6px', display: 'block' }}> + 此标签用作生产端工厂货期时效 + </Text> + )} + {labelKey === '标签12' && ( + <Text type="tertiary" style={{ marginTop: '6px', display: 'block' }}> + 此标签用作前置流程节点,若选择版单不自动带出,建议提前询问一下设计跟单 + </Text> + )} {isRequired && isMissing && ( <Text type="danger" style={{ marginTop: '6px', display: 'block' }}>该标签为必填</Text> )}