|
|
@@ -0,0 +1,2164 @@
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
+import { useNavigate } from 'react-router-dom';
|
|
|
+import { Eye, Search, RotateCcw } from 'lucide-react';
|
|
|
+import { Button, Space, Table as AntdTable, Input, message, Modal, Form as AntdForm, Card, Alert, Select, DatePicker, InputNumber, Switch, Radio, Checkbox, Cascader, Upload } from 'antd';
|
|
|
+import { UploadOutlined, LockOutlined, KeyOutlined } from '@ant-design/icons';
|
|
|
+import type { ColumnsType } from 'antd/es/table';
|
|
|
+import { toast } from 'sonner';
|
|
|
+import dayjs, { Dayjs } from 'dayjs';
|
|
|
+import { taskManagementApi, MyTaskVO, MyTaskPageParam, PageResponse, MyTaskNodeDetailVO, UpdateNodeApprovalParam } from '../api/mytask';
|
|
|
+import { dateFormatter } from '../utils/formatTime';
|
|
|
+import { DICT_TYPE, getDictLabel } from '../utils/dict';
|
|
|
+import { setConfAndFields2, FormCreateData } from '../utils/formCreate';
|
|
|
+
|
|
|
+// 辅助函数:安全地将值转换为 dayjs 对象
|
|
|
+const safeToDayjs = (value: any): dayjs.Dayjs | null => {
|
|
|
+ if (!value && value !== 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是数字(时间戳)
|
|
|
+ if (typeof value === 'number') {
|
|
|
+ // dayjs 默认将数字当作毫秒级时间戳处理
|
|
|
+ // 但如果数字小于 10000000000(10位),可能是秒级时间戳,需要转换为毫秒
|
|
|
+ let timestamp = value;
|
|
|
+ const originalTimestamp = timestamp;
|
|
|
+ if (timestamp > 0 && timestamp < 10000000000) {
|
|
|
+ // 秒级时间戳,转换为毫秒
|
|
|
+ timestamp = timestamp * 1000;
|
|
|
+ console.log(`时间戳转换: 秒级 -> 毫秒级`, { original: originalTimestamp, converted: timestamp });
|
|
|
+ }
|
|
|
+ const dayjsValue = dayjs(timestamp);
|
|
|
+ if (dayjsValue.isValid()) {
|
|
|
+ console.log(`时间戳解析成功:`, {
|
|
|
+ original: value,
|
|
|
+ timestamp,
|
|
|
+ date: dayjsValue.format('YYYY-MM-DD HH:mm:ss'),
|
|
|
+ year: dayjsValue.year()
|
|
|
+ });
|
|
|
+ return dayjsValue;
|
|
|
+ } else {
|
|
|
+ console.warn(`时间戳解析失败:`, { original: value, timestamp });
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是字符串
|
|
|
+ if (typeof value === 'string' && value.trim() !== '') {
|
|
|
+ // 尝试判断是否为时间戳字符串
|
|
|
+ const numValue = Number(value);
|
|
|
+ if (!isNaN(numValue) && numValue > 0 && numValue.toString() === value.trim()) {
|
|
|
+ // 是纯数字字符串,可能是时间戳
|
|
|
+ // 判断是秒级还是毫秒级时间戳
|
|
|
+ let timestamp = numValue;
|
|
|
+ const originalTimestamp = timestamp;
|
|
|
+ if (timestamp < 10000000000) {
|
|
|
+ // 秒级时间戳(10位数字),转换为毫秒
|
|
|
+ timestamp = timestamp * 1000;
|
|
|
+ console.log(`时间戳转换: 秒级 -> 毫秒级`, { original: originalTimestamp, converted: timestamp });
|
|
|
+ } else {
|
|
|
+ console.log(`时间戳转换: 毫秒级时间戳`, { timestamp });
|
|
|
+ }
|
|
|
+ // 否则是毫秒级时间戳(13位或更多),直接使用
|
|
|
+ const dayjsValue = dayjs(timestamp);
|
|
|
+ if (dayjsValue.isValid()) {
|
|
|
+ console.log(`时间戳解析成功:`, {
|
|
|
+ original: value,
|
|
|
+ timestamp,
|
|
|
+ date: dayjsValue.format('YYYY-MM-DD HH:mm:ss'),
|
|
|
+ year: dayjsValue.year()
|
|
|
+ });
|
|
|
+ return dayjsValue;
|
|
|
+ } else {
|
|
|
+ console.warn(`时间戳解析失败:`, { original: value, timestamp });
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 普通日期字符串
|
|
|
+ const dayjsValue = dayjs(value);
|
|
|
+ return dayjsValue.isValid() ? dayjsValue : null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果已经是 dayjs 对象,检查是否有 isValid 方法
|
|
|
+ if (value && typeof value === 'object' && typeof value.isValid === 'function') {
|
|
|
+ try {
|
|
|
+ return value.isValid() ? value : null;
|
|
|
+ } catch (e) {
|
|
|
+ // isValid 调用失败,尝试重新创建
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是对象,检查是否有 isValid 方法(可能是序列化后的 dayjs 对象)
|
|
|
+ if (value && typeof value === 'object') {
|
|
|
+ // 尝试重新创建 dayjs 对象
|
|
|
+ try {
|
|
|
+ // 如果有 $d 属性,可能是 dayjs 对象
|
|
|
+ if (value.$d) {
|
|
|
+ const dayjsValue = dayjs(value.$d);
|
|
|
+ return dayjsValue.isValid() ? dayjsValue : null;
|
|
|
+ }
|
|
|
+ // 如果有时间戳相关的属性
|
|
|
+ if (value.valueOf && typeof value.valueOf === 'function') {
|
|
|
+ const timestamp = value.valueOf();
|
|
|
+ if (typeof timestamp === 'number') {
|
|
|
+ const dayjsValue = dayjs(timestamp);
|
|
|
+ return dayjsValue.isValid() ? dayjsValue : null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // 转换失败,返回 null
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+};
|
|
|
+
|
|
|
+// 辅助函数:验证 dayjs 对象是否有效
|
|
|
+const isValidDayjs = (value: any): boolean => {
|
|
|
+ if (!value) return false;
|
|
|
+ if (typeof value !== 'object') return false;
|
|
|
+ if (typeof value.isValid !== 'function') return false;
|
|
|
+ try {
|
|
|
+ return value.isValid();
|
|
|
+ } catch (e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 辅助函数:将表单值中的日期字符串转换为 dayjs 对象
|
|
|
+const convertDateValues = (formValues: any, fields: any[]): any => {
|
|
|
+ const fieldTypeMap: { [key: string]: string } = {};
|
|
|
+
|
|
|
+ // 建立字段名到类型的映射
|
|
|
+ fields.forEach((field: any) => {
|
|
|
+ try {
|
|
|
+ const fieldObj = typeof field === 'string' ? JSON.parse(field) : field;
|
|
|
+ const fieldName = fieldObj.name || fieldObj.field;
|
|
|
+ if (fieldName) {
|
|
|
+ fieldTypeMap[fieldName] = fieldObj.type;
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // 解析失败,跳过
|
|
|
+ console.warn('解析字段配置失败:', e);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 转换日期值
|
|
|
+ const convertedValues: any = {};
|
|
|
+ Object.keys(formValues).forEach((fieldName) => {
|
|
|
+ const fieldType = fieldTypeMap[fieldName];
|
|
|
+ const value = formValues[fieldName];
|
|
|
+
|
|
|
+ if (fieldType === 'date' || fieldType === 'datetime' || fieldType === 'timepicker') {
|
|
|
+ // 日期、日期时间或时间选择器类型
|
|
|
+ convertedValues[fieldName] = safeToDayjs(value) || undefined;
|
|
|
+ } else if (fieldType === 'daterange') {
|
|
|
+ // 日期范围类型
|
|
|
+ if (Array.isArray(value) && value.length === 2) {
|
|
|
+ const [start, end] = value;
|
|
|
+ const startDayjs = safeToDayjs(start);
|
|
|
+ const endDayjs = safeToDayjs(end);
|
|
|
+
|
|
|
+ // 调试日志
|
|
|
+ if (startDayjs && endDayjs) {
|
|
|
+ console.log(`日期范围字段 ${fieldName} 转换成功:`, {
|
|
|
+ original: { start, end },
|
|
|
+ converted: {
|
|
|
+ start: startDayjs.format('YYYY-MM-DD HH:mm:ss'),
|
|
|
+ end: endDayjs.format('YYYY-MM-DD HH:mm:ss')
|
|
|
+ }
|
|
|
+ });
|
|
|
+ convertedValues[fieldName] = [startDayjs, endDayjs];
|
|
|
+ } else {
|
|
|
+ console.warn(`日期范围字段 ${fieldName} 转换失败:`, { start, end, startDayjs, endDayjs });
|
|
|
+ convertedValues[fieldName] = undefined;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ convertedValues[fieldName] = undefined;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 其他类型直接使用原值
|
|
|
+ convertedValues[fieldName] = value;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return convertedValues;
|
|
|
+};
|
|
|
+
|
|
|
+export default function TaskManagement() {
|
|
|
+ const navigate = useNavigate();
|
|
|
+
|
|
|
+ const [loading, setLoading] = useState(true);
|
|
|
+ const [list, setList] = useState<MyTaskVO[]>([]);
|
|
|
+ const [total, setTotal] = useState(0);
|
|
|
+ const [queryParams, setQueryParams] = useState<MyTaskPageParam>({
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ key: '',
|
|
|
+ });
|
|
|
+ const [searchKey, setSearchKey] = useState('');
|
|
|
+ const [approvalStatusDictList, setApprovalStatusDictList] = useState<any[]>([]);
|
|
|
+
|
|
|
+ // 节点详情弹框相关状态
|
|
|
+ const [detailVisible, setDetailVisible] = useState(false);
|
|
|
+ const [detailLoading, setDetailLoading] = useState(false);
|
|
|
+ const [detailData, setDetailData] = useState<MyTaskNodeDetailVO | null>(null);
|
|
|
+ const [formData, setFormData] = useState<FormCreateData>({
|
|
|
+ rule: [],
|
|
|
+ option: {}
|
|
|
+ });
|
|
|
+ const [formLoading, setFormLoading] = useState(false); // 表单加载状态
|
|
|
+ const [originalFields, setOriginalFields] = useState<string[]>([]); // 保存原始的 fields 数组(JSON 字符串数组)
|
|
|
+ const [originalConf, setOriginalConf] = useState<string>(''); // 保存原始的 conf(JSON 字符串)
|
|
|
+ const [detailForm] = AntdForm.useForm();
|
|
|
+ const [approvalComment, setApprovalComment] = useState(''); // 审核意见
|
|
|
+ const [approvalLoading, setApprovalLoading] = useState(false); // 审核操作loading状态
|
|
|
+ const [submitLoading, setSubmitLoading] = useState(false); // 提交操作loading状态
|
|
|
+
|
|
|
+
|
|
|
+ // 组件挂载时打印调试信息
|
|
|
+ useEffect(() => {
|
|
|
+ console.log('TaskManagement 组件已加载');
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 监听弹框状态变化
|
|
|
+ useEffect(() => {
|
|
|
+ console.log('TaskManagement: detailVisible 状态变化', detailVisible, 'detailData:', detailData);
|
|
|
+ }, [detailVisible, detailData]);
|
|
|
+
|
|
|
+ // 获取审批状态字典
|
|
|
+ const getApprovalStatusDictList = async () => {
|
|
|
+ try {
|
|
|
+ const { dictDataApi } = await import('../api/DictData');
|
|
|
+ const response = await dictDataApi.getDictDataPage({
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: -1,
|
|
|
+ dictType: 'approval_status',
|
|
|
+ });
|
|
|
+ const data = (response as any)?.data || response;
|
|
|
+ const dictList = data?.list || [];
|
|
|
+ setApprovalStatusDictList(dictList);
|
|
|
+ console.log('TaskManagement: 获取审批状态字典成功', dictList);
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('获取审批状态字典失败:', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ getApprovalStatusDictList();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ /** 查询列表 */
|
|
|
+ const getList = async (params?: MyTaskPageParam) => {
|
|
|
+ const searchParams = params || queryParams;
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ console.log('TaskManagement: 开始获取任务管理列表', searchParams);
|
|
|
+ const response = await taskManagementApi.getAdminWorkPage(searchParams);
|
|
|
+ console.log('TaskManagement: 接口响应', response);
|
|
|
+ const data = (response as any)?.data || response;
|
|
|
+ const pageData = (data as PageResponse<MyTaskVO>);
|
|
|
+ console.log('TaskManagement: 解析后的数据', pageData);
|
|
|
+ setList(pageData.list || []);
|
|
|
+ setTotal(pageData.total || 0);
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('TaskManagement: 获取任务管理列表失败', error);
|
|
|
+ toast.error(error.message || '获取任务管理列表失败');
|
|
|
+ setList([]);
|
|
|
+ setTotal(0);
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ console.log('TaskManagement: useEffect 触发,queryParams:', queryParams);
|
|
|
+ getList();
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
+ }, [queryParams.pageNo, queryParams.pageSize, queryParams.key]);
|
|
|
+
|
|
|
+ // 搜索
|
|
|
+ const handleSearch = () => {
|
|
|
+ setQueryParams({
|
|
|
+ ...queryParams,
|
|
|
+ pageNo: 1,
|
|
|
+ key: searchKey.trim() || undefined,
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // 重置
|
|
|
+ const handleReset = () => {
|
|
|
+ setSearchKey('');
|
|
|
+ setQueryParams({
|
|
|
+ ...queryParams,
|
|
|
+ pageNo: 1,
|
|
|
+ key: undefined,
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // 默认表单配置
|
|
|
+ const defaultFormConfig = {
|
|
|
+ name: '',
|
|
|
+ labelPosition: 'right',
|
|
|
+ formSize: 'middle',
|
|
|
+ labelSuffix: '',
|
|
|
+ labelWidth: 100,
|
|
|
+ hideRequiredMark: false,
|
|
|
+ showValidationError: true,
|
|
|
+ inlineValidation: false,
|
|
|
+ showSubmitButton: false,
|
|
|
+ showResetButton: false,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 渲染字段预览(支持嵌套结构)
|
|
|
+ const renderFieldPreview = (field: any, parentSpanStyle?: React.CSSProperties): React.ReactNode => {
|
|
|
+ const formConfig = formData.option?.formConfig || defaultFormConfig;
|
|
|
+ const layoutColumns = formConfig.layoutColumns || 1;
|
|
|
+ const spanStyle = parentSpanStyle || (layoutColumns > 1 ? { gridColumn: `span ${Math.min(layoutColumns, field.span || 1)}` } : undefined);
|
|
|
+
|
|
|
+ // 处理容器类型(card 和 grid)
|
|
|
+ if (field.type === 'card') {
|
|
|
+ const children = field.children || [];
|
|
|
+ // 优先使用 label(字段名称),如果没有则使用 cardTitle,最后使用默认值
|
|
|
+ const cardTitle = field.label || field.cardTitle || '卡片容器';
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={spanStyle} className="mb-4">
|
|
|
+ <Card title={cardTitle} className="w-full">
|
|
|
+ <div className="space-y-4">
|
|
|
+ {children.map((child: any) => renderFieldPreview(child))}
|
|
|
+ </div>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (field.type === 'grid') {
|
|
|
+ const gridColumns = field.gridColumns || 2;
|
|
|
+ const children = field.children || [];
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={spanStyle} className="mb-4">
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ display: 'grid',
|
|
|
+ gridTemplateColumns: `repeat(${gridColumns}, minmax(0, 1fr))`,
|
|
|
+ gap: '16px',
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {children.map((child: any) => {
|
|
|
+ const childSpanStyle = gridColumns > 1
|
|
|
+ ? { gridColumn: `span ${Math.min(gridColumns, child.span || 1)}` }
|
|
|
+ : undefined;
|
|
|
+ return (
|
|
|
+ <div key={child.id} style={childSpanStyle}>
|
|
|
+ {renderFieldPreview(child)}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理 alert 类型
|
|
|
+ if (field.type === 'alert') {
|
|
|
+ const themeClass =
|
|
|
+ field.alertTheme === 'dark'
|
|
|
+ ? 'bg-gray-800 text-white border-gray-700'
|
|
|
+ : '';
|
|
|
+ return (
|
|
|
+ <div key={field.id} className="mb-4" style={spanStyle}>
|
|
|
+ <Alert
|
|
|
+ message={field.alertTitle || field.label}
|
|
|
+ description={field.alertDescription}
|
|
|
+ type={field.alertType || 'info'}
|
|
|
+ showIcon={field.alertShowIcon !== false}
|
|
|
+ closable={field.alertClosable}
|
|
|
+ closeText={field.alertCloseText}
|
|
|
+ className={`${field.alertCentered ? 'text-center' : ''} ${themeClass}`}
|
|
|
+ banner
|
|
|
+ style={field.style}
|
|
|
+ />
|
|
|
+ {field.hint && <div className="text-xs text-gray-400 mt-1">{field.hint}</div>}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理普通字段
|
|
|
+ switch (field.type) {
|
|
|
+ case 'textarea':
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <Input.TextArea
|
|
|
+ placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
|
|
|
+ rows={4}
|
|
|
+ allowClear={field.showClear}
|
|
|
+ maxLength={field.maxLength}
|
|
|
+ readOnly={field.readOnly}
|
|
|
+ disabled={field.disabled}
|
|
|
+ size={field.size || 'middle'}
|
|
|
+ />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ case 'password':
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <Input.Password
|
|
|
+ placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
|
|
|
+ allowClear={field.showClear}
|
|
|
+ maxLength={field.maxLength}
|
|
|
+ readOnly={field.readOnly}
|
|
|
+ disabled={field.disabled}
|
|
|
+ size={field.size || 'middle'}
|
|
|
+ />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ case 'number':
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <InputNumber
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
|
|
|
+ max={field.maxLength}
|
|
|
+ readOnly={field.readOnly}
|
|
|
+ disabled={field.disabled}
|
|
|
+ size={field.size || 'middle'}
|
|
|
+ />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ case 'select':
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <Select
|
|
|
+ placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择'}
|
|
|
+ allowClear={field.showClear}
|
|
|
+ disabled={field.disabled}
|
|
|
+ size={field.size || 'middle'}
|
|
|
+ >
|
|
|
+ {(field.options || []).map((opt: any, idx: number) => (
|
|
|
+ <Select.Option key={idx} value={opt.value}>{opt.label}</Select.Option>
|
|
|
+ ))}
|
|
|
+ </Select>
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ case 'date':
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ getValueFromEvent={(value) => {
|
|
|
+ // 选择日期时,将 dayjs 对象转换为毫秒级时间戳
|
|
|
+ if (!value) return undefined;
|
|
|
+ if (value && typeof value === 'object' && 'valueOf' in value && typeof value.valueOf === 'function' && 'isValid' in value && typeof value.isValid === 'function') {
|
|
|
+ // 确保是有效的 dayjs 对象
|
|
|
+ if (value.isValid()) {
|
|
|
+ return value.valueOf(); // dayjs 的 valueOf() 返回毫秒级时间戳
|
|
|
+ }
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ return value;
|
|
|
+ }}
|
|
|
+ normalize={(value) => {
|
|
|
+ // 回显时,将毫秒级时间戳转换为 dayjs 对象
|
|
|
+ if (!value) return undefined;
|
|
|
+ if (typeof value === 'number') {
|
|
|
+ const dayjsValue = dayjs(value);
|
|
|
+ return dayjsValue.isValid() ? dayjsValue : undefined;
|
|
|
+ }
|
|
|
+ if (typeof value === 'string') {
|
|
|
+ const numValue = Number(value);
|
|
|
+ if (!isNaN(numValue) && numValue > 0) {
|
|
|
+ const dayjsValue = dayjs(numValue);
|
|
|
+ return dayjsValue.isValid() ? dayjsValue : undefined;
|
|
|
+ }
|
|
|
+ const dayjsValue = dayjs(value);
|
|
|
+ return dayjsValue.isValid() ? dayjsValue : undefined;
|
|
|
+ }
|
|
|
+ if (value && typeof value === 'object' && 'isValid' in value && typeof value.isValid === 'function') {
|
|
|
+ return value.isValid() ? value : undefined;
|
|
|
+ }
|
|
|
+ return undefined;
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <DatePicker
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期'}
|
|
|
+ allowClear={field.showClear}
|
|
|
+ disabled={field.disabled}
|
|
|
+ size={field.size || 'middle'}
|
|
|
+ />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ case 'daterange':
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期范围' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ getValueFromEvent={(value) => {
|
|
|
+ // 选择日期范围时,将 dayjs 对象数组转换为毫秒级时间戳数组
|
|
|
+ if (!value || !Array.isArray(value) || value.length !== 2) return undefined;
|
|
|
+ const [start, end] = value;
|
|
|
+ if (start && typeof start === 'object' && 'valueOf' in start && typeof start.valueOf === 'function' && 'isValid' in start && typeof start.isValid === 'function' &&
|
|
|
+ end && typeof end === 'object' && 'valueOf' in end && typeof end.valueOf === 'function' && 'isValid' in end && typeof end.isValid === 'function') {
|
|
|
+ // 确保都是有效的 dayjs 对象
|
|
|
+ if (start.isValid() && end.isValid()) {
|
|
|
+ return [start.valueOf(), end.valueOf()]; // 返回毫秒级时间戳数组
|
|
|
+ }
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ return value;
|
|
|
+ }}
|
|
|
+ normalize={(value) => {
|
|
|
+ // 回显时,将毫秒级时间戳数组转换为 dayjs 对象数组
|
|
|
+ if (!value) return undefined;
|
|
|
+ if (Array.isArray(value) && value.length === 2) {
|
|
|
+ const [start, end] = value;
|
|
|
+ let startDayjs: any = null;
|
|
|
+ let endDayjs: any = null;
|
|
|
+
|
|
|
+ if (typeof start === 'number') {
|
|
|
+ startDayjs = dayjs(start);
|
|
|
+ startDayjs = startDayjs.isValid() ? startDayjs : null;
|
|
|
+ } else if (typeof start === 'string') {
|
|
|
+ const numValue = Number(start);
|
|
|
+ if (!isNaN(numValue) && numValue > 0) {
|
|
|
+ startDayjs = dayjs(numValue);
|
|
|
+ startDayjs = startDayjs.isValid() ? startDayjs : null;
|
|
|
+ } else {
|
|
|
+ startDayjs = dayjs(start);
|
|
|
+ startDayjs = startDayjs.isValid() ? startDayjs : null;
|
|
|
+ }
|
|
|
+ } else if (start && typeof start === 'object' && 'isValid' in start && typeof start.isValid === 'function') {
|
|
|
+ startDayjs = start.isValid() ? start : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof end === 'number') {
|
|
|
+ endDayjs = dayjs(end);
|
|
|
+ endDayjs = endDayjs.isValid() ? endDayjs : null;
|
|
|
+ } else if (typeof end === 'string') {
|
|
|
+ const numValue = Number(end);
|
|
|
+ if (!isNaN(numValue) && numValue > 0) {
|
|
|
+ endDayjs = dayjs(numValue);
|
|
|
+ endDayjs = endDayjs.isValid() ? endDayjs : null;
|
|
|
+ } else {
|
|
|
+ endDayjs = dayjs(end);
|
|
|
+ endDayjs = endDayjs.isValid() ? endDayjs : null;
|
|
|
+ }
|
|
|
+ } else if (end && typeof end === 'object' && 'isValid' in end && typeof end.isValid === 'function') {
|
|
|
+ endDayjs = end.isValid() ? end : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (startDayjs && endDayjs) {
|
|
|
+ return [startDayjs, endDayjs];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return undefined;
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <DatePicker.RangePicker
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ placeholder={Array.isArray(field.placeholder) ? field.placeholder as [string, string] : ['开始日期', '结束日期']}
|
|
|
+ allowClear={field.showClear}
|
|
|
+ disabled={field.disabled}
|
|
|
+ size={field.size || 'middle'}
|
|
|
+ />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ case 'datetime':
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期时间' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ getValueFromEvent={(value) => {
|
|
|
+ // 选择日期时间时,将 dayjs 对象转换为毫秒级时间戳
|
|
|
+ if (!value) return undefined;
|
|
|
+ if (value && typeof value === 'object' && 'valueOf' in value) {
|
|
|
+ return value.valueOf(); // dayjs 的 valueOf() 返回毫秒级时间戳
|
|
|
+ }
|
|
|
+ return value;
|
|
|
+ }}
|
|
|
+ normalize={(value) => {
|
|
|
+ // 回显时,将毫秒级时间戳转换为 dayjs 对象
|
|
|
+ if (!value) return undefined;
|
|
|
+ if (typeof value === 'number') {
|
|
|
+ const dayjsValue = dayjs(value);
|
|
|
+ return dayjsValue.isValid() ? dayjsValue : undefined;
|
|
|
+ }
|
|
|
+ if (typeof value === 'string') {
|
|
|
+ const numValue = Number(value);
|
|
|
+ if (!isNaN(numValue) && numValue > 0) {
|
|
|
+ const dayjsValue = dayjs(numValue);
|
|
|
+ return dayjsValue.isValid() ? dayjsValue : undefined;
|
|
|
+ }
|
|
|
+ const dayjsValue = dayjs(value);
|
|
|
+ return dayjsValue.isValid() ? dayjsValue : undefined;
|
|
|
+ }
|
|
|
+ if (value && typeof value === 'object' && 'isValid' in value && typeof value.isValid === 'function') {
|
|
|
+ return value.isValid() ? value : undefined;
|
|
|
+ }
|
|
|
+ return undefined;
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <DatePicker
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期时间'}
|
|
|
+ allowClear={field.showClear}
|
|
|
+ showTime={{ format: 'HH:mm:ss' }}
|
|
|
+ format="YYYY-MM-DD HH:mm:ss"
|
|
|
+ disabled={field.disabled}
|
|
|
+ size={field.size || 'middle'}
|
|
|
+ />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ case 'switch':
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ valuePropName="checked"
|
|
|
+ >
|
|
|
+ <Switch disabled={field.disabled} />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ case 'radio':
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <Radio.Group disabled={field.disabled}>
|
|
|
+ {(field.options || []).map((opt: any, idx: number) => (
|
|
|
+ <Radio key={idx} value={opt.value}>{opt.label}</Radio>
|
|
|
+ ))}
|
|
|
+ </Radio.Group>
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ case 'checkbox':
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <Checkbox.Group disabled={field.disabled}>
|
|
|
+ {(field.options || []).map((opt: any, idx: number) => (
|
|
|
+ <Checkbox key={idx} value={opt.value}>{opt.label}</Checkbox>
|
|
|
+ ))}
|
|
|
+ </Checkbox.Group>
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ case 'cascader':
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <Cascader
|
|
|
+ placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择'}
|
|
|
+ options={field.cascaderOptions || []}
|
|
|
+ className="w-full"
|
|
|
+ allowClear={field.showClear}
|
|
|
+ disabled={field.disabled}
|
|
|
+ size={field.size || 'middle'}
|
|
|
+ />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ case 'upload':
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请上传' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <Upload
|
|
|
+ disabled={field.disabled}
|
|
|
+ maxCount={field.uploadType === 'single-image' ? 1 : field.maxCount || (field.uploadType === 'multiple-image' ? 9 : undefined)}
|
|
|
+ accept={field.accept || (field.uploadType === 'single-image' || field.uploadType === 'multiple-image' ? 'image/*' : undefined)}
|
|
|
+ listType={(field.uploadType === 'file' ? 'text' : 'picture-card') as 'text' | 'picture-card' | 'picture'}
|
|
|
+ multiple={field.uploadType !== 'single-image'}
|
|
|
+ >
|
|
|
+ {field.uploadType === 'file' ? (
|
|
|
+ <Button icon={<UploadOutlined />}>上传文件</Button>
|
|
|
+ ) : (
|
|
|
+ <div>
|
|
|
+ <UploadOutlined />
|
|
|
+ <div style={{ marginTop: 8 }}>上传</div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </Upload>
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ case 'time':
|
|
|
+ case 'timepicker':
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择时间' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <DatePicker
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择时间'}
|
|
|
+ allowClear={field.showClear}
|
|
|
+ picker="time"
|
|
|
+ format="HH:mm:ss"
|
|
|
+ disabled={field.disabled}
|
|
|
+ size={field.size || 'middle'}
|
|
|
+ />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ default:
|
|
|
+ // 检查 inputType 是否为时间类型
|
|
|
+ const inputType = field.inputType || field.type;
|
|
|
+ if (inputType === 'date' || inputType === 'datetime' || inputType === 'time' || inputType === 'timepicker') {
|
|
|
+ // 如果是时间类型,使用 DatePicker
|
|
|
+ if (inputType === 'datetime') {
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期时间' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <DatePicker
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期时间'}
|
|
|
+ allowClear={field.showClear}
|
|
|
+ showTime={{ format: 'HH:mm:ss' }}
|
|
|
+ format="YYYY-MM-DD HH:mm:ss"
|
|
|
+ disabled={field.disabled}
|
|
|
+ size={field.size || 'middle'}
|
|
|
+ />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ } else if (inputType === 'time' || inputType === 'timepicker') {
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择时间' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <DatePicker
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择时间'}
|
|
|
+ allowClear={field.showClear}
|
|
|
+ picker="time"
|
|
|
+ format="HH:mm:ss"
|
|
|
+ disabled={field.disabled}
|
|
|
+ size={field.size || 'middle'}
|
|
|
+ />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ // date 类型
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <DatePicker
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期'}
|
|
|
+ allowClear={field.showClear}
|
|
|
+ disabled={field.disabled}
|
|
|
+ size={field.size || 'middle'}
|
|
|
+ />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 其他类型使用 Input
|
|
|
+ return (
|
|
|
+ <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
|
|
|
+ <AntdForm.Item
|
|
|
+ label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
|
|
|
+ name={field.name || field.field}
|
|
|
+ required={field.required && !formConfig.hideRequiredMark}
|
|
|
+ rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
|
|
|
+ help={field.hint}
|
|
|
+ >
|
|
|
+ <Input
|
|
|
+ type={field.inputType || 'text'}
|
|
|
+ placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
|
|
|
+ allowClear={field.showClear}
|
|
|
+ maxLength={field.maxLength}
|
|
|
+ readOnly={field.readOnly}
|
|
|
+ disabled={field.disabled}
|
|
|
+ size={field.size || 'middle'}
|
|
|
+ />
|
|
|
+ </AntdForm.Item>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 查看节点详情(节点详情弹框)
|
|
|
+ const handleViewDetail = async (record: MyTaskVO) => {
|
|
|
+ console.log('TaskManagement: handleViewDetail 被调用', record);
|
|
|
+
|
|
|
+ if (!record.nodeId) {
|
|
|
+ console.warn('TaskManagement: 节点ID不存在', record);
|
|
|
+ message.warning('节点ID不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setDetailLoading(true);
|
|
|
+ setDetailVisible(true); // 先打开弹框显示加载状态
|
|
|
+ console.log('TaskManagement: 弹框状态已设置为 true (加载中)');
|
|
|
+
|
|
|
+ try {
|
|
|
+ console.log('TaskManagement: 开始获取节点详情', { nodeId: record.nodeId });
|
|
|
+ const response = await taskManagementApi.getAdminWorkNodeDetail(record.nodeId);
|
|
|
+ console.log('TaskManagement: 节点详情响应', response);
|
|
|
+
|
|
|
+ let data: any = response;
|
|
|
+
|
|
|
+ // 直接从第一层 data.formId 获取 formId
|
|
|
+ let extractedFormId: number | undefined = undefined;
|
|
|
+ if (data?.formId !== undefined && data?.formId !== null) {
|
|
|
+ const formIdValue = data.formId;
|
|
|
+ if (typeof formIdValue === 'string') {
|
|
|
+ const parsed = parseInt(formIdValue, 10);
|
|
|
+ if (!isNaN(parsed)) {
|
|
|
+ extractedFormId = parsed;
|
|
|
+ console.log('TaskManagement: ✅ 从 data.formId (字符串) 提取到 formId', extractedFormId);
|
|
|
+ }
|
|
|
+ } else if (typeof formIdValue === 'number') {
|
|
|
+ extractedFormId = formIdValue;
|
|
|
+ console.log('TaskManagement: ✅ 从 data.formId (数字) 提取到 formId', extractedFormId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('TaskManagement: formId 提取结果', {
|
|
|
+ extractedFormId,
|
|
|
+ '原始 data.formId': data?.formId,
|
|
|
+ 'data.formId 类型': typeof data?.formId
|
|
|
+ });
|
|
|
+
|
|
|
+ if (extractedFormId === undefined) {
|
|
|
+ console.warn('TaskManagement: ⚠️ 未能提取到 formId', {
|
|
|
+ 'data.formId': data?.formId,
|
|
|
+ 'data': data
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 合并列表数据中的作业信息(作业名称、编号、负责人、时间等)
|
|
|
+ // 直接使用第一层 data,不解析 data.data
|
|
|
+ const detailDataWithWorkInfo: MyTaskNodeDetailVO = {
|
|
|
+ ...(data || {}),
|
|
|
+ workName: record.name || data?.workName || data?.name,
|
|
|
+ orderNo: record.orderNo || data?.orderNo,
|
|
|
+ workerUserName: record.workerUserName || data?.workerUserName,
|
|
|
+ initiatorName: record.initiatorName || data?.initiatorName || data?.initiator || record.initiator || '',
|
|
|
+ workTime: record.workTime || data?.workTime,
|
|
|
+ // 确保 type 字段存在,使用第一层 data.type
|
|
|
+ type: data?.type || data?.nodeType || '',
|
|
|
+ nodeType: data?.nodeType || data?.type || '', // 同时设置 nodeType 作为兼容
|
|
|
+ // 使用提取到的 formId
|
|
|
+ formId: extractedFormId,
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('TaskManagement: 合并后的详情数据 - type 和 formId 字段', {
|
|
|
+ 'data.type': data?.type,
|
|
|
+ 'data.nodeType': data?.nodeType,
|
|
|
+ 'data.formId': data?.formId,
|
|
|
+ 'detailDataWithWorkInfo.type': detailDataWithWorkInfo.type,
|
|
|
+ 'detailDataWithWorkInfo.nodeType': detailDataWithWorkInfo.nodeType,
|
|
|
+ 'detailDataWithWorkInfo.formId': detailDataWithWorkInfo.formId,
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log('TaskManagement: 合并后的详情数据', detailDataWithWorkInfo);
|
|
|
+ console.log('TaskManagement: 节点类型', detailDataWithWorkInfo.type);
|
|
|
+
|
|
|
+ // 先设置数据,确保弹框有内容显示
|
|
|
+ setDetailData(detailDataWithWorkInfo);
|
|
|
+ console.log('TaskManagement: detailData 已设置', detailDataWithWorkInfo);
|
|
|
+
|
|
|
+ // 确保弹框是打开状态
|
|
|
+ setDetailVisible(true);
|
|
|
+ setDetailLoading(false);
|
|
|
+ console.log('TaskManagement: 弹框状态设置为 true,loading 设置为 false');
|
|
|
+
|
|
|
+ // 根据节点类型决定是否需要获取表单
|
|
|
+ // isolation、releaseIsolation 和 returnLock 不需要表单,其他类型(review 和其他)需要根据 formId 获取表单
|
|
|
+ const nodeType = detailDataWithWorkInfo.type || detailDataWithWorkInfo.nodeType || '';
|
|
|
+ const isIsolation = nodeType === 'isolation' || nodeType === '隔离' || nodeType === '隔离/方案';
|
|
|
+ const isReleaseIsolation = nodeType === 'releaseIsolation' || nodeType === '解除隔离';
|
|
|
+ const isReturnLock = nodeType === 'returnLock';
|
|
|
+
|
|
|
+ // 检查任务状态是否为"已通过"
|
|
|
+ const isApproved = detailDataWithWorkInfo.approvalStatus === 'approved';
|
|
|
+
|
|
|
+ // 获取 formId(可能为 0,所以需要明确检查 undefined 和 null)
|
|
|
+ const formId = detailDataWithWorkInfo.formId;
|
|
|
+ // formId 可能是数字(包括 0)或字符串,只要不是 undefined、null 或空字符串,就认为有 formId
|
|
|
+ const hasFormId = formId !== undefined && formId !== null && (
|
|
|
+ typeof formId === 'number' ||
|
|
|
+ (typeof formId === 'string' && formId !== '' && formId.trim() !== '')
|
|
|
+ );
|
|
|
+
|
|
|
+ // 检查是否有 formData(已提交的表单数据)
|
|
|
+ const hasFormData = detailDataWithWorkInfo.formData && (
|
|
|
+ (typeof detailDataWithWorkInfo.formData === 'string' && detailDataWithWorkInfo.formData.trim() !== '') ||
|
|
|
+ (typeof detailDataWithWorkInfo.formData === 'object' && Object.keys(detailDataWithWorkInfo.formData).length > 0)
|
|
|
+ );
|
|
|
+
|
|
|
+ console.log('TaskManagement: 表单获取判断', {
|
|
|
+ nodeType,
|
|
|
+ isIsolation,
|
|
|
+ isReleaseIsolation,
|
|
|
+ isReturnLock,
|
|
|
+ isApproved,
|
|
|
+ hasFormData,
|
|
|
+ formId,
|
|
|
+ formIdType: typeof formId,
|
|
|
+ hasFormId,
|
|
|
+ '从formData获取': isApproved && hasFormData,
|
|
|
+ '从formId获取': !isIsolation && !isReleaseIsolation && !isReturnLock && hasFormId && !(isApproved && hasFormData),
|
|
|
+ 'detailDataWithWorkInfo': detailDataWithWorkInfo,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 如果任务状态为"已通过"且有 formData,则从 formData 中解析表单结构
|
|
|
+ if (isApproved && hasFormData) {
|
|
|
+ console.log('TaskManagement: ✅ 任务已通过,从 formData 中解析表单结构');
|
|
|
+ setFormLoading(true);
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 解析 formData(可能是 JSON 字符串或对象)
|
|
|
+ let parsedFormData: any;
|
|
|
+ if (typeof detailDataWithWorkInfo.formData === 'string') {
|
|
|
+ parsedFormData = JSON.parse(detailDataWithWorkInfo.formData);
|
|
|
+ } else {
|
|
|
+ parsedFormData = detailDataWithWorkInfo.formData;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('TaskManagement: 解析后的 formData', parsedFormData);
|
|
|
+
|
|
|
+ // 从 formData 中提取 conf 和 fields
|
|
|
+ const conf = parsedFormData.conf;
|
|
|
+ const fields = parsedFormData.fields;
|
|
|
+
|
|
|
+ if (conf && fields) {
|
|
|
+ // 保存原始的 conf 和 fields(JSON 字符串格式)
|
|
|
+ const confString = typeof conf === 'string' ? conf : JSON.stringify(conf);
|
|
|
+ const fieldsArray = Array.isArray(fields) ? fields : [];
|
|
|
+ const fieldsStringArray = fieldsArray.map((field: any) => {
|
|
|
+ return typeof field === 'string' ? field : JSON.stringify(field);
|
|
|
+ });
|
|
|
+
|
|
|
+ setOriginalConf(confString);
|
|
|
+ setOriginalFields(fieldsStringArray);
|
|
|
+ console.log('TaskManagement: 从 formData 保存原始表单数据', {
|
|
|
+ conf: confString,
|
|
|
+ fieldsCount: fieldsStringArray.length,
|
|
|
+ fields: fieldsStringArray
|
|
|
+ });
|
|
|
+
|
|
|
+ // 解析 fields 中的每个字段,提取 value 值用于回显
|
|
|
+ const formValues: any = {};
|
|
|
+ const fieldTypeMap: { [key: string]: string } = {}; // 存储字段名到类型的映射
|
|
|
+
|
|
|
+ // 先解析所有字段,建立字段类型映射
|
|
|
+ fieldsStringArray.forEach((fieldString: string) => {
|
|
|
+ try {
|
|
|
+ const fieldObj = typeof fieldString === 'string' ? JSON.parse(fieldString) : fieldString;
|
|
|
+ const fieldName = fieldObj.name || fieldObj.field;
|
|
|
+ if (fieldName) {
|
|
|
+ fieldTypeMap[fieldName] = fieldObj.type;
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('TaskManagement: 解析字段 JSON 失败', e, fieldString);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 提取值并转换日期类型
|
|
|
+ fieldsStringArray.forEach((fieldString: string) => {
|
|
|
+ try {
|
|
|
+ const fieldObj = typeof fieldString === 'string' ? JSON.parse(fieldString) : fieldString;
|
|
|
+ const fieldName = fieldObj.name || fieldObj.field;
|
|
|
+ if (fieldName && fieldObj.value !== undefined && fieldObj.value !== null) {
|
|
|
+ const fieldType = fieldTypeMap[fieldName];
|
|
|
+ let value = fieldObj.value;
|
|
|
+
|
|
|
+ // 如果是日期类型,转换为 dayjs 对象
|
|
|
+ if (fieldType === 'date' || fieldType === 'datetime') {
|
|
|
+ if (typeof value === 'string' && value.trim() !== '') {
|
|
|
+ const dayjsValue = dayjs(value);
|
|
|
+ formValues[fieldName] = dayjsValue.isValid() ? dayjsValue : undefined;
|
|
|
+ } else {
|
|
|
+ formValues[fieldName] = undefined;
|
|
|
+ }
|
|
|
+ } else if (fieldType === 'daterange') {
|
|
|
+ // 日期范围类型,转换为 dayjs 数组
|
|
|
+ if (Array.isArray(value) && value.length === 2) {
|
|
|
+ const [start, end] = value;
|
|
|
+ const startDayjs = typeof start === 'string' && start ? dayjs(start) : null;
|
|
|
+ const endDayjs = typeof end === 'string' && end ? dayjs(end) : null;
|
|
|
+ if (startDayjs && startDayjs.isValid() && endDayjs && endDayjs.isValid()) {
|
|
|
+ formValues[fieldName] = [startDayjs, endDayjs];
|
|
|
+ } else {
|
|
|
+ formValues[fieldName] = undefined;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ formValues[fieldName] = undefined;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 其他类型直接使用原值
|
|
|
+ formValues[fieldName] = value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('TaskManagement: 解析字段 JSON 失败', e, fieldString);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log('TaskManagement: 从 formData 提取的表单值', formValues);
|
|
|
+
|
|
|
+ // 设置表单配置和字段
|
|
|
+ setConfAndFields2(setFormData, conf, fields);
|
|
|
+ console.log('TaskManagement: 表单配置已设置(从 formData)');
|
|
|
+
|
|
|
+ // 转换日期值为 dayjs 对象
|
|
|
+ const convertedFormValues = convertDateValues(formValues, fieldsStringArray);
|
|
|
+
|
|
|
+ // 回填表单值
|
|
|
+ setTimeout(() => {
|
|
|
+ detailForm.setFieldsValue(convertedFormValues);
|
|
|
+ console.log('TaskManagement: 表单数据已回填(从 formData)', convertedFormValues);
|
|
|
+ }, 100);
|
|
|
+ } else {
|
|
|
+ console.warn('TaskManagement: formData 中缺少 conf 或 fields', { conf: !!conf, fields: !!fields });
|
|
|
+ message.warning('表单数据不完整');
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('TaskManagement: 解析 formData 失败', e);
|
|
|
+ message.error('解析表单数据失败: ' + (e instanceof Error ? e.message : String(e)));
|
|
|
+ } finally {
|
|
|
+ setFormLoading(false);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 如果不是隔离类型,且有表单ID,且不是从 formData 获取,则从接口获取表单配置
|
|
|
+ else if (!isIsolation && !isReleaseIsolation && !isReturnLock && hasFormId) {
|
|
|
+ console.log('TaskManagement: ✅ 满足条件,开始获取表单', { formId, nodeType });
|
|
|
+ // 重置表单数据
|
|
|
+ setFormData({ rule: [], option: {} });
|
|
|
+ setOriginalFields([]);
|
|
|
+ setOriginalConf('');
|
|
|
+ setFormLoading(true);
|
|
|
+
|
|
|
+ // 立即调用接口,不使用 setTimeout,确保接口被调用
|
|
|
+ (async () => {
|
|
|
+ try {
|
|
|
+ // 确保 formId 是数字类型
|
|
|
+ const numericFormId = typeof formId === 'string' ? parseInt(formId, 10) : formId;
|
|
|
+ if (isNaN(numericFormId)) {
|
|
|
+ console.error('TaskManagement: formId 不是有效数字', formId);
|
|
|
+ message.error('表单ID无效');
|
|
|
+ setFormLoading(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('TaskManagement: 🚀 开始调用表单接口', { formId: numericFormId, nodeType, url: `/bpm/form/get?id=${numericFormId}` });
|
|
|
+ const FormApi = await import('../api/bpm/form');
|
|
|
+ const formDetailResponse = await FormApi.getForm(numericFormId);
|
|
|
+ console.log('TaskManagement: ✅ 表单接口调用成功,原始响应', formDetailResponse);
|
|
|
+
|
|
|
+ // 处理响应数据(可能包含 code 和 data 包装)
|
|
|
+ let formDetail: any = formDetailResponse;
|
|
|
+ if (formDetailResponse && typeof formDetailResponse === 'object' && 'data' in formDetailResponse) {
|
|
|
+ // 如果响应有 data 字段,使用 data
|
|
|
+ formDetail = (formDetailResponse as any).data || formDetailResponse;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('TaskManagement: 处理后的表单详情', formDetail);
|
|
|
+
|
|
|
+ // 获取 conf 和 fields
|
|
|
+ const conf = formDetail?.conf || formDetail?.formConfig;
|
|
|
+ const fields = formDetail?.fields || formDetail?.formFields;
|
|
|
+
|
|
|
+ console.log('TaskManagement: 表单配置和字段', {
|
|
|
+ hasConf: !!conf,
|
|
|
+ confType: typeof conf,
|
|
|
+ hasFields: !!fields,
|
|
|
+ fieldsType: Array.isArray(fields) ? 'array' : typeof fields,
|
|
|
+ fieldsLength: Array.isArray(fields) ? fields.length : 0
|
|
|
+ });
|
|
|
+
|
|
|
+ // 解析表单配置和字段
|
|
|
+ if (conf && fields) {
|
|
|
+ // 保存原始的 conf 和 fields(JSON 字符串格式)
|
|
|
+ const confString = typeof conf === 'string' ? conf : JSON.stringify(conf);
|
|
|
+ const fieldsArray = Array.isArray(fields) ? fields : [];
|
|
|
+ const fieldsStringArray = fieldsArray.map((field: any) => {
|
|
|
+ return typeof field === 'string' ? field : JSON.stringify(field);
|
|
|
+ });
|
|
|
+
|
|
|
+ setOriginalConf(confString);
|
|
|
+ setOriginalFields(fieldsStringArray);
|
|
|
+ console.log('TaskManagement: 保存原始表单数据', {
|
|
|
+ conf: confString,
|
|
|
+ fieldsCount: fieldsStringArray.length,
|
|
|
+ fields: fieldsStringArray
|
|
|
+ });
|
|
|
+
|
|
|
+ // setConfAndFields2 会自动处理 JSON 字符串解析
|
|
|
+ setConfAndFields2(setFormData, conf, fields);
|
|
|
+ console.log('TaskManagement: 表单配置已设置', {
|
|
|
+ conf: typeof conf === 'string' ? 'JSON字符串' : '对象',
|
|
|
+ fieldsCount: Array.isArray(fields) ? fields.length : 0
|
|
|
+ });
|
|
|
+
|
|
|
+ // 如果有表单数据值,回填到表单(在表单配置设置后)
|
|
|
+ if (detailDataWithWorkInfo.formData) {
|
|
|
+ try {
|
|
|
+ let formValues = typeof detailDataWithWorkInfo.formData === 'string'
|
|
|
+ ? JSON.parse(detailDataWithWorkInfo.formData)
|
|
|
+ : detailDataWithWorkInfo.formData;
|
|
|
+
|
|
|
+ // 转换日期值为 dayjs 对象
|
|
|
+ const fieldsArray = Array.isArray(fields) ? fields : [];
|
|
|
+ formValues = convertDateValues(formValues, fieldsArray);
|
|
|
+
|
|
|
+ // 清理无效的日期值,确保所有日期字段都是有效的 dayjs 对象或 undefined
|
|
|
+ const cleanedFormValues: any = {};
|
|
|
+ Object.keys(formValues).forEach(key => {
|
|
|
+ const value = formValues[key];
|
|
|
+ const field = fieldsArray.find((f: any) => {
|
|
|
+ const fieldObj = typeof f === 'string' ? JSON.parse(f) : f;
|
|
|
+ return (fieldObj.name || fieldObj.field) === key;
|
|
|
+ });
|
|
|
+
|
|
|
+ if (field) {
|
|
|
+ const fieldObj = typeof field === 'string' ? JSON.parse(field) : field;
|
|
|
+ const fieldType = fieldObj.type;
|
|
|
+
|
|
|
+ if (fieldType === 'date' || fieldType === 'datetime' || fieldType === 'timepicker') {
|
|
|
+ // 日期、日期时间或时间选择器类型:确保是有效的 dayjs 对象或 undefined
|
|
|
+ if (isValidDayjs(value)) {
|
|
|
+ // 已经是有效的 dayjs 对象
|
|
|
+ cleanedFormValues[key] = value;
|
|
|
+ } else if (value !== null && value !== undefined) {
|
|
|
+ // 尝试转换
|
|
|
+ const dayjsValue = safeToDayjs(value);
|
|
|
+ cleanedFormValues[key] = dayjsValue || undefined;
|
|
|
+ } else {
|
|
|
+ cleanedFormValues[key] = undefined;
|
|
|
+ }
|
|
|
+ } else if (fieldType === 'daterange') {
|
|
|
+ // 日期范围类型:确保数组中的每个元素都是有效的 dayjs 对象
|
|
|
+ if (Array.isArray(value) && value.length === 2) {
|
|
|
+ const [start, end] = value;
|
|
|
+ // 验证开始时间
|
|
|
+ let startDayjs: dayjs.Dayjs | null = null;
|
|
|
+ if (isValidDayjs(start)) {
|
|
|
+ startDayjs = start;
|
|
|
+ } else {
|
|
|
+ startDayjs = safeToDayjs(start);
|
|
|
+ }
|
|
|
+ // 验证结束时间
|
|
|
+ let endDayjs: dayjs.Dayjs | null = null;
|
|
|
+ if (isValidDayjs(end)) {
|
|
|
+ endDayjs = end;
|
|
|
+ } else {
|
|
|
+ endDayjs = safeToDayjs(end);
|
|
|
+ }
|
|
|
+ cleanedFormValues[key] = (startDayjs && endDayjs) ? [startDayjs, endDayjs] : undefined;
|
|
|
+ } else {
|
|
|
+ cleanedFormValues[key] = undefined;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 其他类型直接使用
|
|
|
+ cleanedFormValues[key] = value;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 没有找到字段定义,直接使用原值
|
|
|
+ cleanedFormValues[key] = value;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 延迟一下,确保表单字段已经渲染
|
|
|
+ setTimeout(() => {
|
|
|
+ try {
|
|
|
+ detailForm.setFieldsValue(cleanedFormValues);
|
|
|
+ console.log('TaskManagement: 表单数据已回填', cleanedFormValues);
|
|
|
+ } catch (e) {
|
|
|
+ console.error('TaskManagement: 设置表单值失败', e);
|
|
|
+ // 如果设置失败,尝试只设置非日期字段
|
|
|
+ const safeValues: any = {};
|
|
|
+ Object.keys(cleanedFormValues).forEach(key => {
|
|
|
+ const value = cleanedFormValues[key];
|
|
|
+ const field = fieldsArray.find((f: any) => {
|
|
|
+ const fieldObj = typeof f === 'string' ? JSON.parse(f) : f;
|
|
|
+ return (fieldObj.name || fieldObj.field) === key;
|
|
|
+ });
|
|
|
+ if (field) {
|
|
|
+ const fieldObj = typeof field === 'string' ? JSON.parse(field) : field;
|
|
|
+ const fieldType = fieldObj.type;
|
|
|
+ if (fieldType !== 'date' && fieldType !== 'datetime' && fieldType !== 'daterange' && fieldType !== 'timepicker') {
|
|
|
+ safeValues[key] = value;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ safeValues[key] = value;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ detailForm.setFieldsValue(safeValues);
|
|
|
+ }
|
|
|
+ }, 100);
|
|
|
+ } catch (e) {
|
|
|
+ console.error('TaskManagement: 解析表单数据失败', e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ console.warn('TaskManagement: 表单详情缺少配置或字段', { conf: !!conf, fields: !!fields, formDetail });
|
|
|
+ message.warning('表单配置不完整');
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('TaskManagement: 获取表单详情失败', e);
|
|
|
+ message.error('获取表单详情失败: ' + (e instanceof Error ? e.message : String(e)));
|
|
|
+ // 即使获取表单失败,也继续显示弹框
|
|
|
+ } finally {
|
|
|
+ setFormLoading(false);
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ } else {
|
|
|
+ setFormLoading(false);
|
|
|
+ if (isIsolation || isReleaseIsolation || isReturnLock) {
|
|
|
+ console.log('TaskManagement: ⚠️ 隔离类型节点,跳过表单配置获取', { nodeType, isIsolation, isReleaseIsolation, isReturnLock });
|
|
|
+ } else if (!hasFormId) {
|
|
|
+ console.warn('TaskManagement: ⚠️ 没有表单ID,跳过表单配置获取', {
|
|
|
+ formId,
|
|
|
+ formIdType: typeof formId,
|
|
|
+ hasFormId,
|
|
|
+ nodeType,
|
|
|
+ 'detailDataWithWorkInfo': detailDataWithWorkInfo
|
|
|
+ });
|
|
|
+ // 不显示提示,静默处理
|
|
|
+ } else {
|
|
|
+ console.warn('TaskManagement: ⚠️ 未知原因未获取表单', {
|
|
|
+ nodeType,
|
|
|
+ isIsolation,
|
|
|
+ isReleaseIsolation,
|
|
|
+ isReturnLock,
|
|
|
+ hasFormId,
|
|
|
+ formId
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 注意:表单数据回填已移到表单配置加载完成后,确保表单字段已渲染
|
|
|
+
|
|
|
+ // 回显审核意见(如果有 approvalOpinion 字段,且不是 "pending")
|
|
|
+ if (detailDataWithWorkInfo.approvalOpinion && detailDataWithWorkInfo.approvalOpinion !== 'pending') {
|
|
|
+ setApprovalComment(detailDataWithWorkInfo.approvalOpinion);
|
|
|
+ console.log('TaskManagement: 回显审核意见', detailDataWithWorkInfo.approvalOpinion);
|
|
|
+ } else {
|
|
|
+ setApprovalComment(''); // 重置审核意见(包括 pending 的情况)
|
|
|
+ }
|
|
|
+
|
|
|
+ setDetailLoading(false);
|
|
|
+ console.log('TaskManagement: 弹框已打开', { detailVisible: true, detailData: detailDataWithWorkInfo });
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('TaskManagement: 获取节点详情失败', error);
|
|
|
+ toast.error(error.message || '获取节点详情失败');
|
|
|
+ // 即使接口失败,也显示弹框(显示错误信息)
|
|
|
+ if (!detailData) {
|
|
|
+ setDetailData({
|
|
|
+ id: record.nodeId,
|
|
|
+ nodeId: record.nodeId,
|
|
|
+ workId: record.workId,
|
|
|
+ workName: record.name,
|
|
|
+ orderNo: record.orderNo,
|
|
|
+ workerUserName: record.workerUserName,
|
|
|
+ workTime: record.workTime,
|
|
|
+ type: '',
|
|
|
+ } as MyTaskNodeDetailVO);
|
|
|
+ }
|
|
|
+ setDetailVisible(true);
|
|
|
+ } finally {
|
|
|
+ setDetailLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 获取任务状态显示文本(使用 approval_status 字典)
|
|
|
+ const getTaskStatusText = (status: string | number | undefined): string => {
|
|
|
+ if (!status) return '未知';
|
|
|
+ const statusStr = String(status).toLowerCase();
|
|
|
+ const statusItem = approvalStatusDictList.find(item => String(item.value).toLowerCase() === statusStr);
|
|
|
+ if (statusItem) {
|
|
|
+ return statusItem.label || statusItem.name || '未知';
|
|
|
+ }
|
|
|
+ // 如果没有找到字典值,使用默认映射
|
|
|
+ const statusMap: Record<string, string> = {
|
|
|
+ 'pending': '待审核',
|
|
|
+ 'approved': '已通过',
|
|
|
+ 'rejected': '已驳回',
|
|
|
+ 'unaudited': '未审核',
|
|
|
+ };
|
|
|
+ return statusMap[statusStr] || '未知';
|
|
|
+ };
|
|
|
+
|
|
|
+ // 获取任务状态样式(审批状态)
|
|
|
+ const getTaskStatusClassName = (status: string | number | undefined): string => {
|
|
|
+ if (!status) return 'bg-gray-100 text-gray-600';
|
|
|
+ const statusStr = String(status).toLowerCase();
|
|
|
+ const statusMap: Record<string, string> = {
|
|
|
+ 'pending': 'bg-yellow-100 text-yellow-700',
|
|
|
+ 'approved': 'bg-green-100 text-green-700',
|
|
|
+ 'rejected': 'bg-red-100 text-red-700',
|
|
|
+ 'unaudited': 'bg-gray-100 text-gray-600',
|
|
|
+ };
|
|
|
+ return statusMap[statusStr] || 'bg-gray-100 text-gray-600';
|
|
|
+ };
|
|
|
+
|
|
|
+ // 表格列配置
|
|
|
+ const columns: ColumnsType<MyTaskVO> = [
|
|
|
+ {
|
|
|
+ title: '作业编号',
|
|
|
+ dataIndex: 'orderNo',
|
|
|
+ width: 180,
|
|
|
+ align: 'center',
|
|
|
+ render: (text: string, record: MyTaskVO) => {
|
|
|
+ if (!text) return '-';
|
|
|
+ return (
|
|
|
+ <span
|
|
|
+ className="text-blue-600 hover:text-blue-800 cursor-pointer hover:underline"
|
|
|
+ onClick={(e) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ e.preventDefault();
|
|
|
+ handleViewDetail(record);
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {text}
|
|
|
+ </span>
|
|
|
+ );
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '作业名称',
|
|
|
+ dataIndex: 'name',
|
|
|
+ width: 220,
|
|
|
+ align: 'center',
|
|
|
+ ellipsis: true,
|
|
|
+ render: (text: string, record: MyTaskVO) => {
|
|
|
+ if (!text) return '-';
|
|
|
+ return (
|
|
|
+ <span
|
|
|
+ className="text-blue-600 hover:text-blue-800 cursor-pointer hover:underline"
|
|
|
+ onClick={(e) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ e.preventDefault();
|
|
|
+ handleViewDetail(record);
|
|
|
+ }}
|
|
|
+ title={text}
|
|
|
+ >
|
|
|
+ {text}
|
|
|
+ </span>
|
|
|
+ );
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '负责人',
|
|
|
+ dataIndex: 'workerUserName',
|
|
|
+ width: 150,
|
|
|
+ align: 'center',
|
|
|
+ render: (text: string, record: MyTaskVO) => {
|
|
|
+ // 优先使用 workerUserName,如果没有则尝试从其他字段获取
|
|
|
+ return text || record.responsibleName || record.responsible || record.initiatorName || record.initiator || '-';
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '当前任务',
|
|
|
+ dataIndex: 'currentNodeName',
|
|
|
+ width: 180,
|
|
|
+ align: 'center',
|
|
|
+ render: (text: string, record: MyTaskVO) => {
|
|
|
+ // 优先使用 currentNodeName,如果没有则使用 currentNode
|
|
|
+ return text || record.currentNode || '-';
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '任务开始时间',
|
|
|
+ dataIndex: 'workTime',
|
|
|
+ width: 180,
|
|
|
+ align: 'center',
|
|
|
+ render: (text: string | number | Date | undefined, record: MyTaskVO) => {
|
|
|
+ // 优先使用 workTime,如果没有则使用其他时间字段
|
|
|
+ const time = text || record.taskStartTime || record.initiationTime || record.initiateTime;
|
|
|
+ return time ? dateFormatter(time) : '-';
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '任务状态',
|
|
|
+ dataIndex: 'approvalStatus',
|
|
|
+ width: 120,
|
|
|
+ align: 'center',
|
|
|
+ render: (status: string | number | undefined, record: MyTaskVO) => {
|
|
|
+ // 优先使用 approvalStatus,如果没有则使用其他状态字段
|
|
|
+ const taskStatus = status || record.taskStatus || record.status;
|
|
|
+ return (
|
|
|
+ <span
|
|
|
+ className={`inline-flex px-3 py-1 rounded-lg text-xs ${getTaskStatusClassName(taskStatus)}`}
|
|
|
+ >
|
|
|
+ {getTaskStatusText(taskStatus)}
|
|
|
+ </span>
|
|
|
+ );
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '操作',
|
|
|
+ width: 120,
|
|
|
+ align: 'center',
|
|
|
+ fixed: 'right',
|
|
|
+ render: (_: any, record: MyTaskVO) => (
|
|
|
+ <Space size="small">
|
|
|
+ <Button
|
|
|
+ type="link"
|
|
|
+ size="small"
|
|
|
+ icon={<Eye className="w-4 h-4" />}
|
|
|
+ onClick={(e) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ e.preventDefault();
|
|
|
+ handleViewDetail(record);
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 查看详情
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 计算分页数据
|
|
|
+ const totalPages = Math.ceil(total / queryParams.pageSize) || 1;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="space-y-6">
|
|
|
+ {/* 操作栏和表格容器 */}
|
|
|
+ <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
|
|
|
+ {/* 查询与操作栏 */}
|
|
|
+ <div className="p-4 lg:p-5 border-b border-gray-200/50">
|
|
|
+ <div className="flex flex-wrap items-center gap-3">
|
|
|
+ <Input
|
|
|
+ value={searchKey}
|
|
|
+ onChange={(e) => setSearchKey(e.target.value)}
|
|
|
+ placeholder="请输入作业编号或者作业名称进行查询"
|
|
|
+ allowClear
|
|
|
+ className="flex-1"
|
|
|
+ style={{ minWidth: 200 }}
|
|
|
+ onPressEnter={handleSearch}
|
|
|
+ />
|
|
|
+ <Space size="small">
|
|
|
+ <Button type="primary" icon={<Search className="w-4 h-4" />} onClick={handleSearch}>
|
|
|
+ 搜索
|
|
|
+ </Button>
|
|
|
+ <Button icon={<RotateCcw className="w-4 h-4" />} onClick={handleReset}>
|
|
|
+ 重置
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 表格容器 */}
|
|
|
+ <div className="overflow-hidden min-w-0">
|
|
|
+ <AntdTable
|
|
|
+ loading={loading}
|
|
|
+ columns={columns}
|
|
|
+ dataSource={list}
|
|
|
+ rowKey={(record) => record.id || Math.random()}
|
|
|
+ pagination={false}
|
|
|
+ scroll={{ x: 'max-content' }}
|
|
|
+ locale={{
|
|
|
+ emptyText: '暂无数据',
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 分页 */}
|
|
|
+ {total > 0 && (
|
|
|
+ <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <div className="text-sm text-gray-600">
|
|
|
+ 共 <span className="text-blue-600 font-medium">{total}</span> 条记录
|
|
|
+ </div>
|
|
|
+ <div className="flex gap-2">
|
|
|
+ <Button
|
|
|
+ onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo - 1 })}
|
|
|
+ disabled={queryParams.pageNo <= 1}
|
|
|
+ >
|
|
|
+ 上一页
|
|
|
+ </Button>
|
|
|
+ <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
|
|
|
+ {queryParams.pageNo} / {totalPages}
|
|
|
+ </span>
|
|
|
+ <Button
|
|
|
+ onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo + 1 })}
|
|
|
+ disabled={queryParams.pageNo >= totalPages}
|
|
|
+ >
|
|
|
+ 下一页
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 详情弹框 */}
|
|
|
+ <Modal
|
|
|
+ title={null}
|
|
|
+ open={detailVisible}
|
|
|
+ onCancel={() => {
|
|
|
+ console.log('TaskManagement: 弹框关闭');
|
|
|
+ setDetailVisible(false);
|
|
|
+ setDetailData(null);
|
|
|
+ setFormData({ rule: [], option: {} });
|
|
|
+ setFormLoading(false);
|
|
|
+ setOriginalFields([]);
|
|
|
+ setOriginalConf('');
|
|
|
+ detailForm.resetFields();
|
|
|
+ setApprovalComment('');
|
|
|
+ }}
|
|
|
+ footer={null}
|
|
|
+ width={1000}
|
|
|
+ destroyOnClose
|
|
|
+ confirmLoading={detailLoading}
|
|
|
+ maskClosable={false}
|
|
|
+ zIndex={1000}
|
|
|
+ styles={{
|
|
|
+ body: {
|
|
|
+ minHeight: '600px',
|
|
|
+ maxHeight: '700px',
|
|
|
+ height: '700px',
|
|
|
+ padding: 0,
|
|
|
+ display: 'flex',
|
|
|
+ flexDirection: 'column',
|
|
|
+ overflow: 'hidden'
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {(() => {
|
|
|
+ console.log('TaskManagement: Modal 内容渲染', { detailLoading, detailData, detailVisible });
|
|
|
+ if (detailLoading) {
|
|
|
+ return <div className="py-8 text-center">加载中...</div>;
|
|
|
+ }
|
|
|
+ if (detailData) {
|
|
|
+ return (
|
|
|
+ <div style={{
|
|
|
+ display: 'flex',
|
|
|
+ flexDirection: 'column',
|
|
|
+ height: '100%',
|
|
|
+ overflow: 'hidden'
|
|
|
+ }}>
|
|
|
+ {/* 标题区域 */}
|
|
|
+ <div className="mb-4" style={{ padding: '20px 24px 0' }}>
|
|
|
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
+ <div style={{
|
|
|
+ width: '3px',
|
|
|
+ height: '24px',
|
|
|
+ backgroundColor: '#025fff',
|
|
|
+ borderRadius: '2px',
|
|
|
+ flexShrink: 0
|
|
|
+ }}></div>
|
|
|
+ <h2 className="text-xl font-semibold mb-2" style={{ color: '#025fff', marginBottom: 0 }}>
|
|
|
+ {detailData.workName || detailData.name || '作业详情'}
|
|
|
+ </h2>
|
|
|
+ </div>
|
|
|
+ <div className="text-sm flex gap-4" style={{ color: '#898f9a', marginTop: '12px' }}>
|
|
|
+ <span>作业编号:{detailData.orderNo || '-'}</span>
|
|
|
+ <span>任务负责人:{detailData.workerUserName || '-'}</span>
|
|
|
+ <span>作业负责人:{detailData.initiatorName || '-'}</span>
|
|
|
+ <span>发起时间:{detailData.workTime ? dateFormatter(detailData.workTime) : '-'}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 内容区域 - 可滚动 */}
|
|
|
+ <div className="mt-6" style={{
|
|
|
+ padding: '0 24px',
|
|
|
+ flex: 1,
|
|
|
+ overflowY: 'auto',
|
|
|
+ minHeight: 0
|
|
|
+ }}>
|
|
|
+ {(() => {
|
|
|
+ // 检查节点状态是否为已通过(approved),如果是则禁用所有输入和按钮
|
|
|
+ const isApproved = detailData?.approvalStatus === 'approved';
|
|
|
+
|
|
|
+ // 从 detailData.type 或 detailData.nodeType 获取节点类型
|
|
|
+ const nodeType = String(detailData?.type || detailData?.nodeType || '').trim();
|
|
|
+ console.log('TaskManagement: 渲染内容区域 - 节点类型', nodeType, 'detailData:', detailData);
|
|
|
+
|
|
|
+ const isReview = nodeType === 'review';
|
|
|
+ const isIsolation = nodeType === 'isolation';
|
|
|
+ const isReleaseIsolation = nodeType === 'releaseIsolation';
|
|
|
+ const isReturnLock = nodeType === 'returnLock';
|
|
|
+
|
|
|
+ console.log('TaskManagement: 节点类型判断结果', {
|
|
|
+ isReview,
|
|
|
+ isIsolation,
|
|
|
+ isReleaseIsolation,
|
|
|
+ isReturnLock,
|
|
|
+ nodeType,
|
|
|
+ 'detailData.type': detailData.type,
|
|
|
+ 'detailData.nodeType': detailData.nodeType,
|
|
|
+ 'typeof detailData.type': typeof detailData.type,
|
|
|
+ 'String(detailData.type)': String(detailData.type)
|
|
|
+ });
|
|
|
+
|
|
|
+ // 隔离/方案节点、解除隔离节点和还锁节点
|
|
|
+ if (isIsolation || isReleaseIsolation || isReturnLock) {
|
|
|
+ console.log('TaskManagement: ✅ 进入隔离节点渲染分支', { isIsolation, isReleaseIsolation, isReturnLock, nodeType });
|
|
|
+ const isolationContent = (
|
|
|
+ <div
|
|
|
+ key="isolation-content"
|
|
|
+ style={{
|
|
|
+ display: 'flex',
|
|
|
+ justifyContent: 'center',
|
|
|
+ alignItems: 'center',
|
|
|
+ minHeight: '400px',
|
|
|
+ padding: '40px 20px'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Card
|
|
|
+ style={{
|
|
|
+ width: '100%',
|
|
|
+ maxWidth: '500px',
|
|
|
+ textAlign: 'center',
|
|
|
+ borderRadius: '12px',
|
|
|
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
|
|
+ border: '1px solid #e8e8e8'
|
|
|
+ }}
|
|
|
+ bodyStyle={{
|
|
|
+ padding: '40px 30px'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div style={{ marginBottom: '24px' }}>
|
|
|
+ <LockOutlined
|
|
|
+ style={{
|
|
|
+ fontSize: '64px',
|
|
|
+ color: '#1890ff',
|
|
|
+ display: 'block'
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ fontSize: '20px',
|
|
|
+ fontWeight: 500,
|
|
|
+ color: '#333',
|
|
|
+ lineHeight: '1.6'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 请前往锁控柜进行取锁,取钥匙操作
|
|
|
+ </div>
|
|
|
+ </Card>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ console.log('TaskManagement: 隔离节点内容已创建', isolationContent);
|
|
|
+ return isolationContent;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 审核类型节点 (review)
|
|
|
+ if (isReview) {
|
|
|
+ return (
|
|
|
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
|
|
+ <div className="space-y-6" style={{ padding: '0 24px', flex: 1, overflowY: 'auto', minHeight: 0 }}>
|
|
|
+ {/* 自定义表单 */}
|
|
|
+ {formLoading ? (
|
|
|
+ <div className="py-8 text-center text-gray-500">表单加载中...</div>
|
|
|
+ ) : formData.rule && formData.rule.length > 0 ? (
|
|
|
+ <div>
|
|
|
+ {(() => {
|
|
|
+ const formConfig = formData.option?.formConfig || defaultFormConfig;
|
|
|
+ const layoutColumns = formConfig.layoutColumns || 1;
|
|
|
+ const gridStyle = layoutColumns > 1 ? {
|
|
|
+ display: 'grid',
|
|
|
+ gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
|
|
|
+ gap: '12px',
|
|
|
+ rowGap: '16px',
|
|
|
+ } : undefined;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <AntdForm
|
|
|
+ form={detailForm}
|
|
|
+ layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
|
|
|
+ size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
|
|
|
+ requiredMark={formConfig.hideRequiredMark ? false : undefined}
|
|
|
+ labelCol={formConfig.labelWidth ? {
|
|
|
+ style: {
|
|
|
+ width: `${formConfig.labelWidth}px`,
|
|
|
+ textAlign: formConfig.labelPosition === 'left' ? 'left' : formConfig.labelPosition === 'right' ? 'right' : 'left'
|
|
|
+ }
|
|
|
+ } : undefined}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ style={gridStyle}
|
|
|
+ className={layoutColumns === 1 ? 'space-y-4' : 'form-detail-grid'}
|
|
|
+ >
|
|
|
+ <style>{`
|
|
|
+ .form-detail-grid .ant-form-item {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ }
|
|
|
+ `}</style>
|
|
|
+ {(formData.rule || []).map((field: any) => {
|
|
|
+ // 如果已通过,禁用所有字段
|
|
|
+ const fieldWithDisabled = isApproved ? { ...field, disabled: true, readOnly: true } : field;
|
|
|
+ return renderFieldPreview(fieldWithDisabled);
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </AntdForm>
|
|
|
+ );
|
|
|
+ })()}
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className="py-4 text-center text-gray-400 text-sm">暂无表单内容</div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 审核意见 - label 和 textarea 同一行 */}
|
|
|
+ <div className="flex items-start" style={{ gap: '16px', backgroundColor: '#f5f5f5', padding: '12px 12px 22px 12px', borderRadius: '12px', border: '1px solid #e0e0e0', marginBottom: '12px' }}>
|
|
|
+ <label
|
|
|
+ className="text-sm font-medium text-gray-700 whitespace-nowrap pt-2"
|
|
|
+ style={{
|
|
|
+ width: defaultFormConfig.labelWidth ? `${defaultFormConfig.labelWidth - 20}px` : '80px',
|
|
|
+ textAlign: defaultFormConfig.labelPosition === 'left' ? 'left' : defaultFormConfig.labelPosition === 'right' ? 'right' : 'left',
|
|
|
+ flexShrink: 0
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 审核意见
|
|
|
+ </label>
|
|
|
+ <div className="flex-1">
|
|
|
+ <Input.TextArea
|
|
|
+ rows={4}
|
|
|
+ value={approvalComment}
|
|
|
+ onChange={(e) => setApprovalComment(e.target.value)}
|
|
|
+ placeholder="请输入审核意见"
|
|
|
+ maxLength={500}
|
|
|
+ showCount
|
|
|
+ disabled={isApproved}
|
|
|
+ readOnly={isApproved}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ </div>
|
|
|
+ {/* 底部按钮 */}
|
|
|
+ <div
|
|
|
+ className="flex justify-end gap-3"
|
|
|
+ style={{
|
|
|
+ padding: '16px 24px',
|
|
|
+ borderTop: '1px solid #f0f0f0',
|
|
|
+ flexShrink: 0
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Button
|
|
|
+ danger
|
|
|
+ loading={approvalLoading}
|
|
|
+ disabled={isApproved}
|
|
|
+ onClick={async () => {
|
|
|
+ const nodeId = detailData.nodeId || detailData.id;
|
|
|
+ if (!nodeId) {
|
|
|
+ message.error('节点ID不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果有自定义表单,先校验表单
|
|
|
+ if (formData.rule && formData.rule.length > 0) {
|
|
|
+ try {
|
|
|
+ // 校验表单所有字段
|
|
|
+ await detailForm.validateFields();
|
|
|
+ console.log('TaskManagement: 表单校验通过');
|
|
|
+ } catch (error: any) {
|
|
|
+ // 校验失败,显示错误信息
|
|
|
+ console.error('TaskManagement: 表单校验失败', error);
|
|
|
+ if (error.errorFields && error.errorFields.length > 0) {
|
|
|
+ // 获取第一个错误字段的提示信息
|
|
|
+ const firstError = error.errorFields[0];
|
|
|
+ const errorMessage = firstError.errors?.[0] || '请完善表单内容';
|
|
|
+ message.error(errorMessage);
|
|
|
+ } else {
|
|
|
+ message.error('请完善表单内容');
|
|
|
+ }
|
|
|
+ return; // 阻止提交
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setApprovalLoading(true);
|
|
|
+ try {
|
|
|
+ const params: UpdateNodeApprovalParam = {
|
|
|
+ nodeId: nodeId,
|
|
|
+ approvalStatus: 'rejected',
|
|
|
+ approvalOpinion: approvalComment || undefined,
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('TaskManagement: 调用审核不通过接口', params);
|
|
|
+ await taskManagementApi.updateNodeApproval(params);
|
|
|
+ message.success('审核不通过操作成功');
|
|
|
+
|
|
|
+ // 关闭弹框
|
|
|
+ setDetailVisible(false);
|
|
|
+ setDetailData(null);
|
|
|
+ setFormData({ rule: [], option: {} });
|
|
|
+ setFormLoading(false);
|
|
|
+ setOriginalFields([]);
|
|
|
+ setOriginalConf('');
|
|
|
+ detailForm.resetFields();
|
|
|
+ setApprovalComment('');
|
|
|
+
|
|
|
+ // 刷新列表
|
|
|
+ getList();
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('TaskManagement: 审核不通过失败', error);
|
|
|
+ message.error(error?.message || '审核不通过操作失败');
|
|
|
+ } finally {
|
|
|
+ setApprovalLoading(false);
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 审核不通过
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ loading={approvalLoading}
|
|
|
+ disabled={isApproved}
|
|
|
+ onClick={async () => {
|
|
|
+ const nodeId = detailData.nodeId || detailData.id;
|
|
|
+ if (!nodeId) {
|
|
|
+ message.error('节点ID不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果有自定义表单,先校验表单
|
|
|
+ if (formData.rule && formData.rule.length > 0) {
|
|
|
+ try {
|
|
|
+ // 校验表单所有字段
|
|
|
+ await detailForm.validateFields();
|
|
|
+ console.log('TaskManagement: 表单校验通过');
|
|
|
+ } catch (error: any) {
|
|
|
+ // 校验失败,显示错误信息
|
|
|
+ console.error('TaskManagement: 表单校验失败', error);
|
|
|
+ if (error.errorFields && error.errorFields.length > 0) {
|
|
|
+ // 获取第一个错误字段的提示信息
|
|
|
+ const firstError = error.errorFields[0];
|
|
|
+ const errorMessage = firstError.errors?.[0] || '请完善表单内容';
|
|
|
+ message.error(errorMessage);
|
|
|
+ } else {
|
|
|
+ message.error('请完善表单内容');
|
|
|
+ }
|
|
|
+ return; // 阻止提交
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setApprovalLoading(true);
|
|
|
+ try {
|
|
|
+ const params: UpdateNodeApprovalParam = {
|
|
|
+ nodeId: nodeId,
|
|
|
+ approvalStatus: 'approved',
|
|
|
+ approvalOpinion: approvalComment || undefined,
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('TaskManagement: 调用审核通过接口', params);
|
|
|
+ await taskManagementApi.updateNodeApproval(params);
|
|
|
+ message.success('审核通过操作成功');
|
|
|
+
|
|
|
+ // 关闭弹框
|
|
|
+ setDetailVisible(false);
|
|
|
+ setDetailData(null);
|
|
|
+ setFormData({ rule: [], option: {} });
|
|
|
+ setFormLoading(false);
|
|
|
+ setOriginalFields([]);
|
|
|
+ setOriginalConf('');
|
|
|
+ detailForm.resetFields();
|
|
|
+ setApprovalComment('');
|
|
|
+
|
|
|
+ // 刷新列表
|
|
|
+ getList();
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('TaskManagement: 审核通过失败', error);
|
|
|
+ message.error(error?.message || '审核通过操作失败');
|
|
|
+ } finally {
|
|
|
+ setApprovalLoading(false);
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 审核通过
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 其他节点类型(需要根据 formId 获取表单)
|
|
|
+ return (
|
|
|
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
|
|
+ <div className="space-y-6" style={{ padding: '0 24px', flex: 1, overflowY: 'auto', minHeight: 0 }}>
|
|
|
+ {/* 自定义表单 */}
|
|
|
+ {formLoading ? (
|
|
|
+ <div className="py-8 text-center text-gray-500">表单加载中...</div>
|
|
|
+ ) : formData.rule && formData.rule.length > 0 ? (
|
|
|
+ <div>
|
|
|
+ {(() => {
|
|
|
+ const formConfig = formData.option?.formConfig || defaultFormConfig;
|
|
|
+ const layoutColumns = formConfig.layoutColumns || 1;
|
|
|
+ const gridStyle = layoutColumns > 1 ? {
|
|
|
+ display: 'grid',
|
|
|
+ gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
|
|
|
+ gap: '12px',
|
|
|
+ rowGap: '16px',
|
|
|
+ } : undefined;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <AntdForm
|
|
|
+ form={detailForm}
|
|
|
+ layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
|
|
|
+ size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
|
|
|
+ requiredMark={formConfig.hideRequiredMark ? false : undefined}
|
|
|
+ labelCol={formConfig.labelWidth ? {
|
|
|
+ style: {
|
|
|
+ width: `${formConfig.labelWidth}px`,
|
|
|
+ textAlign: formConfig.labelPosition === 'left' ? 'left' : formConfig.labelPosition === 'right' ? 'right' : 'left'
|
|
|
+ }
|
|
|
+ } : undefined}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ style={gridStyle}
|
|
|
+ className={layoutColumns === 1 ? 'space-y-4' : 'form-detail-grid'}
|
|
|
+ >
|
|
|
+ <style>{`
|
|
|
+ .form-detail-grid .ant-form-item {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ }
|
|
|
+ `}</style>
|
|
|
+ {(formData.rule || []).map((field: any) => {
|
|
|
+ // 如果已通过,禁用所有字段
|
|
|
+ const fieldWithDisabled = isApproved ? { ...field, disabled: true, readOnly: true } : field;
|
|
|
+ return renderFieldPreview(fieldWithDisabled);
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </AntdForm>
|
|
|
+ );
|
|
|
+ })()}
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className="py-4 text-center text-gray-400 text-sm">暂无表单内容</div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ </div>
|
|
|
+ {/* 底部按钮 */}
|
|
|
+ <div
|
|
|
+ className="flex justify-end gap-3"
|
|
|
+ style={{
|
|
|
+ padding: '16px 24px',
|
|
|
+ borderTop: '1px solid #f0f0f0',
|
|
|
+ flexShrink: 0
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Button
|
|
|
+ onClick={() => {
|
|
|
+ setDetailVisible(false);
|
|
|
+ setDetailData(null);
|
|
|
+ setFormData({ rule: [], option: {} });
|
|
|
+ setOriginalFields([]);
|
|
|
+ setOriginalConf('');
|
|
|
+ detailForm.resetFields();
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 取消
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ loading={submitLoading}
|
|
|
+ disabled={isApproved}
|
|
|
+ onClick={async () => {
|
|
|
+ try {
|
|
|
+ // 验证表单
|
|
|
+ const values = await detailForm.validateFields();
|
|
|
+
|
|
|
+ // 获取节点ID
|
|
|
+ const nodeId = detailData.nodeId || detailData.id;
|
|
|
+ if (!nodeId) {
|
|
|
+ message.error('节点ID不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setSubmitLoading(true);
|
|
|
+
|
|
|
+ // 将填写值添加到原始 fields 数组的每个 JSON 字符串中
|
|
|
+ const fieldsWithValues = originalFields.map((fieldString: string) => {
|
|
|
+ try {
|
|
|
+ // 解析字段 JSON 字符串
|
|
|
+ const fieldObj = JSON.parse(fieldString);
|
|
|
+
|
|
|
+ // 获取字段名:优先使用 name(表单实际使用的字段名),其次使用 field
|
|
|
+ const fieldName = fieldObj.name || fieldObj.field;
|
|
|
+
|
|
|
+ // 获取填写值
|
|
|
+ const fieldValue = values[fieldName];
|
|
|
+
|
|
|
+ // 添加或更新 value 字段
|
|
|
+ fieldObj.value = fieldValue !== undefined && fieldValue !== null ? fieldValue : '';
|
|
|
+
|
|
|
+ // 转回 JSON 字符串
|
|
|
+ return JSON.stringify(fieldObj);
|
|
|
+ } catch (e) {
|
|
|
+ console.error('TaskManagement: 解析字段 JSON 失败', e, fieldString);
|
|
|
+ return fieldString; // 如果解析失败,返回原始字符串
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 构建完整的表单数据对象
|
|
|
+ const submitData = {
|
|
|
+ id: detailData.formId || detailData.id,
|
|
|
+ name: detailData.nodeName || '未知',
|
|
|
+ conf: originalConf,
|
|
|
+ fields: fieldsWithValues
|
|
|
+ };
|
|
|
+
|
|
|
+ // 将表单数据转换为 JSON 字符串
|
|
|
+ const formDataString = JSON.stringify(submitData);
|
|
|
+
|
|
|
+ // 调用接口
|
|
|
+ const params: UpdateNodeApprovalParam = {
|
|
|
+ nodeId: nodeId,
|
|
|
+ approvalStatus: 'approved',
|
|
|
+ approvalOpinion: undefined, // 非审核节点,不需要审批意见
|
|
|
+ formData: formDataString
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('TaskManagement: 调用提交接口', params);
|
|
|
+ await taskManagementApi.updateNodeApproval(params);
|
|
|
+
|
|
|
+ // 根据节点类型显示不同的成功消息
|
|
|
+ const nodeType = String(detailData?.type || detailData?.nodeType || '').trim();
|
|
|
+ const isComplete = nodeType === 'complete';
|
|
|
+ message.success(isComplete ? '完成成功' : '提交成功');
|
|
|
+
|
|
|
+ // 关闭弹框
|
|
|
+ setDetailVisible(false);
|
|
|
+ setDetailData(null);
|
|
|
+ setFormData({ rule: [], option: {} });
|
|
|
+ setFormLoading(false);
|
|
|
+ setOriginalFields([]);
|
|
|
+ setOriginalConf('');
|
|
|
+ detailForm.resetFields();
|
|
|
+
|
|
|
+ // 刷新列表
|
|
|
+ getList();
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('TaskManagement: 提交失败', error);
|
|
|
+ if (error?.errorFields) {
|
|
|
+ // 表单验证失败
|
|
|
+ message.error('请填写完整的表单内容');
|
|
|
+ } else {
|
|
|
+ message.error(error?.message || '提交失败');
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ setSubmitLoading(false);
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {(() => {
|
|
|
+ // 从 detailData.type 或 detailData.nodeType 获取节点类型
|
|
|
+ const nodeType = String(detailData?.type || detailData?.nodeType || '').trim();
|
|
|
+ const isComplete = nodeType === 'complete';
|
|
|
+ return isComplete ? '完成' : '提交';
|
|
|
+ })()}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })()}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return <div className="py-8 text-center text-gray-500">暂无数据</div>;
|
|
|
+ })()}
|
|
|
+ </Modal>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|