| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661 |
- import React, { useState, useEffect, useMemo } from 'react';
- import { ChevronDown, ChevronUp, Clock, PlayCircle, CheckCircle, AlertTriangle, List, Plus, RefreshCw, AlertCircle, FileText, Rocket } from 'lucide-react';
- import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, AreaChart, Area } from 'recharts';
- import { Card, Collapse, Modal, Form as AntdForm, Input, Button, message, Select, DatePicker, InputNumber, Switch, Radio, Checkbox, Cascader, Upload, Alert } from 'antd';
- import { UploadOutlined, LockOutlined, CheckCircleOutlined } from '@ant-design/icons';
- const { Panel } = Collapse;
- import { cockpitApi, CockpitStatisticsVO, JobVO, TaskVO } from '../api/cockpit';
- import { dictDataApi, DictDataVO } from '../api/DictData';
- import { taskManagementApi, MyTaskVO, MyTaskPageParam, MyTaskNodeDetailVO, UpdateNodeApprovalParam } from '../api/mytask';
- import { managerHomeApi, ManagerWorkCountVO, ManagerWorkItemVO, ManagerDayWorkCountVO } from '../api/managerHome';
- import { dateFormatter } from '../utils/formatTime';
- import { toast } from 'sonner';
- import { useNavigate } from 'react-router-dom';
- import dayjs, { Dayjs } from 'dayjs';
- import { setConfAndFields2, FormCreateData } from '../utils/formCreate';
- import { useTranslation } from 'react-i18next';
- // 辅助函数:安全地将值转换为 dayjs 对象
- const safeToDayjs = (value: any): dayjs.Dayjs | null => {
- if (!value && value !== 0) {
- return null;
- }
-
- // 如果是数字(时间戳)
- if (typeof value === 'number') {
- let timestamp = value;
- if (timestamp > 0 && timestamp < 10000000000) {
- timestamp = timestamp * 1000;
- }
- const dayjsValue = dayjs(timestamp);
- if (dayjsValue.isValid()) {
- return dayjsValue;
- } else {
- return null;
- }
- }
-
- // 如果是字符串
- if (typeof value === 'string' && value.trim() !== '') {
- const numValue = Number(value);
- if (!isNaN(numValue) && numValue > 0 && numValue.toString() === value.trim()) {
- let timestamp = numValue;
- if (timestamp < 10000000000) {
- timestamp = timestamp * 1000;
- }
- const dayjsValue = dayjs(timestamp);
- if (dayjsValue.isValid()) {
- return dayjsValue;
- } else {
- return null;
- }
- } else {
- const dayjsValue = dayjs(value);
- return dayjsValue.isValid() ? dayjsValue : null;
- }
- }
-
- // 如果已经是 dayjs 对象
- if (value && typeof value === 'object' && typeof value.isValid === 'function') {
- try {
- return value.isValid() ? value : null;
- } catch (e) {
- // isValid 调用失败
- }
- }
-
- // 如果是对象,检查是否有 $d 属性
- if (value && typeof value === 'object') {
- try {
- 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) {
- 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) {
- // 解析失败,跳过
- }
- });
-
- // 转换日期值
- 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) {
- convertedValues[fieldName] = [startDayjs, endDayjs];
- } else {
- convertedValues[fieldName] = undefined;
- }
- } else {
- convertedValues[fieldName] = undefined;
- }
- } else {
- convertedValues[fieldName] = value;
- }
- });
-
- return convertedValues;
- };
- export default function Dashboard() {
- const { t } = useTranslation();
- const navigate = useNavigate();
- const [loading, setLoading] = useState(true);
- const [expandedJobs, setExpandedJobs] = useState<Set<number>>(new Set());
- const [selectedJob, setSelectedJob] = useState<number | null>(null);
- const [jobStatusDictList, setJobStatusDictList] = useState<DictDataVO[]>([]);
- const [approvalStatusDictList, setApprovalStatusDictList] = useState<DictDataVO[]>([]);
- const [jobTaskMap, setJobTaskMap] = useState<Map<number, MyTaskVO[]>>(new Map());
- const [taskLoadingMap, setTaskLoadingMap] = useState<Map<number, boolean>>(new Map());
- const [statistics, setStatistics] = useState<CockpitStatisticsVO>({
- materialsCount: 0,
- loanMaterialsCount: 0,
- totalOperations: 0,
- totalMaterials: 0,
- borrowedMaterials: 0,
- workstationCount: 0,
- workstationDistribution: [],
- operationTypeDistribution: [],
- });
- // 管理员工作统计数据
- const [managerWorkCount, setManagerWorkCount] = useState<ManagerWorkCountVO>({});
- const [managerWorkList, setManagerWorkList] = useState<ManagerWorkItemVO[]>([]);
- const [managerDayWorkCount, setManagerDayWorkCount] = useState<ManagerDayWorkCountVO[]>([]);
- const [managerDataLoading, setManagerDataLoading] = useState(false);
-
- // 任务详情弹框相关状态
- const [taskDetailVisible, setTaskDetailVisible] = useState(false);
- const [taskDetailLoading, setTaskDetailLoading] = useState(false);
- const [taskDetailData, setTaskDetailData] = useState<MyTaskNodeDetailVO | null>(null);
- const [formData, setFormData] = useState<FormCreateData>({
- rule: [],
- option: {}
- });
- const [formLoading, setFormLoading] = useState(false);
- const [originalFields, setOriginalFields] = useState<string[]>([]);
- const [originalConf, setOriginalConf] = useState<string>('');
- const [taskDetailForm] = AntdForm.useForm();
- const [approvalComment, setApprovalComment] = useState('');
- const [approvalLoading, setApprovalLoading] = useState(false);
- const [submitLoading, setSubmitLoading] = useState(false);
- const [showOverdueJobs, setShowOverdueJobs] = useState(false); // 默认隐藏逾期作业
- // 获取统计数据
- const fetchStatistics = async () => {
- setLoading(true);
- try {
- const response = await cockpitApi.getHomePage();
- console.log('API原始响应:', response);
-
- // API返回格式: { code: 0, data: {...}, msg: "" }
- const data = (response && typeof response === 'object' && 'data' in response)
- ? response.data
- : response;
-
- console.log('提取的data:', data);
-
- // 计算总作业数
- const totalOperations = data.jobList?.length || 0;
-
- // 计算作业状态统计
- const jobList = data.jobList || [];
- let pendingCount = 0;
- let inProgressCount = 0;
- let completedCount = 0;
- let overdueCount = 0;
-
- jobList.forEach((job: any) => {
- const status = job.status || job.jobStatus || '';
- if (status === '待执行' || status === 'pending' || status === 'PENDING') {
- pendingCount++;
- } else if (status === '进行中' || status === 'in_progress' || status === 'IN_PROGRESS') {
- inProgressCount++;
- } else if (status === '已完成' || status === 'completed' || status === 'COMPLETED') {
- completedCount++;
- } else if (status === '逾期' || status === 'overdue' || status === 'OVERDUE') {
- overdueCount++;
- } else {
- // 如果没有明确状态,根据其他字段判断
- if (job.progress === 0 || !job.progress) {
- pendingCount++;
- } else if (job.progress === 100) {
- completedCount++;
- } else {
- inProgressCount++;
- }
- }
- });
-
- // 生成最近30天的趋势数据(如果API没有提供,则生成模拟数据)
- const trendData: { date: string; completedCount: number }[] = [];
- if (data.jobTrendData && Array.isArray(data.jobTrendData)) {
- // 使用API提供的数据
- trendData.push(...data.jobTrendData);
- } else {
- // 生成模拟数据(实际应该从API获取)
- for (let i = 1; i <= 30; i++) {
- trendData.push({
- date: `${i}日`,
- completedCount: Math.floor(Math.random() * 5), // 0-4之间的随机数
- });
- }
- }
-
- // 计算工作站数量(去重)
- const workstationSet = new Set<string>();
- jobList.forEach((job: any) => {
- if (job.workstationName) {
- workstationSet.add(job.workstationName);
- }
- });
- const workstationCount = workstationSet.size;
-
- // 计算工作站作业分布
- const workstationMap = new Map<string, number>();
- jobList.forEach((job: any) => {
- const workstation = job.workstationName || '未知';
- workstationMap.set(workstation, (workstationMap.get(workstation) || 0) + 1);
- });
- const workstationDistribution = Array.from(workstationMap.entries()).map(([workstation, count]) => ({
- workstation,
- count,
- }));
-
- // 计算作业类型分布
- const typeMap = new Map<string, number>();
- jobList.forEach((job: any) => {
- // 从ticketName中提取作业类型
- const ticketName = job.ticketName || '';
- let type = '未知';
- if (ticketName.includes('换产') || ticketName.includes('投产')) {
- type = '投产';
- } else if (ticketName.includes('维修')) {
- type = '维修';
- } else if (ticketName.includes('PM')) {
- type = 'PM';
- } else if (job.jobType) {
- type = job.jobType;
- }
- typeMap.set(type, (typeMap.get(type) || 0) + 1);
- });
- const typeEntries = Array.from(typeMap.entries());
- const totalJobs = typeEntries.reduce((sum, [, count]) => sum + count, 0);
- const operationTypeDistribution = typeEntries.map(([type, count]) => ({
- type,
- count,
- percentage: totalJobs > 0 ? Math.round((count / totalJobs) * 100) : 0,
- }));
-
- // 判断物料状态
- const materialStatus = data.exceptionMaterialsCount && data.exceptionMaterialsCount > 0 ? '异常' : '正常';
-
- // 判断借用状态
- const borrowedStatus = data.loanMaterialsCount && data.loanMaterialsCount > 0 ? '关注' : '正常';
-
- // 判断工作站状态
- const workstationStatus = workstationCount > 0 ? '活跃' : '非活跃';
-
- // 组装统计数据
- const statisticsData: CockpitStatisticsVO = {
- ...data,
- totalOperations,
- totalMaterials: data.materialsCount || 0,
- materialStatus,
- borrowedMaterials: data.loanMaterialsCount || 0,
- borrowedStatus,
- workstationCount,
- workstationStatus,
- workstationDistribution,
- operationTypeDistribution,
- // 作业状态统计
- pendingJobsCount: pendingCount,
- inProgressJobsCount: inProgressCount,
- completedJobsCount: completedCount,
- overdueJobsCount: overdueCount,
- // 趋势数据
- jobTrendData: trendData,
- // 物料统计
- availableMaterialsCount: data.availableMaterialsCount || (data.materialsCount || 0) - (data.loanMaterialsCount || 0) - (data.exceptionMaterialsCount || 0) - (data.returnMaterialsCount || 0),
- returnMaterialsCount: data.returnMaterialsCount || 0,
- };
-
- console.log('处理后的统计数据:', statisticsData);
- setStatistics(statisticsData);
- } catch (error: any) {
- console.error('获取驾驶舱统计数据失败:', error);
- toast.error(error.message || '获取统计数据失败');
- } finally {
- setLoading(false);
- }
- };
- // 获取作业状态字典数据
- const getJobStatusDictList = async () => {
- try {
- const response = await dictDataApi.getDictDataPage({
- pageNo: 1,
- pageSize: -1,
- dictType: 'job_status',
- });
- const data = (response && typeof response === 'object' && 'data' in response)
- ? response.data
- : response;
- setJobStatusDictList(data?.list || []);
- } catch (error: any) {
- console.error('获取作业状态字典失败:', error);
- setJobStatusDictList([]);
- }
- };
- // 获取审批状态字典数据
- const getApprovalStatusDictList = async () => {
- try {
- const response = await dictDataApi.getDictDataPage({
- pageNo: 1,
- pageSize: -1,
- dictType: 'approval_status',
- });
- const data = (response && typeof response === 'object' && 'data' in response)
- ? response.data
- : response;
- setApprovalStatusDictList(data?.list || []);
- console.log('Dashboard: 获取审批状态字典成功', data?.list || []);
- } catch (error: any) {
- console.error('获取审批状态字典失败:', error);
- setApprovalStatusDictList([]);
- }
- };
- // 获取管理员工作统计数据
- const fetchManagerWorkCount = async () => {
- try {
- const response = await managerHomeApi.getManagerWorkCount();
- const data = (response && typeof response === 'object' && 'data' in response)
- ? response.data
- : response;
- setManagerWorkCount(data || {});
- } catch (error: any) {
- console.error('获取管理员工作统计失败:', error);
- toast.error(error.message || '获取管理员工作统计失败');
- setManagerWorkCount({});
- }
- };
- // 获取管理员工作列表
- const fetchManagerWorkList = async () => {
- try {
- const response = await managerHomeApi.getManagerWorkList({
- pageNo: 1,
- pageSize: -1,
- });
- console.log('获取管理员工作列表原始响应:', response);
-
- // 接口返回的已经是数组格式(API 层已经处理了 {code, data, msg} 格式)
- const data: ManagerWorkItemVO[] = Array.isArray(response) ? response : [];
- console.log('设置管理员工作列表:', data);
-
- setManagerWorkList(data);
- console.log('获取管理员工作列表成功,设置的数据:', data, '数据长度:', data.length);
- } catch (error: any) {
- console.error('获取管理员工作列表失败:', error);
- toast.error(error.message || '获取管理员工作列表失败');
- setManagerWorkList([]);
- }
- };
- // 获取管理员每日工作统计
- const fetchManagerDayWorkCount = async () => {
- try {
- // 计算最近30天的日期范围
- const endDate = new Date();
- const startDate = new Date();
- startDate.setDate(startDate.getDate() - 30);
-
- const response = await managerHomeApi.getManagerDayWorkCount({
- startDate: startDate.toISOString().split('T')[0],
- endDate: endDate.toISOString().split('T')[0],
- });
- // API 层已经处理了 {code, data, msg} 格式,返回的已经是数组
- const data: ManagerDayWorkCountVO[] = Array.isArray(response) ? response : [];
- console.log('获取管理员每日工作统计:', data);
- setManagerDayWorkCount(data);
- } catch (error: any) {
- console.error('获取管理员每日工作统计失败:', error);
- toast.error(error.message || '获取管理员每日工作统计失败');
- setManagerDayWorkCount([]);
- }
- };
- // 获取所有管理员数据
- const fetchManagerData = async () => {
- setManagerDataLoading(true);
- try {
- await Promise.all([
- fetchManagerWorkCount(),
- fetchManagerWorkList(),
- fetchManagerDayWorkCount(),
- ]);
- } catch (error: any) {
- console.error('获取管理员数据失败:', error);
- } finally {
- setManagerDataLoading(false);
- }
- };
- useEffect(() => {
- fetchStatistics();
- getJobStatusDictList();
- getApprovalStatusDictList();
- fetchManagerData();
- }, []);
- // 获取作业的任务列表
- // 从作业的 workflowWorkNodeDOList 中提取任务列表并转换为 MyTaskVO 格式
- const extractTasksFromJob = (job: ManagerWorkItemVO): MyTaskVO[] => {
- if (!job.workflowWorkNodeDOList || !Array.isArray(job.workflowWorkNodeDOList)) {
- return [];
- }
-
- return job.workflowWorkNodeDOList.map((node: any) => {
- return {
- id: node.id,
- nodeId: node.id, // 节点ID作为任务ID
- workId: job.id, // 作业ID
- name: node.nodeName || node.currentNodeName || node.currentNode || '-',
- currentNodeName: node.nodeName || node.currentNodeName || node.currentNode || '-',
- currentNode: node.currentNodeName || node.currentNode || '-',
- workerUserName: node.workerUserName || node.responsibleName || node.responsible || '-',
- responsibleName: node.workerUserName || node.responsibleName || node.responsible || '-',
- responsible: node.workerUserName || node.responsibleName || node.responsible || '-',
- workTime: node.workTime || node.taskStartTime || node.initiationTime,
- taskStartTime: node.workTime || node.taskStartTime || node.initiationTime,
- initiationTime: node.workTime || node.taskStartTime || node.initiationTime,
- approvalStatus: node.approvalStatus || node.status,
- taskStatus: node.approvalStatus || node.status,
- status: node.approvalStatus || node.status,
- orderNo: job.orderNo,
- initiatorName: job.initiatorName || job.initiator || '-',
- initiator: job.initiatorName || job.initiator || '-',
- } as MyTaskVO;
- });
- };
- // 计算作业的整体进度(已完成任务数 / 总任务数)
- const calculateJobProgress = (job: ManagerWorkItemVO): number => {
- if (!job.workflowWorkNodeDOList || !Array.isArray(job.workflowWorkNodeDOList)) {
- return 0;
- }
-
- const totalTasks = job.workflowWorkNodeDOList.length;
- if (totalTasks === 0) {
- return 0;
- }
-
- // 统计已完成的任务数量
- const completedTasks = job.workflowWorkNodeDOList.filter((node: any) => {
- const status = String(node.approvalStatus || node.status || '').toLowerCase();
- // 判断任务是否已完成:状态为 'approved', 'completed', '已完成' 等
- return status === 'approved' ||
- status === 'completed' ||
- status === '已完成' ||
- status === '完成' ||
- status === 'COMPLETED';
- }).length;
-
- // 计算百分比并四舍五入
- const progress = Math.round((completedTasks / totalTasks) * 100);
- return progress;
- };
- // 切换作业展开/折叠
- const toggleJobExpand = (jobId: number) => {
- console.log('Dashboard: toggleJobExpand 被调用', { jobId, currentExpanded: Array.from(expandedJobs) });
-
- const isCurrentlyExpanded = expandedJobs.has(jobId);
-
- if (isCurrentlyExpanded) {
- // 如果当前作业已展开,则折叠它
- setExpandedJobs(new Set());
- setSelectedJob(null);
- console.log('Dashboard: 折叠作业', jobId);
- } else {
- // 展开新作业时,先清空所有已展开的作业,确保每次只展开一个
- const newExpanded = new Set<number>();
- newExpanded.add(jobId);
- setExpandedJobs(newExpanded);
- setSelectedJob(jobId); // 展开时选中
- console.log('Dashboard: 展开作业', jobId);
-
- // 从作业数据中直接提取任务列表
- const job = managerWorkList.find(j => j.id === jobId);
- console.log('Dashboard: 查找作业', { jobId, job, jobListLength: managerWorkList.length });
-
- if (job) {
- console.log('Dashboard: 从作业数据中提取任务列表', { jobId, workflowWorkNodeDOList: job.workflowWorkNodeDOList });
- const taskList = extractTasksFromJob(job);
- console.log('Dashboard: 提取的任务列表', taskList, '任务数量:', taskList.length);
- setJobTaskMap(prev => new Map(prev).set(jobId, taskList));
- // 设置加载状态为完成(因为数据已经在作业数据中了,不需要异步加载)
- setTaskLoadingMap(prev => {
- const newMap = new Map(prev);
- newMap.delete(jobId);
- return newMap;
- });
- } else {
- console.warn('Dashboard: 未找到作业', jobId);
- setJobTaskMap(prev => new Map(prev).set(jobId, []));
- }
- }
- };
- // 选中作业
- const handleJobSelect = (jobId: number) => {
- setSelectedJob(jobId);
- };
- // 处理作业行点击(选中并切换展开/折叠)
- const handleJobRowClick = (jobId: number) => {
- setSelectedJob(jobId);
- toggleJobExpand(jobId);
- };
- 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 || [];
- 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) => {
- if (!value) return undefined;
- if (value && typeof value === 'object' && 'valueOf' in value && typeof value.valueOf === 'function' && 'isValid' in value && typeof value.isValid === 'function') {
- if (value.isValid()) {
- return value.valueOf();
- }
- return undefined;
- }
- return value;
- }}
- normalize={(value) => {
- 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) => {
- 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') {
- if (start.isValid() && end.isValid()) {
- return [start.valueOf(), end.valueOf()];
- }
- return undefined;
- }
- return value;
- }}
- normalize={(value) => {
- 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) => {
- if (!value) return undefined;
- if (value && typeof value === 'object' && 'valueOf' in value) {
- return value.valueOf();
- }
- return value;
- }}
- normalize={(value) => {
- 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') {
- 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 {
- 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 handleViewTaskDetail = async (task: MyTaskVO) => {
- console.log('Dashboard: handleViewTaskDetail 被调用', task);
-
- if (!task.nodeId) {
- console.warn('Dashboard: 节点ID不存在', task);
- message.warning('节点ID不存在');
- return;
- }
-
- setTaskDetailLoading(true);
- setTaskDetailVisible(true);
- console.log('Dashboard: 弹框状态已设置为 true (加载中)');
-
- try {
- console.log('Dashboard: 开始获取节点详情', { nodeId: task.nodeId });
- const response = await taskManagementApi.getAdminWorkNodeDetail(task.nodeId);
- console.log('Dashboard: 节点详情响应', 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('Dashboard: ✅ 从 data.formId (字符串) 提取到 formId', extractedFormId);
- }
- } else if (typeof formIdValue === 'number') {
- extractedFormId = formIdValue;
- console.log('Dashboard: ✅ 从 data.formId (数字) 提取到 formId', extractedFormId);
- }
- }
-
- if (extractedFormId === undefined) {
- console.warn('Dashboard: ⚠️ 未能提取到 formId', {
- 'data.formId': data?.formId,
- 'data': data
- });
- }
-
- // 合并列表数据中的作业信息
- const detailDataWithWorkInfo: MyTaskNodeDetailVO = {
- ...(data || {}),
- workName: task.name || data?.workName || data?.name,
- orderNo: task.orderNo || data?.orderNo,
- workerUserName: task.workerUserName || data?.workerUserName,
- initiatorName: task.initiatorName || data?.initiatorName || data?.initiator || task.initiator || '',
- workTime: task.workTime || data?.workTime,
- type: data?.type || data?.nodeType || '',
- nodeType: data?.nodeType || data?.type || '',
- formId: extractedFormId,
- };
-
- console.log('Dashboard: 合并后的详情数据', detailDataWithWorkInfo);
-
- // 先设置数据,确保弹框有内容显示
- setTaskDetailData(detailDataWithWorkInfo);
- setTaskDetailVisible(true);
- setTaskDetailLoading(false);
-
- // 根据节点类型决定是否需要获取表单
- 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';
- const formId = detailDataWithWorkInfo.formId;
- const hasFormId = formId !== undefined && formId !== null && (
- typeof formId === 'number' ||
- (typeof formId === 'string' && formId !== '' && formId.trim() !== '')
- );
- const hasFormData = detailDataWithWorkInfo.formData && (
- (typeof detailDataWithWorkInfo.formData === 'string' && detailDataWithWorkInfo.formData.trim() !== '') ||
- (typeof detailDataWithWorkInfo.formData === 'object' && Object.keys(detailDataWithWorkInfo.formData).length > 0)
- );
-
- // 如果任务状态为"已通过"且有 formData,则从 formData 中解析表单结构
- if (isApproved && hasFormData) {
- console.log('Dashboard: ✅ 任务已通过,从 formData 中解析表单结构');
- setFormLoading(true);
-
- try {
- let parsedFormData: any;
- if (typeof detailDataWithWorkInfo.formData === 'string') {
- parsedFormData = JSON.parse(detailDataWithWorkInfo.formData);
- } else {
- parsedFormData = detailDataWithWorkInfo.formData;
- }
-
- const conf = parsedFormData.conf;
- const fields = parsedFormData.fields;
-
- if (conf && fields) {
- 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);
-
- // 解析字段值用于回显
- 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('Dashboard: 解析字段 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;
-
- 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') {
- 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('Dashboard: 解析字段 JSON 失败', e, fieldString);
- }
- });
-
- setConfAndFields2(setFormData, conf, fields);
- const convertedFormValues = convertDateValues(formValues, fieldsStringArray);
-
- setTimeout(() => {
- taskDetailForm.setFieldsValue(convertedFormValues);
- console.log('Dashboard: 表单数据已回填(从 formData)', convertedFormValues);
- }, 100);
- } else {
- console.warn('Dashboard: formData 中缺少 conf 或 fields', { conf: !!conf, fields: !!fields });
- message.warning('表单数据不完整');
- }
- } catch (e) {
- console.error('Dashboard: 解析 formData 失败', e);
- message.error('解析表单数据失败: ' + (e instanceof Error ? e.message : String(e)));
- } finally {
- setFormLoading(false);
- }
- }
- // 如果不是隔离类型,且有表单ID,且不是从 formData 获取,则从接口获取表单配置
- else if (!isIsolation && !isReleaseIsolation && !isReturnLock && hasFormId) {
- console.log('Dashboard: ✅ 满足条件,开始获取表单', { formId, nodeType });
- setFormData({ rule: [], option: {} });
- setOriginalFields([]);
- setOriginalConf('');
- setFormLoading(true);
-
- (async () => {
- try {
- const numericFormId = typeof formId === 'string' ? parseInt(formId, 10) : formId;
- if (isNaN(numericFormId)) {
- console.error('Dashboard: formId 不是有效数字', formId);
- message.error('表单ID无效');
- setFormLoading(false);
- return;
- }
-
- console.log('Dashboard: 🚀 开始调用表单接口', { formId: numericFormId, nodeType });
- const FormApi = await import('../api/bpm/form');
- const formDetailResponse = await FormApi.getForm(numericFormId);
- console.log('Dashboard: ✅ 表单接口调用成功', formDetailResponse);
-
- let formDetail: any = formDetailResponse;
- if (formDetailResponse && typeof formDetailResponse === 'object' && 'data' in formDetailResponse) {
- formDetail = (formDetailResponse as any).data || formDetailResponse;
- }
-
- const conf = formDetail?.conf || formDetail?.formConfig;
- const fields = formDetail?.fields || formDetail?.formFields;
-
- if (conf && fields) {
- 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);
-
- setConfAndFields2(setFormData, conf, fields);
-
- // 如果有表单数据值,回填到表单
- if (detailDataWithWorkInfo.formData) {
- try {
- let formValues = typeof detailDataWithWorkInfo.formData === 'string'
- ? JSON.parse(detailDataWithWorkInfo.formData)
- : detailDataWithWorkInfo.formData;
-
- formValues = convertDateValues(formValues, fieldsArray);
-
- 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') {
- if (isValidDayjs(value)) {
- 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') {
- 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 {
- taskDetailForm.setFieldsValue(cleanedFormValues);
- console.log('Dashboard: 表单数据已回填', cleanedFormValues);
- } catch (e) {
- console.error('Dashboard: 设置表单值失败', e);
- }
- }, 100);
- } catch (e) {
- console.error('Dashboard: 解析表单数据失败', e);
- }
- }
- } else {
- console.warn('Dashboard: 表单详情缺少配置或字段', { conf: !!conf, fields: !!fields });
- message.warning('表单配置不完整');
- }
- } catch (e) {
- console.error('Dashboard: 获取表单详情失败', e);
- message.error('获取表单详情失败: ' + (e instanceof Error ? e.message : String(e)));
- } finally {
- setFormLoading(false);
- }
- })();
- } else {
- setFormLoading(false);
- }
-
- // 回显审核意见
- if (detailDataWithWorkInfo.approvalOpinion && detailDataWithWorkInfo.approvalOpinion !== 'pending') {
- setApprovalComment(detailDataWithWorkInfo.approvalOpinion);
- } else {
- setApprovalComment('');
- }
-
- console.log('Dashboard: 弹框已打开', { detailVisible: true, detailData: detailDataWithWorkInfo });
- } catch (error: any) {
- console.error('Dashboard: 获取节点详情失败', error);
- toast.error(error.message || '获取节点详情失败');
- if (!taskDetailData) {
- setTaskDetailData({
- id: task.nodeId,
- nodeId: task.nodeId,
- workId: task.workId,
- workName: task.name,
- orderNo: task.orderNo,
- workerUserName: task.workerUserName,
- workTime: task.workTime,
- type: '',
- } as MyTaskNodeDetailVO);
- }
- setTaskDetailVisible(true);
- } finally {
- setTaskDetailLoading(false);
- }
- };
- // 获取任务状态文本(使用字典)
- 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 || '未知';
- }
- // 如果没有找到字典值,使用默认映射
- const statusMap: Record<string, string> = {
- 'pending': '待审核',
- 'approved': '已通过',
- 'rejected': '已驳回',
- 'unaudited': '未审核',
- };
- return statusMap[statusStr] || '未知';
- };
- // 获取任务状态样式(审批状态)
- const getTaskStatusStyle = (status: string | number | undefined): React.CSSProperties => {
- if (!status) {
- return {
- backgroundColor: '#e5e5e5',
- color: '#333333',
- };
- }
- const statusStr = String(status).toLowerCase();
-
- // 先获取状态文本,根据文本判断颜色
- const statusText = getTaskStatusText(status);
- const statusTextLower = statusText.toLowerCase();
-
- // 根据状态文本判断颜色
- // 待执行、待发布:灰色 #e5e5e5
- if (statusTextLower.includes('待执行') || statusTextLower.includes('待发布') ||
- statusTextLower.includes('未开始') || statusTextLower.includes('未审核') ||
- statusTextLower.includes('待开始') || statusTextLower.includes('unaudited')) {
- return {
- backgroundColor: '#e5e5e5',
- color: '#333333',
- };
- }
- // 进行中:蓝色 #1677ff
- if (statusTextLower.includes('进行中') || statusTextLower.includes('待审核') ||
- statusTextLower.includes('pending') || statusTextLower.includes('执行中')) {
- return {
- backgroundColor: '#1677ff',
- color: '#ffffff',
- };
- }
- // 已完成:绿色 #0acb57
- if (statusTextLower.includes('已完成') || statusTextLower.includes('已通过') ||
- statusTextLower.includes('approved') || statusTextLower.includes('执行完成')) {
- return {
- backgroundColor: '#0acb57',
- color: '#ffffff',
- };
- }
- // 已退回、已驳回:橙色 #ff9500
- if (statusTextLower.includes('已退回') || statusTextLower.includes('已驳回') ||
- statusTextLower.includes('rejected')) {
- return {
- backgroundColor: '#ff9500',
- color: '#ffffff',
- };
- }
-
- // 默认样式
- return {
- backgroundColor: '#e5e5e5',
- color: '#333333',
- };
- };
- // 获取任务状态文本和样式(兼容函数)
- const getTaskStatusInfo = (status: string | number | undefined) => {
- return {
- label: getTaskStatusText(status),
- style: getTaskStatusStyle(status),
- };
- };
- // 获取作业状态标签样式
- const getStatusBadge = (status: string) => {
- const statusMap: Record<string, { label: string; className: string }> = {
- '待执行': { label: '待执行', className: 'bg-gray-100 text-gray-700 border-gray-200' },
- 'pending': { label: '待执行', className: 'bg-gray-100 text-gray-700 border-gray-200' },
- 'PENDING': { label: '待执行', className: 'bg-gray-100 text-gray-700 border-gray-200' },
- '进行中': { label: '进行中', className: 'bg-blue-100 text-blue-700 border-blue-200' },
- 'in_progress': { label: '进行中', className: 'bg-blue-100 text-blue-700 border-blue-200' },
- 'IN_PROGRESS': { label: '进行中', className: 'bg-blue-100 text-blue-700 border-blue-200' },
- '已完成': { label: '已完成', className: 'bg-green-100 text-green-700 border-green-200' },
- 'completed': { label: '已完成', className: 'bg-green-100 text-green-700 border-green-200' },
- 'COMPLETED': { label: '已完成', className: 'bg-green-100 text-green-700 border-green-200' },
- '逾期': { label: '逾期', className: 'bg-red-100 text-red-700 border-red-200' },
- 'overdue': { label: '逾期', className: 'bg-red-100 text-red-700 border-red-200' },
- 'OVERDUE': { label: '逾期', className: 'bg-red-100 text-red-700 border-red-200' },
- };
-
- return statusMap[status] || { label: status || '未知', className: 'bg-gray-100 text-gray-700 border-gray-200' };
- };
- // 从ticketName中提取作业类型
- const getJobType = (ticketName: string): string => {
- if (ticketName.includes('换产') || ticketName.includes('投产')) {
- return '投产';
- } else if (ticketName.includes('维修')) {
- return '维修';
- } else if (ticketName.includes('PM')) {
- return 'PM';
- }
- return '未知';
- };
- // 获取作业状态显示文本和样式(从字典获取)
- const getJobStatusInfo = (status: string | number | undefined) => {
- if (!status) {
- return { label: '未知', className: 'bg-gray-100 text-gray-700 border-gray-200' };
- }
-
- // 从字典中查找状态文本
- const statusItem = jobStatusDictList.find(item => String(item.value) === String(status));
- const statusText = statusItem ? (statusItem.label || '') : String(status || '未知');
- const statusTextLower = statusText.toLowerCase();
-
- // 根据状态文本判断样式
- // 待执行、待发布:灰色
- if (statusTextLower.includes('待执行') || statusTextLower.includes('待发布') ||
- statusTextLower.includes('pending') || statusTextLower.includes('unreleased')) {
- return { label: statusText, className: 'bg-gray-100 text-gray-700 border-gray-200' };
- }
- // 进行中、执行中:蓝色
- if (statusTextLower.includes('进行中') || statusTextLower.includes('执行中') ||
- statusTextLower.includes('running') || statusTextLower.includes('in_progress')) {
- return { label: statusText, className: 'bg-blue-100 text-blue-700 border-blue-200' };
- }
- // 已完成、执行完成:绿色
- if (statusTextLower.includes('已完成') || statusTextLower.includes('执行完成') ||
- statusTextLower.includes('completed') || statusTextLower.includes('完成')) {
- return { label: statusText, className: 'bg-green-100 text-green-700 border-green-200' };
- }
- // 已退回:橙色
- if (statusTextLower.includes('已退回') || statusTextLower.includes('rejected')) {
- return { label: statusText, className: 'bg-orange-100 text-orange-700 border-orange-200' };
- }
- // 已跳过:灰色
- if (statusTextLower.includes('已跳过') || statusTextLower.includes('skipped')) {
- return { label: statusText, className: 'bg-gray-100 text-gray-700 border-gray-200' };
- }
-
- // 如果没有匹配到,使用字典中的文本,默认灰色样式
- return { label: statusText, className: 'bg-gray-100 text-gray-700 border-gray-200' };
- };
- if (loading) {
- return (
- <div className="flex items-center justify-center h-96">
- <div className="text-gray-500">加载中...</div>
- </div>
- );
- }
- return (
- <div className="p-6 space-y-6 bg-gray-50 min-h-screen">
- <style>{`
- .custom-collapse .ant-collapse-item {
- border: none !important;
- border-bottom: 1px solid #e5e7eb !important;
- }
- .custom-collapse .ant-collapse-item:last-child {
- border-bottom: none !important;
- }
- .custom-collapse .ant-collapse-item .ant-collapse-header {
- padding: 0 !important;
- display: flex !important;
- flex-direction: row !important;
- align-items: center !important;
- position: relative !important;
- }
- .custom-collapse .ant-collapse-item .ant-collapse-header .ant-collapse-arrow {
- display: none !important;
- }
- .custom-collapse .ant-collapse-content {
- border-top: 1px solid #e5e7eb !important;
- }
- `}</style>
- {/* 作业管理部分 */}
- <div>
- {/* 作业状态统计卡片 */}
- <div className="flex mb-6 gap-8">
- {/* 待发布作业 - 灰色主题 */}
- <div
- className="flex-1 bg-white rounded-lg border border-gray-200 p-4 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden cursor-pointer"
- onClick={() => {
- // 保存status到 sessionStorage
- sessionStorage.setItem('workManagementStatus', 'unreleased');
- // 触发自定义事件切换菜单
- window.dispatchEvent(new CustomEvent('switchToMenu', {
- detail: {
- menu: 'isolationWork',
- subMenu: 'workManagement'
- }
- }));
- }}
- >
- <div className="flex items-start justify-between mb-3">
- <div className="text-sm text-gray-500 font-medium">待发布作业</div>
- <div className="p-3 rounded-xl flex items-center justify-center flex-shrink-0" style={{ backgroundColor: 'rgba(188, 185, 183, 0.2)' }}>
- <Clock className="w-6 h-6 text-black-700" strokeWidth={2} />
- </div>
- </div>
- <div>
- <div className="text-2xl font-bold text-gray-900 leading-none">
- {managerWorkCount.unreleasedCount ?? statistics.pendingJobsCount ?? 0} <span className="text-base font-normal text-gray-500">项</span>
- </div>
- </div>
- </div>
-
- {/* 进行中作业 - 蓝色主题 */}
- <div
- className="flex-1 bg-white rounded-lg border border-gray-200 p-4 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden cursor-pointer"
- onClick={() => {
- // 保存status到 sessionStorage
- sessionStorage.setItem('workManagementStatus', 'running');
- // 触发自定义事件切换菜单
- window.dispatchEvent(new CustomEvent('switchToMenu', {
- detail: {
- menu: 'isolationWork',
- subMenu: 'workManagement'
- }
- }));
- }}
- >
- <div className="flex items-start justify-between mb-3">
- <div className="text-sm text-gray-500 font-medium">进行中作业</div>
- <div className="p-3 rounded-xl flex items-center justify-center flex-shrink-0" style={{ backgroundColor: 'rgba(59, 130, 246, 0.2)' }}>
- <PlayCircle className="w-6 h-6 text-blue-700" strokeWidth={2} />
- </div>
- </div>
- <div>
- <div className="text-2xl font-bold text-gray-900 leading-none">
- {managerWorkCount.runningCount ?? statistics.inProgressJobsCount ?? 0} <span className="text-base font-normal text-gray-500">项</span>
- </div>
- </div>
- </div>
-
- {/* 已完成作业 - 绿色主题 */}
- <div
- className="flex-1 bg-white rounded-lg border border-gray-200 p-4 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden cursor-pointer"
- onClick={() => {
- // 保存status到 sessionStorage
- sessionStorage.setItem('workManagementStatus', 'completed');
- // 触发自定义事件切换菜单
- window.dispatchEvent(new CustomEvent('switchToMenu', {
- detail: {
- menu: 'isolationWork',
- subMenu: 'workManagement'
- }
- }));
- }}
- >
- <div className="flex items-start justify-between mb-3">
- <div className="text-sm text-gray-500 font-medium">已完成作业</div>
- <div className="p-3 rounded-xl flex items-center justify-center flex-shrink-0" style={{ backgroundColor: 'rgba(34, 197, 94, 0.2)' }}>
- <CheckCircle className="w-6 h-6 text-green-700" strokeWidth={2} />
- </div>
- </div>
- <div>
- <div className="text-2xl font-bold text-gray-900 leading-none">
- {managerWorkCount.completedCount ?? statistics.completedJobsCount ?? 0} <span className="text-base font-normal text-gray-500">项</span>
- </div>
- </div>
- </div>
-
- {/* 逾期作业 - 红色主题 */}
- {showOverdueJobs && (
- <div className="flex-1 bg-white rounded-lg border border-red-200 p-4 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
- <div className="flex items-start justify-between mb-3">
- <div className="text-sm text-gray-500 font-medium">逾期作业</div>
- <div className="p-3 rounded-xl flex items-center justify-center flex-shrink-0" style={{ backgroundColor: 'rgba(239, 68, 68, 0.2)' }}>
- <AlertTriangle className="w-6 h-6 text-red-700" strokeWidth={2} />
- </div>
- </div>
- <div className="flex items-center justify-between">
- <div>
- <div className="text-2xl font-bold text-gray-900 leading-none">
- {statistics.overdueJobsCount ?? 0} <span className="text-base font-normal text-gray-500">项</span>
- </div>
- </div>
- <button
- onClick={() => setShowOverdueJobs(false)}
- className="text-red-600 text-sm font-medium hover:text-red-700 transition-colors"
- >
- 隐藏
- </button>
- </div>
- </div>
- )}
- </div>
- {/* 作业列表 */}
- <Card
- title={
- <div className="flex items-center justify-between">
- <span>作业列表 (含任务明细)</span>
- <button
- onClick={() => {
- // 通过 sessionStorage 传递参数,让 Dashboard 自动切换到作业管理菜单
- sessionStorage.setItem('navigateToMenu', JSON.stringify({
- menu: 'isolationWork',
- subMenu: 'workManagement'
- }));
-
- // 如果当前已经在 dashboard 页面,触发自定义事件来切换菜单
- if (window.location.pathname === '/dashboard') {
- // 触发自定义事件,通知 Dashboard 切换菜单
- window.dispatchEvent(new CustomEvent('switchToMenu', {
- detail: { menu: 'isolationWork', subMenu: 'workManagement' }
- }));
- } else {
- // 如果不在 dashboard 页面,导航过去
- navigate('/dashboard');
- }
- }}
- className="text-sm font-medium border-0 bg-transparent p-0 cursor-pointer"
- style={{ color: '#2563eb' }}
- onMouseEnter={(e) => e.currentTarget.style.color = '#1d4ed8'}
- onMouseLeave={(e) => e.currentTarget.style.color = '#2563eb'}
- >
- 查看全部作业
- </button>
- </div>
- }
- >
- <div className="border border-gray-200 rounded-lg overflow-hidden">
- {managerDataLoading ? (
- <div className="p-8 text-center text-gray-500">加载中...</div>
- ) : managerWorkList.length > 0 ? (
- <Collapse
- activeKey={Array.from(expandedJobs).map(id => String(id))}
- onChange={(keys) => {
- const keyArray = Array.isArray(keys) ? keys : [keys];
- if (keyArray.length === 0) {
- // 全部折叠
- setExpandedJobs(new Set());
- setSelectedJob(null);
- } else {
- // 只展开一个
- const jobId = Number(keyArray[keyArray.length - 1]);
- const newExpanded = new Set<number>();
- newExpanded.add(jobId);
- setExpandedJobs(newExpanded);
- setSelectedJob(jobId);
-
- // 从作业数据中直接提取任务列表
- const job = managerWorkList.find(j => j.id === jobId);
- if (job) {
- console.log('Dashboard: 从作业数据中提取任务列表', { jobId, workflowWorkNodeDOList: job.workflowWorkNodeDOList });
- const taskList = extractTasksFromJob(job);
- console.log('Dashboard: 提取的任务列表', taskList, '任务数量:', taskList.length);
- setJobTaskMap(prev => new Map(prev).set(jobId, taskList));
- // 设置加载状态为完成(因为数据已经在作业数据中了,不需要异步加载)
- setTaskLoadingMap(prev => {
- const newMap = new Map(prev);
- newMap.delete(jobId);
- return newMap;
- });
- } else {
- console.warn('Dashboard: 未找到作业', jobId);
- setJobTaskMap(prev => new Map(prev).set(jobId, []));
- }
- }
- }}
- ghost
- style={{ background: 'transparent' }}
- className="custom-collapse"
- >
- {managerWorkList.map((job: ManagerWorkItemVO, index: number) => {
- const isExpanded = expandedJobs.has(job.id!);
- const isSelected = selectedJob === job.id;
- const statusInfo = getTaskStatusInfo(job.status);
- const jobName = job.name || job.orderNo || job.code || `作业#${job.id}`;
- const responsiblePerson = job.initiatorName || job.initiator || '未分配';
-
- return (
- <Panel
- key={String(job.id)}
- showArrow={false}
- header={
- <div
- className="p-3 flex items-center relative w-full"
- style={isSelected ? { backgroundColor: '#eff6ff' } : { backgroundColor: 'white' }}
- onClick={(e) => {
- e.stopPropagation();
- handleJobRowClick(job.id!);
- }}
- >
- {/* 作业名称 - 固定宽度100px,超出显示省略号 */}
- <div className="flex-shrink-0 min-w-0" style={{ width: '300px' }}>
- <span className={`text-base truncate block ${
- isSelected ? '' : 'text-gray-900'
- }`} style={isSelected ? { color: '#2563eb' } : { color: '#111827' }}>
- {jobName}
- </span>
- </div>
-
- {/* 状态标签 - 固定宽度 */}
- <div className="flex-shrink-0 ml-6" style={{ width: '80px',marginRight:'50px' }}>
- <span className="inline-flex px-2 py-0.5 rounded text-xs font-medium whitespace-nowrap" style={statusInfo.style}>
- {statusInfo.label}
- </span>
- </div>
-
- {/* 作业发起人 - 固定宽度 */}
- <div className="flex-shrink-0 ml-6" style={{ width: '160px' }}>
- <span className={`text-sm truncate block ${
- isSelected ? '' : 'text-gray-900'
- }`} style={isSelected ? { color: '#2563eb' } : { color: '#111827' }}>
- <span>作业发起人:</span>{responsiblePerson}
- </span>
- </div>
- {/* 整体进度 - 固定宽度和位置,右对齐 */}
- <div className="flex-shrink-0" style={{ width: '128px', textAlign: 'right' }}>
- <span className={`text-sm whitespace-nowrap ${
- isSelected ? '' : 'text-gray-900'
- }`} style={isSelected ? { color: '#2563eb' } : { color: '#111827' }}>
- <span>整体进度:</span>{calculateJobProgress(job)}%
- </span>
- </div>
- {/* 右侧弹性空间 */}
- <div className="flex-1"></div>
-
- {/* 自定义箭头 - 最右侧 */}
- <div className="flex-shrink-0 ml-4" style={{ width: '24px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
- {isExpanded ? (
- <ChevronUp className="w-5 h-5 text-gray-600" />
- ) : (
- <ChevronDown className="w-5 h-5 text-gray-600" />
- )}
- </div>
- </div>
- }
- className="custom-panel"
- >
- <div className="px-3 pb-3 pt-3 bg-white">
- <div className="overflow-x-auto">
- {taskLoadingMap.get(job.id!) ? (
- <div className="px-4 py-8 text-center text-sm text-gray-500">加载中...</div>
- ) : (
- <table className="w-full border-collapse">
- <thead>
- <tr className="bg-gray-50 border-b border-gray-200">
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">任务名称</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">任务负责人</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">开始时间</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">状态</th>
- <th className="px-4 py-3 text-center text-xs font-medium text-gray-700">操作</th>
- </tr>
- </thead>
- <tbody className="bg-white">
- {(() => {
- const tasks = jobTaskMap.get(job.id!);
- console.log('Dashboard: 渲染任务列表', { jobId: job.id, tasks, tasksLength: tasks?.length });
-
- if (tasks && tasks.length > 0) {
- return tasks.map((task: MyTaskVO, taskIndex: number) => {
- const taskStatus = task.approvalStatus || task.taskStatus || task.status;
- const statusInfo = getTaskStatusInfo(taskStatus);
- const taskName = task.currentNodeName || task.currentNode || '-';
- const responsiblePerson = task.workerUserName || task.responsibleName || task.responsible || '-';
- const startTime = task.workTime || task.taskStartTime || task.initiationTime;
-
- return (
- <tr key={task.id || taskIndex} className="border-b border-gray-100 hover:bg-gray-50">
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
- {taskName}
- </td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">
- {responsiblePerson}
- </td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">
- {startTime ? dateFormatter(startTime) : '-'}
- </td>
- <td className="px-4 py-3 whitespace-nowrap text-sm">
- <span className="inline-flex px-2 py-0.5 rounded text-xs font-medium" style={statusInfo.style}>
- {statusInfo.label}
- </span>
- </td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-center">
- <button
- className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
- style={{ backgroundColor: '#1677ff', color: '#ffffff' }}
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#0958d9'}
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#1677ff'}
- onClick={() => {
- handleViewTaskDetail(task);
- }}
- >
- 查看
- </button>
- </td>
- </tr>
- );
- });
- } else {
- return (
- <tr>
- <td colSpan={5} className="px-4 py-8 text-center text-sm text-gray-500">
- 暂无任务明细
- </td>
- </tr>
- );
- }
- })()}
- </tbody>
- </table>
- )}
- </div>
- </div>
- </Panel>
- );
- })}
- </Collapse>
- ) : (
- <div className="p-8 text-center text-gray-500">暂无作业数据</div>
- )}
- </div>
- </Card>
- </div>
- {/* 全系统作业完成趋势图表 */}
- <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
- <h3 className="text-lg font-semibold text-gray-900 mb-6">
- 全系统作业完成趋势 (最近30天)
- </h3>
- <div className="h-80">
- <ResponsiveContainer width="100%" height="100%">
- <AreaChart
- data={managerDayWorkCount.length > 0
- ? managerDayWorkCount.map(item => ({
- date: item.day || item.date || '',
- completedCount: item.completedCount || item.count || 0
- }))
- : (statistics.jobTrendData || [])
- }
- margin={{ top: 10, right: 30, left: 20, bottom: 20 }}
- >
- <defs>
- <linearGradient id="colorCompleted" x1="0" y1="0" x2="0" y2="1">
- <stop offset="5%" stopColor="#3b82f6" stopOpacity={0.8}/>
- <stop offset="95%" stopColor="#3b82f6" stopOpacity={0.1}/>
- </linearGradient>
- </defs>
- <CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
- <XAxis
- dataKey="date"
- stroke="#6b7280"
- tick={{ fontSize: 12, fill: '#6b7280' }}
- axisLine={{ stroke: '#e5e7eb' }}
- tickFormatter={(value) => {
- // 格式化日期显示,只显示月-日
- if (!value) return '';
- const date = new Date(value);
- if (isNaN(date.getTime())) return value;
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- return `${month}-${day}`;
- }}
- />
- <YAxis
- stroke="#6b7280"
- tick={{ fontSize: 12, fill: '#6b7280' }}
- axisLine={{ stroke: '#e5e7eb' }}
- domain={[0, 5]}
- ticks={[0, 1, 2, 3, 4, 5]}
- />
- <Tooltip
- contentStyle={{
- backgroundColor: '#fff',
- border: '1px solid #e5e7eb',
- borderRadius: '6px',
- fontSize: '12px',
- boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
- }}
- />
- <Legend
- wrapperStyle={{ paddingTop: '20px' }}
- iconType="square"
- align="right"
- verticalAlign="top"
- formatter={(value) => <span style={{ fontSize: '12px', color: '#6b7280' }}>□ {value}</span>}
- />
- <Area
- type="monotone"
- dataKey="completedCount"
- stroke="#3b82f6"
- strokeWidth={2}
- fill="url(#colorCompleted)"
- dot={{ fill: '#3b82f6', r: 4, strokeWidth: 2, stroke: '#fff' }}
- name="每日完成作业数"
- />
- </AreaChart>
- </ResponsiveContainer>
- </div>
- </div>
- {/* 物料管理部分 */}
- <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
- <h3 className="text-lg font-semibold text-gray-900 mb-4">物料管理</h3>
- <div className="flex gap-4">
- {/* 可用物料 - 绿色 */}
- <div className="flex-1 bg-green-50 rounded-lg border border-green-200 p-5 text-center">
- <div className="text-3xl font-bold text-green-900 mb-2">
- {statistics.availableMaterialsCount || 0}
- </div>
- <div className="text-sm text-green-700">可用物料</div>
- </div>
-
- {/* 借用中物料 - 橙色 */}
- <div className="flex-1 bg-orange-50 rounded-lg border border-orange-200 p-5 text-center">
- <div className="text-3xl font-bold text-orange-900 mb-2">
- {statistics.loanMaterialsCount || 0}
- </div>
- <div className="text-sm text-orange-700">借用中物料</div>
- </div>
-
- {/* 异常物料 - 红色 */}
- <div className="flex-1 bg-red-50 rounded-lg border border-red-200 p-5 text-center">
- <div className="text-3xl font-bold text-red-900 mb-2">
- {statistics.exceptionMaterialsCount || 0}
- </div>
- <div className="text-sm text-red-700">异常物料</div>
- </div>
-
- {/* 待归还物料 - 灰色 */}
- <div className="flex-1 bg-gray-50 rounded-lg border border-gray-200 p-5 text-center">
- <div className="text-3xl font-bold text-gray-900 mb-2">
- {statistics.returnMaterialsCount || 0}
- </div>
- <div className="text-sm text-gray-600">待归还物料</div>
- </div>
- </div>
- </div>
- {/* 物料状态列表 */}
- <Card
- title={
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-2">
- <List className="w-5 h-5 text-gray-600" />
- <span>物料状态列表</span>
- </div>
- <button
- className="text-sm font-medium border-0 bg-transparent p-0 cursor-pointer"
- style={{ color: '#2563eb' }}
- onMouseEnter={(e) => e.currentTarget.style.color = '#1d4ed8'}
- onMouseLeave={(e) => e.currentTarget.style.color = '#2563eb'}
- >
- 查看全部物料
- </button>
- </div>
- }
- >
- <div className="overflow-x-auto">
- <table className="w-full border-collapse">
- <thead>
- <tr className="bg-gray-50 border-b border-gray-200">
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">物料编号</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">物料名称</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">物料类型</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">状态</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">借用人员</th>
- <th className="px-4 py-3 text-center text-xs font-medium text-gray-700">操作</th>
- </tr>
- </thead>
- <tbody className="bg-white">
- {/* 示例数据 - 实际应该从API获取 */}
- <tr className="border-b border-gray-100 hover:bg-gray-50">
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">LOCK-001</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">隔离锁具 (通用型)</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">锁具</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm">
- <span className="px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-gray-900">借用中</span>
- </td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">张三</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-center">
- <div className="flex items-center justify-center gap-2">
- <button
- className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
- style={{ backgroundColor: '#1677ff', color: '#ffffff' }}
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#0958d9'}
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#1677ff'}
- >
- 查看
- </button>
- <button
- className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
- style={{ backgroundColor: '#fa8c16', color: '#ffffff' }}
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#d46b08'}
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#fa8c16'}
- >
- 催还
- </button>
- </div>
- </td>
- </tr>
- <tr className="border-b border-gray-100 hover:bg-gray-50">
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">TAG-002</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">警示标签 (大号)</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">标签</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm">
- <span className="px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700">异常</span>
- </td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">-</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-center">
- <div className="flex items-center justify-center gap-2">
- <button
- className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
- style={{ backgroundColor: '#1677ff', color: '#ffffff' }}
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#0958d9'}
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#1677ff'}
- >
- 查看
- </button>
- <button
- className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
- style={{ backgroundColor: '#ff4d4f', color: '#ffffff' }}
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#cf1322'}
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#ff4d4f'}
- >
- 处理异常
- </button>
- </div>
- </td>
- </tr>
- <tr className="border-b border-gray-100 hover:bg-gray-50">
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">DEV-003</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">断电测试仪</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">仪器</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm">
- <span className="px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-gray-900">可用</span>
- </td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">-</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-center">
- <div className="flex items-center justify-center gap-2">
- <button
- className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
- style={{ backgroundColor: '#1677ff', color: '#ffffff' }}
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#0958d9'}
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#1677ff'}
- >
- 查看
- </button>
- <button
- className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
- style={{ backgroundColor: '#52c41a', color: '#ffffff' }}
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#389e0d'}
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#52c41a'}
- >
- 申领
- </button>
- </div>
- </td>
- </tr>
- <tr className="border-b border-gray-100 hover:bg-gray-50">
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">LOCK-004</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">隔离锁具 (防爆型)</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">锁具</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm">
- <span className="px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700">待归还</span>
- </td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">李四</td>
- <td className="px-4 py-3 whitespace-nowrap text-sm text-center">
- <div className="flex items-center justify-center gap-2">
- <button
- className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
- style={{ backgroundColor: '#1677ff', color: '#ffffff' }}
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#0958d9'}
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#1677ff'}
- >
- 查看
- </button>
- <button
- className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
- style={{ backgroundColor: '#fa8c16', color: '#ffffff' }}
- onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#d46b08'}
- onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#fa8c16'}
- >
- 催还
- </button>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- </Card>
- {/* 物料管理快捷操作 */}
- <Card
- title={
- <div className="flex items-center gap-2">
- <Rocket className="w-5 h-5 text-gray-600" />
- <span>物料管理快捷操作</span>
- </div>
- }
- >
- <div className="flex gap-4">
- <button className="flex-1 bg-white border border-gray-200 rounded-lg p-4 hover:bg-gray-50 hover:border-gray-300 transition-all flex flex-col items-center justify-center gap-2">
- <Plus className="w-6 h-6 text-gray-600" />
- <span className="text-sm font-medium text-gray-700">新增物料</span>
- </button>
- <button className="flex-1 bg-white border border-gray-200 rounded-lg p-4 hover:bg-gray-50 hover:border-gray-300 transition-all flex flex-col items-center justify-center gap-2">
- <RefreshCw className="w-6 h-6 text-gray-600" />
- <span className="text-sm font-medium text-gray-700">刷新物料状态</span>
- </button>
- <button className="flex-1 bg-white border border-gray-200 rounded-lg p-4 hover:bg-gray-50 hover:border-gray-300 transition-all flex flex-col items-center justify-center gap-2">
- <AlertCircle className="w-6 h-6 text-gray-600" />
- <span className="text-sm font-medium text-gray-700">处理异常物料</span>
- </button>
- <button className="flex-1 bg-white border border-gray-200 rounded-lg p-4 hover:bg-gray-50 hover:border-gray-300 transition-all flex flex-col items-center justify-center gap-2">
- <FileText className="w-6 h-6 text-gray-600" />
- <span className="text-sm font-medium text-gray-700">物料台账导出</span>
- </button>
- </div>
- </Card>
- {/* 任务详情弹框 */}
- <Modal
- title={null}
- open={taskDetailVisible}
- onCancel={() => {
- setTaskDetailVisible(false);
- setTaskDetailData(null);
- }}
- footer={null}
- width={1000}
- destroyOnClose
- maskClosable={false}
- styles={{
- body: {
- minHeight: '600px',
- maxHeight: '700px',
- height: '700px',
- padding: 0,
- display: 'flex',
- flexDirection: 'column',
- overflow: 'hidden'
- }
- }}
- >
- {taskDetailLoading ? (
- <div className="py-8 text-center text-gray-500">加载中...</div>
- ) : taskDetailData ? (
- <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 }}>
- {taskDetailData.workName || taskDetailData.name || '任务详情'}
- </h2>
- <span
- className="inline-flex px-3 py-1 rounded-full text-xs font-medium"
- style={getTaskStatusStyle(taskDetailData.approvalStatus || taskDetailData.taskStatus || taskDetailData.status)}
- >
- {getTaskStatusText(taskDetailData.approvalStatus || taskDetailData.taskStatus || taskDetailData.status)}
- </span>
- </div>
- <div className="text-sm flex gap-4" style={{ color: '#898f9a', marginTop: '12px' }}>
- <span>工单编号:{taskDetailData.orderNo || '-'}</span>
- <span>作业发起人:{taskDetailData.initiatorName || '-'}</span>
- <span>任务负责人:{taskDetailData.workerUserName || '-'}</span>
- <span>开始时间:{taskDetailData.workTime ? dateFormatter(taskDetailData.workTime) : '-'}</span>
- </div>
- </div>
- {/* 内容区域 - 可滚动 */}
- <div className="mt-6" style={{
- padding: '0 24px',
- flex: 1,
- overflowY: 'auto',
- minHeight: 0
- }}>
- {(() => {
- const isApproved = taskDetailData?.approvalStatus === 'approved';
- const nodeType = String(taskDetailData?.type || taskDetailData?.nodeType || '').trim();
- const isReview = nodeType === 'review';
- const isIsolation = nodeType === 'isolation' || nodeType === '隔离' || nodeType === '隔离/方案';
- const isReleaseIsolation = nodeType === 'releaseIsolation' || nodeType === '解除隔离';
- const isReturnLock = nodeType === 'returnLock';
- // 隔离/方案节点、解除隔离节点和还锁节点
- if (isIsolation || isReleaseIsolation || isReturnLock) {
- return (
- <div
- 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>
- );
- }
- // 审核类型节点 (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={taskDetailForm}
- 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
- 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' }}>
- <CheckCircleOutlined
- style={{
- fontSize: '64px',
- color: '#1890ff',
- display: 'block'
- }}
- />
- </div>
- <div
- style={{
- fontSize: '20px',
- fontWeight: 500,
- color: '#333',
- lineHeight: '1.6'
- }}
- >
- 无需填写表单,直接点击底部按钮提交即可
- </div>
- </Card>
- </div>
- )}
- {/* 审核意见 */}
- <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>
- );
- }
- // 其他节点类型(需要根据 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={taskDetailForm}
- 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
- 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' }}>
- <CheckCircleOutlined
- style={{
- fontSize: '64px',
- color: '#1890ff',
- display: 'block'
- }}
- />
- </div>
- <div
- style={{
- fontSize: '20px',
- fontWeight: 500,
- color: '#333',
- lineHeight: '1.6'
- }}
- >
- 无需填写表单,直接点击底部按钮提交即可
- </div>
- </Card>
- </div>
- )}
- </div>
- {/* 底部按钮 - 其他节点类型不显示按钮(只读模式) */}
- </div>
- );
- })()}
- </div>
- </div>
- ) : (
- <div className="py-8 text-center text-gray-500">暂无数据</div>
- )}
- </Modal>
- </div>
- );
- }
|