1202 lines
48 KiB
TypeScript
1202 lines
48 KiB
TypeScript
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 './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' });
|
||
};
|
||
|
||
// 步骤管理服务类
|
||
class StepService {
|
||
// 保存步骤完成时间
|
||
static async saveStepCompletionTime(
|
||
posTable: ITable,
|
||
selectedRecordId: string,
|
||
stepItem: StepItem,
|
||
isSubStep: boolean = false
|
||
): Promise<number | null> {
|
||
if (!selectedRecordId || !stepItem.actualTimeFieldId) return null;
|
||
|
||
try {
|
||
const timeField = await posTable.getFieldById<IDateTimeField>(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<IDateTimeField>(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<IDateTimeField>(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<IDateTimeField>(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<IDateTimeField>(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("更新步骤开始时间失败");
|
||
}
|
||
}
|
||
}
|
||
|
||
// 判断并行步骤是否完成
|
||
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;
|
||
loading: boolean;
|
||
onFinishStep: (stepIndex: number, subStepIndex?: number) => void;
|
||
formatTimestamp: (timestamp: number | undefined) => string;
|
||
isCurrentStep: boolean;
|
||
isActiveParallelGroup: boolean;
|
||
isAnySubStepFinished: boolean;
|
||
canAccessSubSteps: boolean;
|
||
}) => {
|
||
return (
|
||
<div className="step-content-container" style={{ minWidth: 300, marginTop: step.parallelSteps ? 10 : 0 }}>
|
||
{/* 并行步骤组标题 */}
|
||
{step.parallelSteps && (
|
||
<div className="parallel-group-header" style={{
|
||
padding: '10px 16px',
|
||
backgroundColor: '#f5f7fa',
|
||
borderRadius: '4px',
|
||
marginBottom: '16px',
|
||
borderLeft: '4px solid #165DFF'
|
||
}}>
|
||
<Typography.Text strong style={{ fontSize: 16, color: '#165DFF' }}>
|
||
并行处理组: {step.name}
|
||
</Typography.Text>
|
||
<Typography.Text style={{ fontSize: 14, color: '#86909c', marginTop: 4, display: 'block' }}>
|
||
{step.requireAllSubSteps ? '所有子步骤完成后自动推进' : '任一子步骤完成后自动推进'}
|
||
</Typography.Text>
|
||
</div>
|
||
)}
|
||
|
||
{/* 普通步骤的时间信息展示 */}
|
||
{!step.parallelSteps && (
|
||
<div className="step-time-info" style={{
|
||
margin: '16px 0 8px 0',
|
||
padding: '12px',
|
||
border: '1px solid #eee',
|
||
borderRadius: '4px'
|
||
}}>
|
||
<Typography.Text strong style={{color: '#000', fontSize: 14, marginBottom: 8, display: 'block'}}>
|
||
预计完成时间:{formatTimestamp(step.expectTimeValue)}
|
||
</Typography.Text>
|
||
<Typography.Text strong style={{color: '#000', fontSize: 14, marginBottom: 8, display: 'block'}}>
|
||
实际开始时间:{formatTimestamp(step.startTimeValue)}
|
||
</Typography.Text>
|
||
<Typography.Text strong style={{color: '#000', fontSize: 14, marginBottom: 8, display: 'block'}}>
|
||
实际完成时间:{formatTimestamp(step.actualTimeValue)}
|
||
</Typography.Text>
|
||
<Typography.Text strong style={{
|
||
color: step.actualTimeValue && step.expectTimeValue && 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 小时') : '-'}
|
||
</Typography.Text>
|
||
</div>
|
||
)}
|
||
|
||
{/* 并行子步骤 */}
|
||
{step.parallelSteps && (
|
||
<div className="parallel-steps-container" style={{
|
||
display: 'flex',
|
||
flexDirection: 'row',
|
||
gap: 16,
|
||
marginTop: 10,
|
||
flexWrap: 'wrap'
|
||
}}>
|
||
{step.parallelSteps.map((sub, subIdx) => (
|
||
<div
|
||
key={subIdx}
|
||
className="sub-step-card"
|
||
style={{
|
||
flex: 1,
|
||
minWidth: 280,
|
||
border: '1px solid #eee',
|
||
borderRadius: 4,
|
||
padding: 12,
|
||
backgroundColor: '#fafafa',
|
||
transition: 'all 0.3s',
|
||
position: 'relative'
|
||
}}
|
||
>
|
||
{/* 完成状态标识 */}
|
||
{sub.finished && (
|
||
<div className="sub-step-finished-badge" style={{
|
||
position: 'absolute',
|
||
top: '8px',
|
||
right: '8px',
|
||
backgroundColor: '#52c41a',
|
||
color: '#fff',
|
||
fontSize: '12px',
|
||
padding: '2px 6px',
|
||
borderRadius: '4px'
|
||
}}>
|
||
已完成
|
||
</div>
|
||
)}
|
||
|
||
<Typography.Text strong className="sub-step-title" style={{ fontSize: 15 }}>{sub.name}</Typography.Text>
|
||
|
||
<div className="sub-step-time-info" style={{ margin: '10px 0 8px 0' }}>
|
||
<Typography.Text strong style={{color: '#000', fontSize: 13, marginBottom: 8, display: 'block'}}>
|
||
预计完成时间:{formatTimestamp(sub.expectTimeValue || step.expectTimeValue)}
|
||
</Typography.Text>
|
||
<Typography.Text strong style={{color: '#000', fontSize: 13, marginBottom: 8, display: 'block'}}>
|
||
实际开始时间:{formatTimestamp(sub.startTimeValue || step.startTimeValue)}
|
||
</Typography.Text>
|
||
<Typography.Text strong style={{color: '#000', fontSize: 13, marginBottom: 8, display: 'block'}}>
|
||
实际完成时间:{formatTimestamp(sub.actualTimeValue)}
|
||
</Typography.Text>
|
||
<Typography.Text strong style={{
|
||
color: sub.actualTimeValue && (sub.expectTimeValue || step.expectTimeValue) &&
|
||
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) + ' 小时' : '-')}
|
||
</Typography.Text>
|
||
</div>
|
||
|
||
{/* 允许活动并行组中的子步骤被点击,或并行组中已有子步骤完成 */}
|
||
{!sub.finished && (isCurrentStep || isActiveParallelGroup || isAnySubStepFinished || canAccessSubSteps) && (
|
||
<Button
|
||
theme="solid"
|
||
size="small"
|
||
loading={loading}
|
||
onClick={() => onFinishStep(idx, subIdx)}
|
||
style={{ marginTop: 8 }}
|
||
>
|
||
完成此子步骤
|
||
</Button>
|
||
)}
|
||
|
||
{/* 只有当父步骤未完成且不在当前活动状态时才显示等待提示 */}
|
||
{!sub.finished && !(isCurrentStep || isActiveParallelGroup || isAnySubStepFinished || canAccessSubSteps) && (
|
||
<div style={{ marginTop: 8, padding: '6px 12px', backgroundColor: '#f2f3f5', borderRadius: 4, textAlign: 'center' }}>
|
||
<Typography.Text style={{ fontSize: 13, color: '#86909c' }}>
|
||
等待前置步骤完成
|
||
</Typography.Text>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* 当前步骤的完成按钮 */}
|
||
{idx === currentStep && !step.finished && !step.parallelSteps && (
|
||
<Button
|
||
theme="solid"
|
||
size="default"
|
||
loading={loading}
|
||
onClick={() => onFinishStep(idx)}
|
||
style={{ marginTop: 16 }}
|
||
>
|
||
完成此步骤
|
||
</Button>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default function App() {
|
||
const [dateFields, setDateFields] = useState<IDateTimeFieldMeta[]>([]);
|
||
const [steps, setSteps] = useState<StepItem[]>([]);
|
||
const [currentStep, setCurrentStep] = useState(0);
|
||
const [loading, setLoading] = useState(false);
|
||
const [selectedRecordId, setSelectedRecordId] = useState<string | undefined>();
|
||
const [refreshSteps, setRefreshSteps] = useState<boolean>(false);
|
||
const [isChangingRecord, setIsChangingRecord] = useState(false); // 记录是否正在切换记录
|
||
const [hasNoProcess, setHasNoProcess] = useState(false); // 记录是否没有配置流程
|
||
|
||
// 缓存所有流程配置数据
|
||
const [allProcessConfigs, setAllProcessConfigs] = useState<ProcessConfigItem[]>([]);
|
||
const [processConfigLoading, setProcessConfigLoading] = useState(true);
|
||
// 缓存字段元数据,避免重复获取
|
||
const [fieldMetadataCache, setFieldMetadataCache] = useState<Map<string, any>>(new Map());
|
||
// 缓存按POS记录ID分组的配置
|
||
const [configsByPosLink, setConfigsByPosLink] = useState<Map<string, ProcessConfigItem[]>>(new Map());
|
||
|
||
// 预加载所有流程配置数据
|
||
async function fetchAllProcessConfigs() {
|
||
setProcessConfigLoading(true);
|
||
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<string, ProcessConfigItem[]>();
|
||
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);
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
fetchAllProcessConfigs();
|
||
}, []);
|
||
|
||
// 初始化日期字段
|
||
useEffect(() => {
|
||
async function fetchDateFieldsForUI() {
|
||
try {
|
||
const activeTable = await bitable.base.getActiveTable();
|
||
const fields = await activeTable.getFieldMetaListByType<IDateTimeFieldMeta>(FieldType.DateTime);
|
||
setDateFields(fields);
|
||
} catch (e) {
|
||
console.error("Failed to fetch date fields for UI: ", e);
|
||
Toast.error("获取可选日期字段列表失败");
|
||
}
|
||
}
|
||
fetchDateFieldsForUI();
|
||
}, []);
|
||
|
||
// 优化:初始化步骤时处理并行组
|
||
const initializeStepsWithParallel = (configs: ProcessConfigItem[]) => {
|
||
if (configs.length === 0) return [];
|
||
|
||
// 使用Map按order分组,提高查询效率
|
||
const stepsByOrder = new Map<number, StepItem[]>();
|
||
|
||
// 初始化步骤
|
||
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) // 标记是否为主步骤(整数顺序)
|
||
};
|
||
|
||
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 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.');
|
||
return;
|
||
}
|
||
|
||
setLoading(true);
|
||
setIsChangingRecord(true);
|
||
setHasNoProcess(false); // 重置状态
|
||
console.log('Loading process steps from cached configs...');
|
||
|
||
try {
|
||
const posTable = await bitable.base.getTableByName(POS_TABLE_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);
|
||
return;
|
||
}
|
||
|
||
// 按流程顺序分组配置
|
||
const stepsByOrder = new Map<number, StepItem[]>();
|
||
const tempStepsMap = new Map<string, StepItem>();
|
||
|
||
// 首先处理每个步骤的基本信息
|
||
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<IDateTimeField>(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<number, StepItem[]>();
|
||
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<IDateTimeField>(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;
|
||
} else {
|
||
// 保存步骤完成时间
|
||
const completionTime = await StepService.saveStepCompletionTime(
|
||
posTable, selectedRecordId, stepToComplete, isSubStep
|
||
);
|
||
Toast.success(`步骤 "${stepToComplete.name}" 时间已记录`);
|
||
|
||
// 如果是主步骤完成,更新后续步骤的开始时间
|
||
if (!isSubStep && completionTime) {
|
||
await StepService.updateSubsequentStepsStartTimes(
|
||
posTable, selectedRecordId, newSteps, stepIndex, completionTime
|
||
);
|
||
|
||
// 强制刷新步骤数据
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新步骤状态和当前步骤
|
||
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);
|
||
}
|
||
};
|
||
|
||
// 确定当前并行组是否为活动组
|
||
const isActiveParallelGroup = (step: StepItem): boolean => {
|
||
// 如果当前步骤是并行组,则该并行组是活动的
|
||
if (step.parallelSteps && currentStep === steps.findIndex(s => s === step)) {
|
||
return true;
|
||
}
|
||
|
||
// 如果当前步骤是并行组中的子步骤,则其父并行组是活动的活动的
|
||
if (currentStep < steps.length && steps[currentStep].parallelSteps) {
|
||
const currentParallelStep = steps[currentStep];
|
||
// 检查当前步骤的并行组ID是否与参数步骤的并行组ID相同
|
||
return currentParallelStep.parallelGroup === step.parallelGroup;
|
||
}
|
||
|
||
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--;
|
||
}
|
||
|
||
// 如果找到了上一个主步骤且它已完成
|
||
if (previousMainStepIndex >= 0 && steps[previousMainStepIndex].finished) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
};
|
||
|
||
// 渲染加载状态
|
||
if (processConfigLoading) {
|
||
return (
|
||
<div className="app-container" style={{ padding: 24, minWidth: 410 }}>
|
||
<Typography.Title heading={4}>流程进度</Typography.Title>
|
||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
|
||
<Spin size="large" tip="加载中..." />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 渲染无流程记录提示
|
||
if (hasNoProcess && selectedRecordId) {
|
||
return (
|
||
<div className="app-container" style={{ padding: 24, minWidth: 410 }}>
|
||
<Typography.Title heading={4}>流程进度</Typography.Title>
|
||
<div style={{ marginTop: 24, textAlign: 'center' }}>
|
||
<Empty
|
||
description={
|
||
<Typography.Text style={{ fontSize: 16, color: '#4e5969' }}>
|
||
此记录没有配置流程
|
||
</Typography.Text>
|
||
}
|
||
/>
|
||
<Button
|
||
onClick={async () => { // 修改为 async 函数
|
||
setHasNoProcess(false);
|
||
await fetchAllProcessConfigs(); // 首先重新获取所有流程配置
|
||
if (selectedRecordId) {
|
||
await loadProcessSteps(selectedRecordId); // 然后使用新的配置加载步骤
|
||
}
|
||
}}
|
||
style={{ marginTop: 20 }}
|
||
loading={processConfigLoading || loading} // 当任一加载进行中时显示loading
|
||
>
|
||
刷新重试
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 渲染切换记录时的加载状态
|
||
if (isChangingRecord) {
|
||
return (
|
||
<div className="app-container" style={{ padding: 24, minWidth: 410 }}>
|
||
<Typography.Title heading={4}>流程进度</Typography.Title>
|
||
<div style={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
height: '50vh',
|
||
backgroundColor: '#fafafa',
|
||
borderRadius: 8,
|
||
border: '1px solid #e5e6eb'
|
||
}}>
|
||
<Spin
|
||
size="large"
|
||
style={{ marginBottom: 20 }}
|
||
/>
|
||
<Typography.Title
|
||
heading={5}
|
||
style={{
|
||
fontSize: 18,
|
||
fontWeight: 500,
|
||
color: '#4e5969',
|
||
textAlign: 'center'
|
||
}}
|
||
>
|
||
正在加载流程数据...
|
||
</Typography.Title>
|
||
<Typography.Text
|
||
style={{
|
||
marginTop: 8,
|
||
color: '#86909c',
|
||
fontSize: 14
|
||
}}
|
||
>
|
||
请稍候,系统正在为您获取当前记录的流程信息
|
||
</Typography.Text>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 渲染主界面
|
||
return (
|
||
<div className="app-container" style={{ padding: 24, minWidth: 410 }}>
|
||
<Typography.Title heading={4}>流程进度</Typography.Title>
|
||
<Steps direction="vertical" type="basic" current={currentStep} style={{ margin: '28px 0' }}>
|
||
{steps.map((step, idx) => (
|
||
<Steps.Step
|
||
key={step.name}
|
||
title={step.name}
|
||
status={step.finished ? 'finish' : idx === currentStep ? 'process' : 'wait'}
|
||
description={
|
||
<StepCard
|
||
step={step}
|
||
idx={idx}
|
||
currentStep={currentStep}
|
||
loading={loading}
|
||
onFinishStep={handleFinishStep}
|
||
formatTimestamp={formatTimestamp}
|
||
isCurrentStep={idx === currentStep}
|
||
isActiveParallelGroup={isActiveParallelGroup(step)}
|
||
isAnySubStepFinished={isAnySubStepFinished(step)}
|
||
canAccessSubSteps={canAccessSubSteps(step)}
|
||
/>
|
||
}
|
||
/>
|
||
))}
|
||
</Steps>
|
||
</div>
|
||
);
|
||
}
|