From da666a50869c768ad2e5aa7b5ff362aee2cc5ccc Mon Sep 17 00:00:00 2001 From: mairuiming Date: Wed, 30 Jul 2025 19:54:16 +0800 Subject: [PATCH] 1 1 --- src/App.tsx | 1801 +++++++++++++++++++-------------------------------- 1 file changed, 664 insertions(+), 1137 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index cb64fd4..3537baa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,1201 +1,728 @@ -import { useEffect, useState, useMemo } from 'react'; -import { bitable, FieldType, IDateTimeFieldMeta, IDateTimeField, ITable, ITextField, ISelectionField, INumberField, IOpenSegment, IRecord } from '@lark-base-open/js-sdk'; -import { Steps, Button, Typography, Form, Toast, Spin, Empty } from '@douyinfe/semi-ui'; +import React, { useEffect, useState } from 'react'; +import { bitable, ITable, IRecord, FieldType, ISingleSelectField, IMultiSelectField } from '@lark-base-open/js-sdk'; +import { Card, Table, Typography, Button, Spin, Empty, Toast, Modal, Select, Dropdown } from '@douyinfe/semi-ui'; +import { IconPlus } from '@douyinfe/semi-icons'; import './App.css'; -// 步骤数据结构定义 -interface StepItem { - name: string; - fieldId: string; // 原始的fieldId,代表流程名称本身 - finished: boolean; - parallelSteps?: StepItem[]; // 并行子步骤数组 - requireAllSubSteps?: boolean; // 是否所有子步骤都完成才标记主步骤完成 - expectTimeFieldId?: string; - expectTimeValue?: number; - actualTimeFieldId?: string; - actualTimeValue?: number; - startTimeFieldId?: string; - startTimeValue?: number; - processNodeValue?: string; - order?: number; // 流程顺序,相同顺序的步骤视为并行 - parallelGroup?: number; // 并行组标识,用于UI分组显示 - isMainStep?: boolean; // 是否为主步骤(整数顺序) -} - -// 流程配置项结构 -interface ProcessConfigItem { - processName: string; - order: number; - processNodeValue: string; - timeFieldId: string; - originalRecordId: string; - posLink: string; -} - // 常量定义 -const POS_TABLE_NAME = "pos_订单基础信息明细"; // 替换为你的实际POS表名 -const PROCESS_TABLE_NAME = "订单流程表"; // 替换为你的实际流程配置表名 -let POS_TABLE_ID = ""; -let PROCESS_TABLE_ID = ""; - -async function initTableIds() { - const posTable = await bitable.base.getTableByName(POS_TABLE_NAME); - POS_TABLE_ID = posTable.id; - const processTable = await bitable.base.getTableByName(PROCESS_TABLE_NAME); - PROCESS_TABLE_ID = processTable.id; -} - -// 在应用初始化或需要用到表ID前调用 -initTableIds(); - -const PROCESS_NAME_FIELD_NAME = '流程名称'; -const PROCESS_ORDER_FIELD_NAME = '流程顺序'; - -const POS_LINK_FIELD_NAME_IN_PROCESS_TABLE = 'POS表recordid'; -const PROCESS_NODE_FIELD_NAME = '流程节点'; -const FIELD_ID_FIELD_NAME = '字段ID'; - - -const PROCESS_NODE_EXPECT_TIME = '预计完成时间'; -const PROCESS_NODE_ACTUAL_TIME = '实际完成时间'; -const PROCESS_NODE_START_TIME = '实际开始时间'; - -// 格式化时间戳为可读格式 -const formatTimestamp = (timestamp: number | undefined): string => { - if (timestamp === undefined || timestamp === null) return '-'; - return new Date(timestamp).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); +const CONSTANTS = { + POS_TABLE_NAME: 'pos_订单基础信息明细', + PROCESS_TABLE_NAME: '订单流程表', + POS_RECORD_ID_FIELD_NAME: 'POS表recordid', + TARGET_VIEW_NAME: '插件专用', + TEMPLATE_FIELD_NAME: '流程模板', + PROCESS_ORDER_FIELD: '流程顺序', + CURRENT_PROGRESS_FIELD: '当前进度', + PROCESS_NAME_FIELD: '流程名称', + FEISHU_CALLBACK_URL: 'https://open.feishu.cn/anycross/trigger/callback/MDgzNzk1ZTE1MGVhOWEzZTcyZDFjOTliZmJmOTNiYTZk', + DEFAULT_COLUMNS: ['操作', '款式SKC', '流程顺序', '流程名称', '实际开始时间', '实际完成时间'] }; -// 步骤管理服务类 -class StepService { - // 保存步骤完成时间 - static async saveStepCompletionTime( - posTable: ITable, - selectedRecordId: string, - stepItem: StepItem, - isSubStep: boolean = false - ): Promise { - if (!selectedRecordId || !stepItem.actualTimeFieldId) return null; - - try { - const timeField = await posTable.getFieldById(stepItem.actualTimeFieldId); - const now = Date.now(); - - await timeField.setValue(selectedRecordId, now); - stepItem.actualTimeValue = now; - stepItem.finished = true; - - console.log(`${isSubStep ? '子步骤' : '步骤'} "${stepItem.name}" 完成时间已保存到字段 ${stepItem.actualTimeFieldId}`); - return now; - } catch (e) { - console.error(`Error saving ${isSubStep ? 'sub-step' : 'step'} completion time:`, e); - throw e; - } - } - - // 更新后续步骤的开始时间 - static async updateSubsequentStepsStartTimes( - posTable: ITable, - selectedRecordId: string, - steps: StepItem[], - stepIndex: number, - actualTimeValue: number - ) { - if (!selectedRecordId) return; - - try { - // 从当前步骤的下一个步骤开始更新 - for (let i = stepIndex + 1; i < steps.length; i++) { - const nextStep = steps[i]; - - // 如果下一步骤已有开始时间,且早于当前完成时间,则不更新 - if (nextStep.startTimeValue && nextStep.startTimeValue <= actualTimeValue) { - continue; - } - - // 如果下一步骤有开始时间字段ID,更新该字段 - if (nextStep.startTimeFieldId) { - const startTimeField = await posTable.getFieldById(nextStep.startTimeFieldId); - await startTimeField.setValue(selectedRecordId, actualTimeValue); - nextStep.startTimeValue = actualTimeValue; - console.log(`Updated step ${i} (${nextStep.name}) start time to ${new Date(actualTimeValue).toLocaleString()}`); - } - - // 如果下一步骤有并行子步骤,也更新它们的开始时间 - if (nextStep.parallelSteps) { - for (const subStep of nextStep.parallelSteps) { - if (subStep.startTimeFieldId) { - const subStartTimeField = await posTable.getFieldById(subStep.startTimeFieldId); - await subStartTimeField.setValue(selectedRecordId, actualTimeValue); - subStep.startTimeValue = actualTimeValue; - console.log(`Updated sub-step ${subStep.name} start time to ${new Date(actualTimeValue).toLocaleString()}`); - } - } - } - } - } catch (e) { - console.error("Error updating subsequent steps start times:", e); - Toast.warning("更新后续步骤开始时间失败"); - } - } - - // 处理子步骤完成的逻辑 - static async handleSubStepCompletion( - newSteps: StepItem[], - stepIndex: number, - subStepIndex: number, - posTable: ITable, - selectedRecordId: string, - refreshSteps: (value: boolean) => void - ) { - const parentStep = newSteps[stepIndex]; - const subStep = parentStep.parallelSteps![subStepIndex]; - - // 如果父步骤要求所有子步骤完成才标记为完成 - if (parentStep.requireAllSubSteps && parentStep.parallelSteps?.every(ps => ps.finished)) { - parentStep.finished = true; - - // 如果父步骤也有完成时间字段,记录完成时间 - if (parentStep.actualTimeFieldId) { - try { - const completionTime = await StepService.saveStepCompletionTime(posTable, selectedRecordId, parentStep); - if (completionTime) { - // 更新父步骤后的步骤开始时间 - await StepService.updateSubsequentStepsStartTimes( - posTable, selectedRecordId, newSteps, stepIndex, completionTime - ); - - // 强制刷新步骤数据 - refreshSteps(true); - } - } catch (e) { - console.error("Error saving parent step completion time:", e); - } - } else { - // 如果没有完成时间字段,也需要更新后续步骤 - refreshSteps(true); - } - } else if (!parentStep.requireAllSubSteps && parentStep.parallelSteps?.some(ps => ps.finished)) { - // 任一子步骤完成即可推进主步骤 - parentStep.finished = true; - - // 如果父步骤有完成时间字段,记录完成时间 - if (parentStep.actualTimeFieldId) { - try { - const completionTime = await StepService.saveStepCompletionTime(posTable, selectedRecordId, parentStep); - if (completionTime) { - // 更新父步骤后的步骤开始时间 - await StepService.updateSubsequentStepsStartTimes( - posTable, selectedRecordId, newSteps, stepIndex, completionTime - ); - - // 强制刷新步骤数据 - refreshSteps(true); - } - } catch (e) { - console.error("Error saving parent step completion time:", e); - } - } else { - // 如果没有完成时间字段,也需要更新后续步骤 - refreshSteps(true); - } - } - } - - // 查找上一个主步骤的完成时间 - static findPreviousMainStepFinishTime(steps: StepItem[], currentIndex: number): number | undefined { - for (let i = currentIndex - 1; i >= 0; i--) { - const step = steps[i]; - // 如果是主步骤(整数顺序)且已完成 - if (step.isMainStep && step.finished && step.actualTimeValue) { - return step.actualTimeValue; - } - } - return undefined; - } - - // 更新步骤的开始时间为上一个主步骤的完成时间 - static async updateStepStartTimeFromPreviousMainStep( - posTable: ITable, - selectedRecordId: string, - steps: StepItem[], - stepIndex: number - ) { - if (!selectedRecordId) return; - - const currentStep = steps[stepIndex]; - if (!currentStep.startTimeFieldId) return; - - try { - // 查找上一个主步骤的完成时间 - const previousFinishTime = StepService.findPreviousMainStepFinishTime(steps, stepIndex); - - if (previousFinishTime) { - // 更新当前步骤的开始时间 - const startTimeField = await posTable.getFieldById(currentStep.startTimeFieldId); - await startTimeField.setValue(selectedRecordId, previousFinishTime); - currentStep.startTimeValue = previousFinishTime; - console.log(`Updated step ${stepIndex} (${currentStep.name}) start time to previous main step's finish time: ${new Date(previousFinishTime).toLocaleString()}`); - - // 如果当前步骤有并行子步骤,也更新它们的开始时间 - if (currentStep.parallelSteps) { - for (const subStep of currentStep.parallelSteps) { - if (subStep.startTimeFieldId) { - const subStartTimeField = await posTable.getFieldById(subStep.startTimeFieldId); - await subStartTimeField.setValue(selectedRecordId, previousFinishTime); - subStep.startTimeValue = previousFinishTime; - console.log(`Updated sub-step ${subStep.name} start time to previous main step's finish time: ${new Date(previousFinishTime).toLocaleString()}`); - } - } - } - } - } catch (e) { - console.error(`Error updating step ${stepIndex} start time from previous main step:`, e); - Toast.warning("更新步骤开始时间失败"); - } - } +// 类型定义 +interface ProcessRecord { + recordId: string; + fields: Record; + displayData: Record; } -// 判断并行步骤是否完成 -const isParallelFinished = (parallelSteps: StepItem[], requireAll: boolean = true) => { - if (!parallelSteps || parallelSteps.length === 0) return true; - if (requireAll) { - return parallelSteps.every(step => step.finished); - } else { - return parallelSteps.some(step => step.finished); - } -}; - -// 步骤UI组件 -const StepCard = ({ - step, - idx, - currentStep, - loading, - onFinishStep, - formatTimestamp, - isCurrentStep, - isActiveParallelGroup, - isAnySubStepFinished, // 判断并行组中是否有子步骤已完成 - canAccessSubSteps // 判断是否可以访问子步骤 -}: { - step: StepItem; - idx: number; - currentStep: number; +interface AppState { + selectedRecordId?: string; + processRecords: ProcessRecord[]; loading: boolean; - onFinishStep: (stepIndex: number, subStepIndex?: number) => void; - formatTimestamp: (timestamp: number | undefined) => string; - isCurrentStep: boolean; - isActiveParallelGroup: boolean; - isAnySubStepFinished: boolean; - canAccessSubSteps: boolean; -}) => { - return ( -
- {/* 并行步骤组标题 */} - {step.parallelSteps && ( -
- - 并行处理组: {step.name} - - - {step.requireAllSubSteps ? '所有子步骤完成后自动推进' : '任一子步骤完成后自动推进'} - -
- )} + error: string | null; + showAllColumns: boolean; + currentProgress: string | null; + nextProgress: string | null; + parallelProgress: string[]; +} - {/* 普通步骤的时间信息展示 */} - {!step.parallelSteps && ( -
- - 预计完成时间:{formatTimestamp(step.expectTimeValue)} - - - 实际开始时间:{formatTimestamp(step.startTimeValue)} - - - 实际完成时间:{formatTimestamp(step.actualTimeValue)} - - step.expectTimeValue ? '#f5222d' : - (step.actualTimeValue && step.expectTimeValue && step.actualTimeValue <= step.expectTimeValue ? '#52c41a' : '#000'), - fontSize: 14, marginBottom: 0, display: 'block' - }}> - 耗时:{step.startTimeValue && step.actualTimeValue ? - ((step.actualTimeValue - step.startTimeValue) > 0 ? - ((step.actualTimeValue - step.startTimeValue) / 1000 / 60 / 60).toFixed(2) + ' 小时' : '0 小时') : '-'} - -
- )} +interface InsertState { + modalVisible: boolean; + currentRecord: ProcessRecord | null; + direction: 'up' | 'down' | null; + selectedTemplate: string[]; + templateOptions: { label: string; value: string }[]; +} - {/* 并行子步骤 */} - {step.parallelSteps && ( -
- {step.parallelSteps.map((sub, subIdx) => ( -
- {/* 完成状态标识 */} - {sub.finished && ( -
- 已完成 -
- )} - - {sub.name} - -
- - 预计完成时间:{formatTimestamp(sub.expectTimeValue || step.expectTimeValue)} - - - 实际开始时间:{formatTimestamp(sub.startTimeValue || step.startTimeValue)} - - - 实际完成时间:{formatTimestamp(sub.actualTimeValue)} - - (sub.expectTimeValue || step.expectTimeValue) ? '#f5222d' : - (sub.actualTimeValue && (sub.expectTimeValue || step.expectTimeValue) && - sub.actualTimeValue <= (sub.expectTimeValue || step.expectTimeValue) ? '#52c41a' : '#000'), - fontSize: 13, marginBottom: 0, display: 'block' - }}> - 耗时:{sub.startTimeValue && sub.actualTimeValue ? - ((sub.actualTimeValue - sub.startTimeValue) > 0 ? - ((sub.actualTimeValue - sub.startTimeValue) / 1000 / 60 / 60).toFixed(2) + ' 小时' : '0 小时') : - (step.startTimeValue && sub.actualTimeValue ? - ((sub.actualTimeValue - step.startTimeValue) / 1000 / 60 / 60).toFixed(2) + ' 小时' : '-')} - -
- - {/* 允许活动并行组中的子步骤被点击,或并行组中已有子步骤完成 */} - {!sub.finished && (isCurrentStep || isActiveParallelGroup || isAnySubStepFinished || canAccessSubSteps) && ( - - )} - - {/* 只有当父步骤未完成且不在当前活动状态时才显示等待提示 */} - {!sub.finished && !(isCurrentStep || isActiveParallelGroup || isAnySubStepFinished || canAccessSubSteps) && ( -
- - 等待前置步骤完成 - -
- )} -
- ))} -
- )} - - {/* 当前步骤的完成按钮 */} - {idx === currentStep && !step.finished && !step.parallelSteps && ( - - )} -
- ); -}; +interface TableState { + posTable: ITable | null; + processTable: ITable | null; + posRecordIdFieldId: string | null; + tableColumns: any[]; + allTableColumns: any[]; +} export default function App() { - const [dateFields, setDateFields] = useState([]); - const [steps, setSteps] = useState([]); - const [currentStep, setCurrentStep] = useState(0); - const [loading, setLoading] = useState(false); - const [selectedRecordId, setSelectedRecordId] = useState(); - const [refreshSteps, setRefreshSteps] = useState(false); - const [isChangingRecord, setIsChangingRecord] = useState(false); // 记录是否正在切换记录 - const [hasNoProcess, setHasNoProcess] = useState(false); // 记录是否没有配置流程 - - // 缓存所有流程配置数据 - const [allProcessConfigs, setAllProcessConfigs] = useState([]); - const [processConfigLoading, setProcessConfigLoading] = useState(true); - // 缓存字段元数据,避免重复获取 - const [fieldMetadataCache, setFieldMetadataCache] = useState>(new Map()); - // 缓存按POS记录ID分组的配置 - const [configsByPosLink, setConfigsByPosLink] = useState>(new Map()); + // 状态管理 + const [appState, setAppState] = useState({ + processRecords: [], + loading: false, + error: null, + showAllColumns: false, + currentProgress: null, + nextProgress: null, + parallelProgress: [] + }); - // 预加载所有流程配置数据 - async function fetchAllProcessConfigs() { - setProcessConfigLoading(true); + const [insertState, setInsertState] = useState({ + modalVisible: false, + currentRecord: null, + direction: null, + selectedTemplate: [], + templateOptions: [] + }); + + const [tableState, setTableState] = useState({ + posTable: null, + processTable: null, + posRecordIdFieldId: null, + tableColumns: [], + allTableColumns: [] + }); + + // 工具函数 + const formatDateTime = (value: any): string => { + if (!value || value === '-') return '-'; try { - // 检查缓存中是否已有配置数据 - if (allProcessConfigs.length > 0) { - console.log('Using cached process configs'); - return; - } - - const processTable = await bitable.base.getTableByName(PROCESS_TABLE_NAME); - - // 优化:批量获取所有字段元数据,减少API调用 - const allFields = await processTable.getFieldMetaList(); - const fieldMap = new Map(allFields.map(field => [field.id, field])); - setFieldMetadataCache(fieldMap); - - // 验证字段是否存在 - 使用优化后的字段映射 - const requiredFieldNames = [ - POS_LINK_FIELD_NAME_IN_PROCESS_TABLE, - PROCESS_NODE_FIELD_NAME, - FIELD_ID_FIELD_NAME, - PROCESS_NAME_FIELD_NAME, - PROCESS_ORDER_FIELD_NAME - ]; - // 检查必需字段是否存在 - const missingFields: string[] = []; - requiredFieldNames.forEach(name => { - const field = allFields.find(f => f.name === name); - if (!field) { - missingFields.push(`字段名: ${name}`); - } - }); - if (missingFields.length > 0) { - const errorMsg = `订单流程表中缺少关键字段: ${missingFields.join(', ')}`; - console.error(errorMsg); - Toast.error(errorMsg); - setProcessConfigLoading(false); - return; - } - // 使用更高效的分页获取记录 - const allRecords: IRecord[] = []; - let pageToken: string | undefined; - do { - const response = await processTable.getRecords({ - pageSize: 5000, // 最大页大小 - pageToken - }); - allRecords.push(...response.records); - pageToken = response.pageToken; - } while (pageToken); - console.log(`Fetched ${allRecords.length} process config records`); - const allConfigs: ProcessConfigItem[] = []; - // 获取字段对象 - const posLinkField = allFields.find(f => f.name === POS_LINK_FIELD_NAME_IN_PROCESS_TABLE); - const processNodeField = allFields.find(f => f.name === PROCESS_NODE_FIELD_NAME); - const fieldIdField = allFields.find(f => f.name === FIELD_ID_FIELD_NAME); - for (const record of allRecords) { - const fields = record.fields; - const recordId = record.recordId; - // 处理POS链接字段 - let posLinkText = ''; - const posLinkFieldValue = fields[posLinkField?.id ?? '']; - if (Array.isArray(posLinkFieldValue)) { - posLinkText = (posLinkFieldValue[0] as IOpenSegment)?.text?.trim() || ''; - } else if (typeof posLinkFieldValue === 'object' && posLinkFieldValue) { - posLinkText = (posLinkFieldValue as IOpenSegment).text?.trim() || ''; - } - // 处理流程名称字段 - 使用字段ID直接访问 - const processNameField = allFields.find(f => f.name === PROCESS_NAME_FIELD_NAME); - let processName = ''; - if (processNameField) { - const fieldValue = fields[processNameField.id]; - processName = Array.isArray(fieldValue) - ? (fieldValue[0] as IOpenSegment)?.text?.trim() || '' - : (fieldValue as IOpenSegment)?.text?.trim() || ''; - } - // 处理顺序字段 - const processOrderField = allFields.find(f => f.name === PROCESS_ORDER_FIELD_NAME); - const order = processOrderField - ? (fields[processOrderField.id] as number | undefined) || 0 - : 0; - // 处理流程节点字段 - const processNodeValue = fields[processNodeField?.id ?? ''] - ? (Array.isArray(fields[processNodeField.id]) - ? (fields[processNodeField.id][0] as IOpenSegment)?.text?.trim() || '' - : (fields[processNodeField.id] as IOpenSegment)?.text?.trim() || '') - : ''; - // 处理时间字段ID字段 - const timeFieldId = fields[fieldIdField?.id ?? ''] - ? (Array.isArray(fields[fieldIdField.id]) - ? (fields[fieldIdField.id][0] as IOpenSegment)?.text?.trim() || '' - : (fields[fieldIdField.id] as IOpenSegment)?.text?.trim() || '') - : ''; - allConfigs.push({ - processName, - order, - processNodeValue, - timeFieldId, - originalRecordId: recordId, - posLink: posLinkText - }); - } - - // 按POS记录ID分组配置,便于快速查找 - const configsByPosMap = new Map(); - allConfigs.forEach(config => { - if (!configsByPosMap.has(config.posLink)) { - configsByPosMap.set(config.posLink, []); - } - configsByPosMap.get(config.posLink)?.push(config); - }); - - setAllProcessConfigs(allConfigs); - setConfigsByPosLink(configsByPosMap); - console.log('Fetched all process configs:', allConfigs.length, 'items'); - } catch (error) { - console.error('Failed to fetch all process configs:', error); - Toast.error('加载流程配置数据失败,请检查网络连接或表格权限'); - } finally { - setProcessConfigLoading(false); + const timestamp = typeof value === 'string' && /^\d+$/.test(value) ? parseInt(value) : value; + const date = new Date(timestamp > 1000000000000 ? timestamp : timestamp * 1000); + if (isNaN(date.getTime())) return value; + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }).replace(/\//g, '-'); + } catch { + return value; } - } + }; - useEffect(() => { - fetchAllProcessConfigs(); - }, []); + const isDateField = (fieldName: string): boolean => { + return ['时间', '日期', 'time', 'date'].some(keyword => + fieldName.toLowerCase().includes(keyword.toLowerCase()) + ); + }; - // 初始化日期字段 - useEffect(() => { - async function fetchDateFieldsForUI() { - try { - const activeTable = await bitable.base.getActiveTable(); - const fields = await activeTable.getFieldMetaListByType(FieldType.DateTime); - setDateFields(fields); - } catch (e) { - console.error("Failed to fetch date fields for UI: ", e); - Toast.error("获取可选日期字段列表失败"); + const updateAppState = (updates: Partial) => { + setAppState(prev => ({ ...prev, ...updates })); + }; + + const updateInsertState = (updates: Partial) => { + setInsertState(prev => ({ ...prev, ...updates })); + }; + + const updateTableState = (updates: Partial) => { + setTableState(prev => ({ ...prev, ...updates })); + }; + + // 初始化表格 + const initializeTables = async () => { + try { + updateAppState({ loading: true, error: null }); + + const tableList = await bitable.base.getTableList(); + const tables = await Promise.all( + tableList.map(async table => ({ table, name: await table.getName() })) + ); + + const posTable = tables.find(t => t.name === CONSTANTS.POS_TABLE_NAME)?.table; + const processTable = tables.find(t => t.name === CONSTANTS.PROCESS_TABLE_NAME)?.table; + + if (!posTable) throw new Error(`未找到表格: ${CONSTANTS.POS_TABLE_NAME}`); + if (!processTable) throw new Error(`未找到表格: ${CONSTANTS.PROCESS_TABLE_NAME}`); + + const processFields = await processTable.getFieldMetaList(); + const posRecordIdField = processFields.find(field => field.name === CONSTANTS.POS_RECORD_ID_FIELD_NAME); + + if (!posRecordIdField) { + throw new Error(`在表格 ${CONSTANTS.PROCESS_TABLE_NAME} 中未找到字段: ${CONSTANTS.POS_RECORD_ID_FIELD_NAME}`); } - } - fetchDateFieldsForUI(); - }, []); - // 优化:初始化步骤时处理并行组 - const initializeStepsWithParallel = (configs: ProcessConfigItem[]) => { - if (configs.length === 0) return []; - - // 使用Map按order分组,提高查询效率 - const stepsByOrder = new Map(); - - // 初始化步骤 - configs.forEach(config => { - const step: StepItem = { - name: config.processName, - fieldId: config.timeFieldId, - finished: false, - order: config.order, - processNodeValue: config.processNodeValue, - isMainStep: config.order === Math.floor(config.order) // 标记是否为主步骤(整数顺序) + // 设置表格列 + const actionColumn = { + title: '操作', + key: 'action', + width: 80, + fixed: 'left' as const, + render: (_: any, record: ProcessRecord) => renderInsertButton(record) }; - - if (!stepsByOrder.has(config.order)) { - stepsByOrder.set(config.order, []); - } - stepsByOrder.get(config.order)?.push(step); - }); - - // 处理并行组 - const processedSteps: StepItem[] = []; - stepsByOrder.forEach((groupSteps, order) => { - if (groupSteps.length === 1) { - processedSteps.push(groupSteps[0]); - } else { - const parallelGroupStep: StepItem = { - name: `并行组${order}`, - fieldId: `parallel_group_${order}`, - finished: false, - order, - parallelSteps: groupSteps, - requireAllSubSteps: true, - parallelGroup: order, - isMainStep: true // 并行组作为一个整体视为主步骤 - }; - - // 继承第一个子步骤的时间字段 - if (groupSteps.length > 0) { - const firstSubStep = groupSteps[0]; - parallelGroupStep.expectTimeFieldId = firstSubStep.expectTimeFieldId; - parallelGroupStep.expectTimeValue = firstSubStep.expectTimeValue; - parallelGroupStep.startTimeFieldId = firstSubStep.startTimeFieldId; - parallelGroupStep.startTimeValue = firstSubStep.startTimeValue; - } - - processedSteps.push(parallelGroupStep); - } - }); - - return processedSteps.sort((a, b) => (a.order || 0) - (b.order || 0)); - }; - // 刷新步骤数据 - const refreshProcessSteps = async () => { - if (selectedRecordId) { - await loadProcessSteps(selectedRecordId); + const dataColumns = processFields.map(field => ({ + title: field.name, + dataIndex: field.id, + key: field.id, + render: (value: any, record: ProcessRecord) => { + const displayValue = record.displayData[field.id] || '-'; + return isDateField(field.name) && displayValue !== '-' + ? formatDateTime(displayValue) + : displayValue; + } + })); + + const allColumns = [actionColumn, ...dataColumns]; + const visibleColumns = allColumns.filter(col => + appState.showAllColumns || CONSTANTS.DEFAULT_COLUMNS.includes(col.title) + ); + + updateTableState({ + posTable, + processTable, + posRecordIdFieldId: posRecordIdField.id, + allTableColumns: allColumns, + tableColumns: visibleColumns + }); + + } catch (error) { + const message = error instanceof Error ? error.message : '初始化失败'; + updateAppState({ error: message }); + Toast.error('初始化表格失败'); + } finally { + updateAppState({ loading: false }); } }; - // 从缓存中加载流程步骤 - const loadProcessSteps = async (currentPosRecordId: string) => { - console.log('loadProcessSteps triggered with currentPosRecordId:', currentPosRecordId); - if (!currentPosRecordId) { - setSteps([]); - setCurrentStep(0); - setHasNoProcess(false); - console.log('No POS record selected, steps cleared.'); + // 加载订单流程记录 + const loadProcessRecords = async (posRecordId: string) => { + const { processTable, posRecordIdFieldId } = tableState; + if (!processTable || !posRecordIdFieldId) { + Toast.error('系统未初始化完成,请稍后重试'); return; } - setLoading(true); - setIsChangingRecord(true); - setHasNoProcess(false); // 重置状态 - console.log('Loading process steps from cached configs...'); + try { + updateAppState({ loading: true }); + + const viewList = await processTable.getViewList(); + const targetView = await Promise.all( + viewList.map(async view => ({ view, meta: await view.getMeta() })) + ).then(views => views.find(v => v.meta.name === CONSTANTS.TARGET_VIEW_NAME)); + + if (!targetView) { + throw new Error(`未找到名为"${CONSTANTS.TARGET_VIEW_NAME}"的视图`); + } + + // 清理并设置筛选条件 + const currentFilterInfo = await targetView.view.getFilterInfo(); + if (currentFilterInfo?.conditions) { + await Promise.all( + currentFilterInfo.conditions.map(condition => + targetView.view.deleteFilterCondition(condition.conditionId) + ) + ); + } + + await targetView.view.addFilterCondition({ + fieldId: posRecordIdFieldId, + operator: 'is', + value: posRecordId + }); + + await targetView.view.setFilterConjunction('and'); + await targetView.view.applySetting(); + await new Promise(resolve => setTimeout(resolve, 500)); + + const recordsResponse = await processTable.getRecords({ + pageSize: 1000, + viewId: targetView.meta.id + }); + + const processedRecords = await Promise.all( + recordsResponse.records.map(async record => { + const fields = await record.fields; + const displayData: Record = {}; + + Object.entries(fields).forEach(([fieldId, value]) => { + if (value == null) { + displayData[fieldId] = '-'; + } else if (Array.isArray(value)) { + displayData[fieldId] = value.map(item => + typeof item === 'object' && item.text ? item.text : String(item) + ).join(', '); + } else if (typeof value === 'object' && value.text) { + displayData[fieldId] = value.text; + } else { + displayData[fieldId] = String(value); + } + }); + + return { recordId: record.recordId, fields, displayData }; + }) + ); + + // 计算进度信息和排序 + const { sortedRecords, progressInfo } = await processRecordsWithProgress( + processedRecords, processTable + ); + + updateAppState({ + processRecords: sortedRecords, + ...progressInfo + }); + + Toast.success(`找到 ${sortedRecords.length} 条匹配的订单流程记录`); + + } catch (error) { + const message = error instanceof Error ? error.message : '未知错误'; + Toast.error(`加载订单流程记录失败: ${message}`); + updateAppState({ processRecords: [] }); + } finally { + updateAppState({ loading: false }); + } + }; + + // 处理记录排序和进度计算 + const processRecordsWithProgress = async (records: ProcessRecord[], processTable: ITable) => { + const processFields = await processTable.getFieldMetaList(); + const orderField = processFields.find(f => f.name === CONSTANTS.PROCESS_ORDER_FIELD); + const currentProgressField = processFields.find(f => f.name === CONSTANTS.CURRENT_PROGRESS_FIELD); + const processNameField = processFields.find(f => f.name === CONSTANTS.PROCESS_NAME_FIELD); + + let progressInfo = { + currentProgress: null as string | null, + nextProgress: null as string | null, + parallelProgress: [] as string[] + }; + + // 计算进度信息 + if (currentProgressField && processNameField && orderField) { + const currentProgressRecord = records.find(record => { + const progressValue = record.fields[currentProgressField.id]; + return progressValue != null && progressValue !== ''; + }); + + if (currentProgressRecord) { + progressInfo.currentProgress = currentProgressRecord.displayData[processNameField.id]; + const currentOrder = Number(currentProgressRecord.fields[orderField.id]); + + const nextRecords = records + .filter(record => { + const order = Number(record.fields[orderField.id]); + return Number.isInteger(order) && order > currentOrder; + }) + .sort((a, b) => Number(a.fields[orderField.id]) - Number(b.fields[orderField.id])); + + if (nextRecords.length > 0) { + const nextRecord = nextRecords[0]; + progressInfo.nextProgress = nextRecord.displayData[processNameField.id]; + + const nextOrder = Number(nextRecord.fields[orderField.id]); + progressInfo.parallelProgress = records + .filter(record => { + const order = Number(record.fields[orderField.id]); + return !Number.isInteger(order) && Math.floor(order) === nextOrder && order > nextOrder; + }) + .sort((a, b) => Number(a.fields[orderField.id]) - Number(b.fields[orderField.id])) + .map(record => record.displayData[processNameField.id]); + } else { + progressInfo.nextProgress = '已完成所有流程'; + } + } + } + + // 排序记录 + const sortedRecords = orderField ? records.sort((a, b) => { + const aNum = Number(a.fields[orderField.id]); + const bNum = Number(b.fields[orderField.id]); + if (!isNaN(aNum) && !isNaN(bNum)) return aNum - bNum; + if (isNaN(aNum) && !isNaN(bNum)) return 1; + if (!isNaN(aNum) && isNaN(bNum)) return -1; + return String(a.fields[orderField.id]).localeCompare(String(b.fields[orderField.id])); + }) : records; + + return { sortedRecords, progressInfo }; + }; + + // 加载模板选项 + const loadTemplateOptions = async () => { + let { posTable } = tableState; + + if (!posTable) { + try { + const tableList = await bitable.base.getTableList(); + const tables = await Promise.all( + tableList.map(async table => ({ table, name: await table.getName() })) + ); + posTable = tables.find(t => t.name === CONSTANTS.POS_TABLE_NAME)?.table || null; + if (!posTable) throw new Error('POS表未找到'); + updateTableState({ posTable }); + } catch (error) { + Toast.error('POS表初始化失败'); + return; + } + } try { - const posTable = await bitable.base.getTableByName(POS_TABLE_NAME); + const posFields = await posTable.getFieldMetaList(); + const templateField = posFields.find(field => field.name === CONSTANTS.TEMPLATE_FIELD_NAME); - // 从缓存中快速筛选当前记录的配置 - const filteredConfigs = configsByPosLink.get(currentPosRecordId) || - allProcessConfigs.filter(config => config.posLink === currentPosRecordId); - console.log('Filtered configs for current record:', filteredConfigs.length); - - if (filteredConfigs.length === 0) { - console.log('No configs found for current record'); - setSteps([]); - setCurrentStep(0); - setHasNoProcess(true); // 设置为没有配置流程 - setLoading(false); - setIsChangingRecord(false); + if (!templateField) { + Toast.error('未找到[流程模板]字段'); return; } - // 按流程顺序分组配置 - const stepsByOrder = new Map(); - const tempStepsMap = new Map(); + let options: { label: string; value: string }[] = []; - // 首先处理每个步骤的基本信息 - for (const config of filteredConfigs) { - const stepName = config.processName; - const order = config.order; - - // 如果该步骤不存在,创建新步骤 - if (!tempStepsMap.has(stepName)) { - tempStepsMap.set(stepName, { - name: stepName, - fieldId: stepName, - finished: false, - order, - parallelSteps: undefined, - requireAllSubSteps: true, // 默认需要所有子步骤完成 - isMainStep: order === Math.floor(order) // 标记是否为主步骤 - }); - } else { - // 如果步骤已存在,使用最小的order值 - const existingStep = tempStepsMap.get(stepName)!; - existingStep.order = Math.min(existingStep.order || 0, order); - // 重新计算是否为主步骤 - existingStep.isMainStep = existingStep.order === Math.floor(existingStep.order); - } - - const stepItem = tempStepsMap.get(stepName)!; - - // 根据节点类型设置相应的时间字段 - if (config.timeFieldId) { - try { - const posTimeField = await posTable.getFieldById(config.timeFieldId); - const timeValue = await posTimeField.getValue(currentPosRecordId) as number | null; - - if (config.processNodeValue?.includes('预计')) { - stepItem.expectTimeFieldId = config.timeFieldId; - stepItem.expectTimeValue = timeValue || undefined; - } else if (config.processNodeValue?.includes('实际') && !config.processNodeValue?.includes('开始')) { - stepItem.actualTimeFieldId = config.timeFieldId; - stepItem.actualTimeValue = timeValue || undefined; - if (timeValue) stepItem.finished = true; - } else if (config.processNodeValue === PROCESS_NODE_START_TIME || config.processNodeValue?.includes('实际开始')) { - stepItem.startTimeFieldId = config.timeFieldId; - stepItem.startTimeValue = timeValue || undefined; - } - } catch (e) { - console.warn(`Error processing time field ${config.timeFieldId} for step ${stepName} (node: ${config.processNodeValue}):`, e); - Toast.warning(`处理步骤 ${stepName} 的时间字段 ${config.timeFieldId} 出错`); - } - } - } - - // 按顺序分组步骤 - const loadedSteps = Array.from(tempStepsMap.values()).sort((a, b) => (a.order || 0) - (b.order || 0)); - - // 处理并行组 - 相同整数顺序的步骤形成并行组 - const parallelGroups = new Map(); - loadedSteps.forEach(step => { - const mainOrder = Math.floor(step.order || 0); // 使用order的整数部分作为分组键 - if (!parallelGroups.has(mainOrder)) { - parallelGroups.set(mainOrder, []); - } - parallelGroups.get(mainOrder)?.push(step); - }); - - // 重构步骤数组,将并行组封装为主步骤 - const revisedSteps: StepItem[] = []; - parallelGroups.forEach((groupSteps, mainOrder) => { - // 条件:组内多于一个步骤,或者组内只有一个步骤但其原始order包含小数 - const shouldFormParallelBlock = groupSteps.length > 1 || - (groupSteps.length === 1 && groupSteps[0].order !== Math.floor(groupSteps[0].order || 0)); - - if (!shouldFormParallelBlock && groupSteps.length === 1) { - // 单个普通步骤(order为整数且不成组)直接添加 - revisedSteps.push(groupSteps[0]); - } else { - // 形成并行组 (包含单个小数order的步骤,如16.1自己也形成一个"并行组") - let requiresAll = true; // 默认需要所有子步骤完成 - // 检查组内是否有任何一个步骤的order以.2结尾 - if (groupSteps.some(s => s.order && s.order.toString().endsWith('.2'))) { - requiresAll = false; - } - - // 父并行步骤的名称 - // 如果并行块仅由一个原始步骤构成(例如,order为16.1的步骤单独形成一个并行块),则父步骤名称可以就是该原始步骤的名称。 - // 否则,组合所有子步骤名称。 - const parallelStepName = groupSteps.length === 1 - ? groupSteps[0].name - : `并行处理组 ${mainOrder}`; - - revisedSteps.push({ - name: parallelStepName, - fieldId: `parallel_${mainOrder}`, - finished: requiresAll - ? groupSteps.every(s => s.finished) - : groupSteps.some(s => s.finished), // 完成状态根据requireAllSubSteps判断 - order: mainOrder, // 父并行步骤使用整数order进行排序 - parallelSteps: groupSteps.sort((a,b) => (a.order || 0) - (b.order || 0)), // 子步骤按原始order排序 - requireAllSubSteps: requiresAll, - // 尝试从第一个子步骤或具有整数order的子步骤继承时间字段 - expectTimeFieldId: groupSteps.find(s => s.order === mainOrder)?.expectTimeFieldId || groupSteps[0]?.expectTimeFieldId, - expectTimeValue: groupSteps.find(s => s.order === mainOrder)?.expectTimeValue || groupSteps[0]?.expectTimeValue, - startTimeFieldId: groupSteps.find(s => s.order === mainOrder)?.startTimeFieldId || groupSteps[0]?.startTimeFieldId, - startTimeValue: groupSteps.find(s => s.order === mainOrder)?.startTimeValue || groupSteps[0]?.startTimeValue, - parallelGroup: mainOrder, // 用于UI分组显示的标识 - isMainStep: true // 并行组作为一个整体视为主步骤 - }); - } - }); - - // 按顺序排序最终的步骤列表 (并行组和普通步骤) - const sortedSteps = revisedSteps.sort((a, b) => (a.order || 0) - (b.order || 0)); - - // 处理步骤间的开始时间关联 - for (let i = 0; i < sortedSteps.length; i++) { - const currentStep = sortedSteps[i]; - - // 第一个步骤的开始时间不依赖于前一步骤 - if (i === 0) continue; - - // 查找上一个已完成的主步骤(包括并行组) - let previousMainStepIndex = i - 1; - while (previousMainStepIndex >= 0 && !sortedSteps[previousMainStepIndex].isMainStep) { - previousMainStepIndex--; - } - - // 如果找到了上一个主步骤 - if (previousMainStepIndex >= 0) { - const previousMainStep = sortedSteps[previousMainStepIndex]; - - // 如果前一个主步骤已完成,且当前步骤没有开始时间,设置当前步骤的开始时间为前一步骤的完成时间 - if (previousMainStep.finished && previousMainStep.actualTimeValue && - (!currentStep.startTimeValue || currentStep.startTimeValue < previousMainStep.actualTimeValue)) { - - currentStep.startTimeValue = previousMainStep.actualTimeValue; - - // 如果当前步骤有开始时间字段ID,也更新该字段的值 - if (currentStep.startTimeFieldId && selectedRecordId) { - try { - const startTimeField = await posTable.getFieldById(currentStep.startTimeFieldId); - await startTimeField.setValue(selectedRecordId, previousMainStep.actualTimeValue); - console.log(`Set step ${i} (${currentStep.name}) start time to previous main step's finish time: ${new Date(previousMainStep.actualTimeValue).toLocaleString()}`); - } catch (e) { - console.warn(`Failed to update start time field for step ${i} (${currentStep.name}):`, e); - } - } - } - } - } - - console.log('Loaded steps:', JSON.stringify(sortedSteps, null, 2)); - setSteps(sortedSteps); - - // 确定当前步骤 - const firstUnfinished = sortedSteps.findIndex(s => !s.finished); - setCurrentStep(firstUnfinished === -1 ? sortedSteps.length : firstUnfinished); - - } catch (error) { - console.error('Failed to load process steps:', error); - Toast.error('加载流程步骤失败,请检查控制台获取更多信息。'); - setSteps([]); - } finally { - setLoading(false); - setIsChangingRecord(false); - console.log('Finished loading process steps.'); - } - }; - - // 监听表格选择变化 - useEffect(() => { - const off = bitable.base.onSelectionChange(async (event) => { - if (event.data && event.data.tableId === POS_TABLE_ID && event.data.recordId) { - setSelectedRecordId(event.data.recordId); - // 只在配置数据加载完成后才加载步骤 - if (!processConfigLoading) { - await loadProcessSteps(event.data.recordId); - } - } else if (!event.data.recordId) { - setSelectedRecordId(undefined); - setSteps([]); - setCurrentStep(0); - setHasNoProcess(false); - } - }); - return () => { - off(); - }; - }, [POS_TABLE_ID, processConfigLoading]); - - // 监听刷新标志 - useEffect(() => { - if (refreshSteps && selectedRecordId) { - refreshProcessSteps(); - setRefreshSteps(false); - } - }, [refreshSteps, selectedRecordId]); - - // 处理步骤完成 - const handleFinishStep = async (stepIndex: number, subStepIndex?: number) => { - if (!selectedRecordId) { - Toast.error('没有选中的POS记录'); - return; - } - - const newSteps = [...steps]; - let stepToComplete: StepItem; - let isSubStep = typeof subStepIndex === 'number'; - - // 确定要完成的步骤 - if (isSubStep && newSteps[stepIndex] && newSteps[stepIndex].parallelSteps) { - stepToComplete = newSteps[stepIndex].parallelSteps![subStepIndex!]; - } else { - stepToComplete = newSteps[stepIndex]; - } - - // 检查步骤是否存在 - if (!stepToComplete) { - Toast.error('找不到要完成的步骤'); - return; - } - - // 检查步骤是否已经完成 - if (stepToComplete.finished && stepToComplete.actualTimeValue) { - Toast.info(`步骤 "${stepToComplete.name}" 已于 ${formatTimestamp(stepToComplete.actualTimeValue)} 完成`); - return; - } - - setLoading(true); - - try { - const posTable = await bitable.base.getTableById(POS_TABLE_ID); - - // 如果没有配置实际完成时间字段,仅标记为完成 - if (!stepToComplete.actualTimeFieldId) { - Toast.warning(`步骤 "${stepToComplete.name}" 未配置实际完成时间字段ID,无法自动记录时间。将仅标记为完成。`); - stepToComplete.finished = true; + if (templateField.type === FieldType.MultiSelect) { + const field = await posTable.getField(templateField.id); + const fieldOptions = await field.getOptions(); + options = fieldOptions.map(option => ({ label: option.name, value: option.id })); + } else if (templateField.type === FieldType.SingleSelect) { + const field = await posTable.getField(templateField.id); + const fieldOptions = await field.getOptions(); + options = fieldOptions.map(option => ({ label: option.name, value: option.id })); } else { - // 保存步骤完成时间 - const completionTime = await StepService.saveStepCompletionTime( - posTable, selectedRecordId, stepToComplete, isSubStep - ); - Toast.success(`步骤 "${stepToComplete.name}" 时间已记录`); + const records = await posTable.getRecords({ pageSize: 1000 }); + const uniqueTemplates = new Set(); - // 如果是主步骤完成,更新后续步骤的开始时间 - if (!isSubStep && completionTime) { - await StepService.updateSubsequentStepsStartTimes( - posTable, selectedRecordId, newSteps, stepIndex, completionTime - ); + for (const record of records.records) { + const fields = await record.fields; + const templateValue = fields[templateField.id]; - // 强制刷新步骤数据 - setRefreshSteps(true); - } - } - - // 处理子步骤完成的特殊逻辑 - if (isSubStep) { - const parentStep = newSteps[stepIndex]; - - // 如果父步骤要求所有子步骤完成才标记为完成 - if (parentStep.requireAllSubSteps && parentStep.parallelSteps?.every(ps => ps.finished)) { - parentStep.finished = true; - - // 如果父步骤也有完成时间字段,记录完成时间 - if (parentStep.actualTimeFieldId) { - try { - const completionTime = await StepService.saveStepCompletionTime(posTable, selectedRecordId, parentStep); - if (completionTime) { - // 更新父步骤后的步骤开始时间 - await StepService.updateSubsequentStepsStartTimes( - posTable, selectedRecordId, newSteps, stepIndex, completionTime - ); - - // 强制刷新步骤数据 - setRefreshSteps(true); - } - } catch (e) { - console.error("Error saving parent step completion time:", e); - } - } else { - // 如果没有完成时间字段,也需要更新后续步骤 - setRefreshSteps(true); - } - } else if (!parentStep.requireAllSubSteps && parentStep.parallelSteps?.some(ps => ps.finished)) { - // 任一子步骤完成即可推进主步骤 - parentStep.finished = true; - - // 如果父步骤有完成时间字段,记录完成时间 - if (parentStep.actualTimeFieldId) { - try { - const completionTime = await StepService.saveStepCompletionTime(posTable, selectedRecordId, parentStep); - if (completionTime) { - // 更新父步骤后的步骤开始时间 - await StepService.updateSubsequentStepsStartTimes( - posTable, selectedRecordId, newSteps, stepIndex, completionTime - ); - - // 强制刷新步骤数据 - setRefreshSteps(true); - } - } catch (e) { - console.error("Error saving parent step completion time:", e); - } - } else { - // 如果没有完成时间字段,也需要更新后续步骤 - setRefreshSteps(true); + if (typeof templateValue === 'string') { + uniqueTemplates.add(templateValue); + } else if (Array.isArray(templateValue)) { + templateValue.forEach(value => { + const text = typeof value === 'object' && value.text ? value.text : String(value); + if (text) uniqueTemplates.add(text); + }); + } else if (templateValue?.text) { + uniqueTemplates.add(templateValue.text); } } + + options = Array.from(uniqueTemplates).map(template => ({ + label: template, + value: template + })); } - // 更新步骤状态和当前步骤 - setSteps(newSteps); - const firstUnfinished = newSteps.findIndex(s => !s.finished); - setCurrentStep(firstUnfinished === -1 ? newSteps.length : firstUnfinished); - } catch (e) { - console.error("Error finishing step:", e); - Toast.error(`完成步骤 "${stepToComplete.name}" 失败`); - } finally { - setLoading(false); + updateInsertState({ templateOptions: options }); + } catch (error) { + Toast.error(`获取流程模板选项失败: ${error instanceof Error ? error.message : '未知错误'}`); } }; - // 确定当前并行组是否为活动组 - const isActiveParallelGroup = (step: StepItem): boolean => { - // 如果当前步骤是并行组,则该并行组是活动的 - if (step.parallelSteps && currentStep === steps.findIndex(s => s === step)) { - return true; + // 处理插入确认 + const handleInsertConfirm = async () => { + const { selectedTemplate, currentRecord, direction } = insertState; + + if (!selectedTemplate.length) { + Toast.warning('请选择一个或多个流程模板'); + return; } - // 如果当前步骤是并行组中的子步骤,则其父并行组是活动的活动的 - if (currentStep < steps.length && steps[currentStep].parallelSteps) { - const currentParallelStep = steps[currentStep]; - // 检查当前步骤的并行组ID是否与参数步骤的并行组ID相同 - return currentParallelStep.parallelGroup === step.parallelGroup; + if (!currentRecord) { + Toast.error('系统状态不完整,无法执行操作'); + return; } - - return false; - }; - // 判断并行组中是否有任何子步骤已完成 - const isAnySubStepFinished = (step: StepItem): boolean => { - if (!step.parallelSteps) return false; - return step.parallelSteps.some(subStep => subStep.finished); - }; - - // 判断是否可以访问子步骤 - const canAccessSubSteps = (step: StepItem): boolean => { - // 如果是当前步骤或活动并行组,当然可以访问 - if (isActiveParallelGroup(step)) return true; - - // 如果是已完成的并行组,也可以访问 - if (step.finished) return true; - - // 如果并行组中已有任何子步骤完成,也可以访问 - if (isAnySubStepFinished(step)) return true; - - // 如果并行组的前置步骤(主步骤)已完成,也可以访问 - const stepIndex = steps.findIndex(s => s === step); - if (stepIndex > 0) { - // 查找上一个主步骤 - let previousMainStepIndex = stepIndex - 1; - while (previousMainStepIndex >= 0 && !steps[previousMainStepIndex].isMainStep) { - previousMainStepIndex--; - } + try { + updateAppState({ loading: true }); - // 如果找到了上一个主步骤且它已完成 - if (previousMainStepIndex >= 0 && steps[previousMainStepIndex].finished) { - return true; - } + const processFields = await tableState.processTable!.getFieldMetaList(); + const processOrderField = processFields.find(field => field.name === CONSTANTS.PROCESS_ORDER_FIELD); + + if (!processOrderField) throw new Error('未找到"流程顺序"字段'); + + const currentOrder = currentRecord.fields[processOrderField.id]; + if (currentOrder == null) throw new Error('当前记录缺少"流程顺序"值'); + + const newOrder = Number(currentOrder) + (direction === 'up' ? -0.1 : 0.1); + + const callbackData = { + selectedTemplates: selectedTemplate, + processOrder: currentOrder, + insertDirection: direction, + newProcessOrder: newOrder, + recordId: appState.selectedRecordId, + timestamp: new Date().toISOString() + }; + + await fetch(CONSTANTS.FEISHU_CALLBACK_URL, { + method: 'POST', + mode: 'no-cors', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(callbackData) + }); + + Toast.success('数据已发送到飞书回调'); + + } catch (error) { + Toast.error(`操作失败: ${error instanceof Error ? error.message : '未知错误'}`); + } finally { + updateAppState({ loading: false }); + resetInsertState(); } - - return false; }; - // 渲染加载状态 - if (processConfigLoading) { - return ( -
- 流程进度 -
- -
-
- ); - } + // 重置插入状态 + const resetInsertState = () => { + updateInsertState({ + modalVisible: false, + currentRecord: null, + direction: null, + selectedTemplate: [], + templateOptions: [] + }); + }; + + // 渲染插入按钮 + const renderInsertButton = (record: ProcessRecord) => { + const handleDirectionSelect = (direction: 'up' | 'down') => { + updateInsertState({ + direction, + currentRecord: record, + modalVisible: true + }); + loadTemplateOptions(); + }; - // 渲染无流程记录提示 - if (hasNoProcess && selectedRecordId) { return ( -
- 流程进度 -
- - 此记录没有配置流程 + + {['up', 'down'].map(dir => ( +
handleDirectionSelect(dir as 'up' | 'down')} + style={{ padding: '8px 16px', cursor: 'pointer' }} + > + {dir === 'up' ? '向上插入' : '向下插入'} +
+ ))} +
+ } + > + + + ); + }; + + // 切换列显示 + const toggleColumns = () => { + const newShowAll = !appState.showAllColumns; + const columnsToShow = newShowAll + ? tableState.allTableColumns + : tableState.allTableColumns.filter(col => CONSTANTS.DEFAULT_COLUMNS.includes(col.title)); + + updateAppState({ showAllColumns: newShowAll }); + updateTableState({ tableColumns: columnsToShow }); + }; + + // Effects + useEffect(() => { + initializeTables(); + }, []); + + useEffect(() => { + let cleanup: (() => void) | undefined; + + const setupSelectionListener = async () => { + const { posTable, processTable, posRecordIdFieldId } = tableState; + if (!posTable || !processTable || !posRecordIdFieldId) return; + + try { + const meta = await posTable.getMeta(); + cleanup = bitable.base.onSelectionChange(async (event) => { + if (event.data?.tableId === meta.id && event.data?.recordId) { + updateAppState({ selectedRecordId: event.data.recordId }); + await loadProcessRecords(event.data.recordId); + } + }); + } catch (error) { + console.error('设置选择监听失败:', error); + } + }; + + setupSelectionListener(); + return () => cleanup?.(); + }, [tableState.posTable, tableState.processTable, tableState.posRecordIdFieldId]); + + // 渲染组件 + const renderProgressInfo = () => { + const { currentProgress, nextProgress, parallelProgress } = appState; + if (!currentProgress && !nextProgress) return null; + + return ( + + + 进度信息 + +
+ {currentProgress && ( +
+ + 当前进度 - } - /> - + + {currentProgress} + +
+ )} + {nextProgress && ( +
+ + 下个进度 + + + {nextProgress} + +
+ )} + {parallelProgress.length > 0 && ( +
+ + 下个进度-并行进度 + + + {parallelProgress.join(', ')} + +
+ )}
-
+ ); - } + }; - // 渲染切换记录时的加载状态 - if (isChangingRecord) { - return ( -
- 流程进度 -
- - - 正在加载流程数据... - - - 请稍候,系统正在为您获取当前记录的流程信息 - -
-
- ); - } + const renderUsageInstructions = () => ( + + + 使用说明 + + + 1. 在 {CONSTANTS.POS_TABLE_NAME} 表中点击选择任意单元格
+ 2. 系统将自动获取该记录的ID
+ 3. 在 {CONSTANTS.PROCESS_TABLE_NAME} 表中筛选 {CONSTANTS.POS_RECORD_ID_FIELD_NAME} 字段匹配的记录
+ 4. 在下方表格中展示筛选结果
+ 5. 点击"展开所有字段"按钮可查看完整信息 +
+
+ ); - // 渲染主界面 return (
- 流程进度 - - {steps.map((step, idx) => ( - 订单流程查询 + + {/* 当前选中记录信息 */} + + + 当前选中记录 + + {appState.selectedRecordId ? ( + + 记录ID: {appState.selectedRecordId} + + ) : ( + + 请在 {CONSTANTS.POS_TABLE_NAME} 表中选择一条记录 + + )} + + + {/* 筛选结果 */} + {appState.selectedRecordId && ( + <> + {renderProgressInfo()} + + +
+ + 匹配的订单流程记录 + +
+ + {appState.loading && } +
+
+ + {appState.processRecords.length > 0 ? ( + `共 ${total} 条记录` + }} + rowKey="recordId" + size="small" + scroll={{ x: 'max-content' }} + style={{ marginTop: 16 }} /> - } - /> - ))} - + ) : ( + !appState.loading && ( + + 未找到匹配的订单流程记录 + + } + style={{ marginTop: 20 }} + /> + ) + )} + + + )} + + {/* 使用说明 */} + {!appState.selectedRecordId && renderUsageInstructions()} + + {/* 流程模板选择弹窗 */} + +
+ 请选择要插入的流程模板: +
+ + + + {insertState.selectedTemplate.length > 0 && ( +
+ + 已选择: {insertState.selectedTemplate.map(id => + insertState.templateOptions.find(opt => opt.value === id)?.label + ).filter(Boolean).join(', ')} + +
+ )} +
); -} +} \ No newline at end of file