Dashboard.tsx 121 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661
  1. import React, { useState, useEffect, useMemo } from 'react';
  2. import { ChevronDown, ChevronUp, Clock, PlayCircle, CheckCircle, AlertTriangle, List, Plus, RefreshCw, AlertCircle, FileText, Rocket } from 'lucide-react';
  3. import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, AreaChart, Area } from 'recharts';
  4. import { Card, Collapse, Modal, Form as AntdForm, Input, Button, message, Select, DatePicker, InputNumber, Switch, Radio, Checkbox, Cascader, Upload, Alert } from 'antd';
  5. import { UploadOutlined, LockOutlined, CheckCircleOutlined } from '@ant-design/icons';
  6. const { Panel } = Collapse;
  7. import { cockpitApi, CockpitStatisticsVO, JobVO, TaskVO } from '../api/cockpit';
  8. import { dictDataApi, DictDataVO } from '../api/DictData';
  9. import { taskManagementApi, MyTaskVO, MyTaskPageParam, MyTaskNodeDetailVO, UpdateNodeApprovalParam } from '../api/mytask';
  10. import { managerHomeApi, ManagerWorkCountVO, ManagerWorkItemVO, ManagerDayWorkCountVO } from '../api/managerHome';
  11. import { dateFormatter } from '../utils/formatTime';
  12. import { toast } from 'sonner';
  13. import { useNavigate } from 'react-router-dom';
  14. import dayjs, { Dayjs } from 'dayjs';
  15. import { setConfAndFields2, FormCreateData } from '../utils/formCreate';
  16. import { useTranslation } from 'react-i18next';
  17. // 辅助函数:安全地将值转换为 dayjs 对象
  18. const safeToDayjs = (value: any): dayjs.Dayjs | null => {
  19. if (!value && value !== 0) {
  20. return null;
  21. }
  22. // 如果是数字(时间戳)
  23. if (typeof value === 'number') {
  24. let timestamp = value;
  25. if (timestamp > 0 && timestamp < 10000000000) {
  26. timestamp = timestamp * 1000;
  27. }
  28. const dayjsValue = dayjs(timestamp);
  29. if (dayjsValue.isValid()) {
  30. return dayjsValue;
  31. } else {
  32. return null;
  33. }
  34. }
  35. // 如果是字符串
  36. if (typeof value === 'string' && value.trim() !== '') {
  37. const numValue = Number(value);
  38. if (!isNaN(numValue) && numValue > 0 && numValue.toString() === value.trim()) {
  39. let timestamp = numValue;
  40. if (timestamp < 10000000000) {
  41. timestamp = timestamp * 1000;
  42. }
  43. const dayjsValue = dayjs(timestamp);
  44. if (dayjsValue.isValid()) {
  45. return dayjsValue;
  46. } else {
  47. return null;
  48. }
  49. } else {
  50. const dayjsValue = dayjs(value);
  51. return dayjsValue.isValid() ? dayjsValue : null;
  52. }
  53. }
  54. // 如果已经是 dayjs 对象
  55. if (value && typeof value === 'object' && typeof value.isValid === 'function') {
  56. try {
  57. return value.isValid() ? value : null;
  58. } catch (e) {
  59. // isValid 调用失败
  60. }
  61. }
  62. // 如果是对象,检查是否有 $d 属性
  63. if (value && typeof value === 'object') {
  64. try {
  65. if (value.$d) {
  66. const dayjsValue = dayjs(value.$d);
  67. return dayjsValue.isValid() ? dayjsValue : null;
  68. }
  69. if (value.valueOf && typeof value.valueOf === 'function') {
  70. const timestamp = value.valueOf();
  71. if (typeof timestamp === 'number') {
  72. const dayjsValue = dayjs(timestamp);
  73. return dayjsValue.isValid() ? dayjsValue : null;
  74. }
  75. }
  76. } catch (e) {
  77. return null;
  78. }
  79. }
  80. return null;
  81. };
  82. // 辅助函数:验证 dayjs 对象是否有效
  83. const isValidDayjs = (value: any): boolean => {
  84. if (!value) return false;
  85. if (typeof value !== 'object') return false;
  86. if (typeof value.isValid !== 'function') return false;
  87. try {
  88. return value.isValid();
  89. } catch (e) {
  90. return false;
  91. }
  92. };
  93. // 辅助函数:将表单值中的日期字符串转换为 dayjs 对象
  94. const convertDateValues = (formValues: any, fields: any[]): any => {
  95. const fieldTypeMap: { [key: string]: string } = {};
  96. // 建立字段名到类型的映射
  97. fields.forEach((field: any) => {
  98. try {
  99. const fieldObj = typeof field === 'string' ? JSON.parse(field) : field;
  100. const fieldName = fieldObj.name || fieldObj.field;
  101. if (fieldName) {
  102. fieldTypeMap[fieldName] = fieldObj.type;
  103. }
  104. } catch (e) {
  105. // 解析失败,跳过
  106. }
  107. });
  108. // 转换日期值
  109. const convertedValues: any = {};
  110. Object.keys(formValues).forEach((fieldName) => {
  111. const fieldType = fieldTypeMap[fieldName];
  112. const value = formValues[fieldName];
  113. if (fieldType === 'date' || fieldType === 'datetime' || fieldType === 'timepicker') {
  114. convertedValues[fieldName] = safeToDayjs(value) || undefined;
  115. } else if (fieldType === 'daterange') {
  116. if (Array.isArray(value) && value.length === 2) {
  117. const [start, end] = value;
  118. const startDayjs = safeToDayjs(start);
  119. const endDayjs = safeToDayjs(end);
  120. if (startDayjs && endDayjs) {
  121. convertedValues[fieldName] = [startDayjs, endDayjs];
  122. } else {
  123. convertedValues[fieldName] = undefined;
  124. }
  125. } else {
  126. convertedValues[fieldName] = undefined;
  127. }
  128. } else {
  129. convertedValues[fieldName] = value;
  130. }
  131. });
  132. return convertedValues;
  133. };
  134. export default function Dashboard() {
  135. const { t } = useTranslation();
  136. const navigate = useNavigate();
  137. const [loading, setLoading] = useState(true);
  138. const [expandedJobs, setExpandedJobs] = useState<Set<number>>(new Set());
  139. const [selectedJob, setSelectedJob] = useState<number | null>(null);
  140. const [jobStatusDictList, setJobStatusDictList] = useState<DictDataVO[]>([]);
  141. const [approvalStatusDictList, setApprovalStatusDictList] = useState<DictDataVO[]>([]);
  142. const [jobTaskMap, setJobTaskMap] = useState<Map<number, MyTaskVO[]>>(new Map());
  143. const [taskLoadingMap, setTaskLoadingMap] = useState<Map<number, boolean>>(new Map());
  144. const [statistics, setStatistics] = useState<CockpitStatisticsVO>({
  145. materialsCount: 0,
  146. loanMaterialsCount: 0,
  147. totalOperations: 0,
  148. totalMaterials: 0,
  149. borrowedMaterials: 0,
  150. workstationCount: 0,
  151. workstationDistribution: [],
  152. operationTypeDistribution: [],
  153. });
  154. // 管理员工作统计数据
  155. const [managerWorkCount, setManagerWorkCount] = useState<ManagerWorkCountVO>({});
  156. const [managerWorkList, setManagerWorkList] = useState<ManagerWorkItemVO[]>([]);
  157. const [managerDayWorkCount, setManagerDayWorkCount] = useState<ManagerDayWorkCountVO[]>([]);
  158. const [managerDataLoading, setManagerDataLoading] = useState(false);
  159. // 任务详情弹框相关状态
  160. const [taskDetailVisible, setTaskDetailVisible] = useState(false);
  161. const [taskDetailLoading, setTaskDetailLoading] = useState(false);
  162. const [taskDetailData, setTaskDetailData] = useState<MyTaskNodeDetailVO | null>(null);
  163. const [formData, setFormData] = useState<FormCreateData>({
  164. rule: [],
  165. option: {}
  166. });
  167. const [formLoading, setFormLoading] = useState(false);
  168. const [originalFields, setOriginalFields] = useState<string[]>([]);
  169. const [originalConf, setOriginalConf] = useState<string>('');
  170. const [taskDetailForm] = AntdForm.useForm();
  171. const [approvalComment, setApprovalComment] = useState('');
  172. const [approvalLoading, setApprovalLoading] = useState(false);
  173. const [submitLoading, setSubmitLoading] = useState(false);
  174. const [showOverdueJobs, setShowOverdueJobs] = useState(false); // 默认隐藏逾期作业
  175. // 获取统计数据
  176. const fetchStatistics = async () => {
  177. setLoading(true);
  178. try {
  179. const response = await cockpitApi.getHomePage();
  180. console.log('API原始响应:', response);
  181. // API返回格式: { code: 0, data: {...}, msg: "" }
  182. const data = (response && typeof response === 'object' && 'data' in response)
  183. ? response.data
  184. : response;
  185. console.log('提取的data:', data);
  186. // 计算总作业数
  187. const totalOperations = data.jobList?.length || 0;
  188. // 计算作业状态统计
  189. const jobList = data.jobList || [];
  190. let pendingCount = 0;
  191. let inProgressCount = 0;
  192. let completedCount = 0;
  193. let overdueCount = 0;
  194. jobList.forEach((job: any) => {
  195. const status = job.status || job.jobStatus || '';
  196. if (status === '待执行' || status === 'pending' || status === 'PENDING') {
  197. pendingCount++;
  198. } else if (status === '进行中' || status === 'in_progress' || status === 'IN_PROGRESS') {
  199. inProgressCount++;
  200. } else if (status === '已完成' || status === 'completed' || status === 'COMPLETED') {
  201. completedCount++;
  202. } else if (status === '逾期' || status === 'overdue' || status === 'OVERDUE') {
  203. overdueCount++;
  204. } else {
  205. // 如果没有明确状态,根据其他字段判断
  206. if (job.progress === 0 || !job.progress) {
  207. pendingCount++;
  208. } else if (job.progress === 100) {
  209. completedCount++;
  210. } else {
  211. inProgressCount++;
  212. }
  213. }
  214. });
  215. // 生成最近30天的趋势数据(如果API没有提供,则生成模拟数据)
  216. const trendData: { date: string; completedCount: number }[] = [];
  217. if (data.jobTrendData && Array.isArray(data.jobTrendData)) {
  218. // 使用API提供的数据
  219. trendData.push(...data.jobTrendData);
  220. } else {
  221. // 生成模拟数据(实际应该从API获取)
  222. for (let i = 1; i <= 30; i++) {
  223. trendData.push({
  224. date: `${i}日`,
  225. completedCount: Math.floor(Math.random() * 5), // 0-4之间的随机数
  226. });
  227. }
  228. }
  229. // 计算工作站数量(去重)
  230. const workstationSet = new Set<string>();
  231. jobList.forEach((job: any) => {
  232. if (job.workstationName) {
  233. workstationSet.add(job.workstationName);
  234. }
  235. });
  236. const workstationCount = workstationSet.size;
  237. // 计算工作站作业分布
  238. const workstationMap = new Map<string, number>();
  239. jobList.forEach((job: any) => {
  240. const workstation = job.workstationName || '未知';
  241. workstationMap.set(workstation, (workstationMap.get(workstation) || 0) + 1);
  242. });
  243. const workstationDistribution = Array.from(workstationMap.entries()).map(([workstation, count]) => ({
  244. workstation,
  245. count,
  246. }));
  247. // 计算作业类型分布
  248. const typeMap = new Map<string, number>();
  249. jobList.forEach((job: any) => {
  250. // 从ticketName中提取作业类型
  251. const ticketName = job.ticketName || '';
  252. let type = '未知';
  253. if (ticketName.includes('换产') || ticketName.includes('投产')) {
  254. type = '投产';
  255. } else if (ticketName.includes('维修')) {
  256. type = '维修';
  257. } else if (ticketName.includes('PM')) {
  258. type = 'PM';
  259. } else if (job.jobType) {
  260. type = job.jobType;
  261. }
  262. typeMap.set(type, (typeMap.get(type) || 0) + 1);
  263. });
  264. const typeEntries = Array.from(typeMap.entries());
  265. const totalJobs = typeEntries.reduce((sum, [, count]) => sum + count, 0);
  266. const operationTypeDistribution = typeEntries.map(([type, count]) => ({
  267. type,
  268. count,
  269. percentage: totalJobs > 0 ? Math.round((count / totalJobs) * 100) : 0,
  270. }));
  271. // 判断物料状态
  272. const materialStatus = data.exceptionMaterialsCount && data.exceptionMaterialsCount > 0 ? '异常' : '正常';
  273. // 判断借用状态
  274. const borrowedStatus = data.loanMaterialsCount && data.loanMaterialsCount > 0 ? '关注' : '正常';
  275. // 判断工作站状态
  276. const workstationStatus = workstationCount > 0 ? '活跃' : '非活跃';
  277. // 组装统计数据
  278. const statisticsData: CockpitStatisticsVO = {
  279. ...data,
  280. totalOperations,
  281. totalMaterials: data.materialsCount || 0,
  282. materialStatus,
  283. borrowedMaterials: data.loanMaterialsCount || 0,
  284. borrowedStatus,
  285. workstationCount,
  286. workstationStatus,
  287. workstationDistribution,
  288. operationTypeDistribution,
  289. // 作业状态统计
  290. pendingJobsCount: pendingCount,
  291. inProgressJobsCount: inProgressCount,
  292. completedJobsCount: completedCount,
  293. overdueJobsCount: overdueCount,
  294. // 趋势数据
  295. jobTrendData: trendData,
  296. // 物料统计
  297. availableMaterialsCount: data.availableMaterialsCount || (data.materialsCount || 0) - (data.loanMaterialsCount || 0) - (data.exceptionMaterialsCount || 0) - (data.returnMaterialsCount || 0),
  298. returnMaterialsCount: data.returnMaterialsCount || 0,
  299. };
  300. console.log('处理后的统计数据:', statisticsData);
  301. setStatistics(statisticsData);
  302. } catch (error: any) {
  303. console.error('获取驾驶舱统计数据失败:', error);
  304. toast.error(error.message || '获取统计数据失败');
  305. } finally {
  306. setLoading(false);
  307. }
  308. };
  309. // 获取作业状态字典数据
  310. const getJobStatusDictList = async () => {
  311. try {
  312. const response = await dictDataApi.getDictDataPage({
  313. pageNo: 1,
  314. pageSize: -1,
  315. dictType: 'job_status',
  316. });
  317. const data = (response && typeof response === 'object' && 'data' in response)
  318. ? response.data
  319. : response;
  320. setJobStatusDictList(data?.list || []);
  321. } catch (error: any) {
  322. console.error('获取作业状态字典失败:', error);
  323. setJobStatusDictList([]);
  324. }
  325. };
  326. // 获取审批状态字典数据
  327. const getApprovalStatusDictList = async () => {
  328. try {
  329. const response = await dictDataApi.getDictDataPage({
  330. pageNo: 1,
  331. pageSize: -1,
  332. dictType: 'approval_status',
  333. });
  334. const data = (response && typeof response === 'object' && 'data' in response)
  335. ? response.data
  336. : response;
  337. setApprovalStatusDictList(data?.list || []);
  338. console.log('Dashboard: 获取审批状态字典成功', data?.list || []);
  339. } catch (error: any) {
  340. console.error('获取审批状态字典失败:', error);
  341. setApprovalStatusDictList([]);
  342. }
  343. };
  344. // 获取管理员工作统计数据
  345. const fetchManagerWorkCount = async () => {
  346. try {
  347. const response = await managerHomeApi.getManagerWorkCount();
  348. const data = (response && typeof response === 'object' && 'data' in response)
  349. ? response.data
  350. : response;
  351. setManagerWorkCount(data || {});
  352. } catch (error: any) {
  353. console.error('获取管理员工作统计失败:', error);
  354. toast.error(error.message || '获取管理员工作统计失败');
  355. setManagerWorkCount({});
  356. }
  357. };
  358. // 获取管理员工作列表
  359. const fetchManagerWorkList = async () => {
  360. try {
  361. const response = await managerHomeApi.getManagerWorkList({
  362. pageNo: 1,
  363. pageSize: -1,
  364. });
  365. console.log('获取管理员工作列表原始响应:', response);
  366. // 接口返回的已经是数组格式(API 层已经处理了 {code, data, msg} 格式)
  367. const data: ManagerWorkItemVO[] = Array.isArray(response) ? response : [];
  368. console.log('设置管理员工作列表:', data);
  369. setManagerWorkList(data);
  370. console.log('获取管理员工作列表成功,设置的数据:', data, '数据长度:', data.length);
  371. } catch (error: any) {
  372. console.error('获取管理员工作列表失败:', error);
  373. toast.error(error.message || '获取管理员工作列表失败');
  374. setManagerWorkList([]);
  375. }
  376. };
  377. // 获取管理员每日工作统计
  378. const fetchManagerDayWorkCount = async () => {
  379. try {
  380. // 计算最近30天的日期范围
  381. const endDate = new Date();
  382. const startDate = new Date();
  383. startDate.setDate(startDate.getDate() - 30);
  384. const response = await managerHomeApi.getManagerDayWorkCount({
  385. startDate: startDate.toISOString().split('T')[0],
  386. endDate: endDate.toISOString().split('T')[0],
  387. });
  388. // API 层已经处理了 {code, data, msg} 格式,返回的已经是数组
  389. const data: ManagerDayWorkCountVO[] = Array.isArray(response) ? response : [];
  390. console.log('获取管理员每日工作统计:', data);
  391. setManagerDayWorkCount(data);
  392. } catch (error: any) {
  393. console.error('获取管理员每日工作统计失败:', error);
  394. toast.error(error.message || '获取管理员每日工作统计失败');
  395. setManagerDayWorkCount([]);
  396. }
  397. };
  398. // 获取所有管理员数据
  399. const fetchManagerData = async () => {
  400. setManagerDataLoading(true);
  401. try {
  402. await Promise.all([
  403. fetchManagerWorkCount(),
  404. fetchManagerWorkList(),
  405. fetchManagerDayWorkCount(),
  406. ]);
  407. } catch (error: any) {
  408. console.error('获取管理员数据失败:', error);
  409. } finally {
  410. setManagerDataLoading(false);
  411. }
  412. };
  413. useEffect(() => {
  414. fetchStatistics();
  415. getJobStatusDictList();
  416. getApprovalStatusDictList();
  417. fetchManagerData();
  418. }, []);
  419. // 获取作业的任务列表
  420. // 从作业的 workflowWorkNodeDOList 中提取任务列表并转换为 MyTaskVO 格式
  421. const extractTasksFromJob = (job: ManagerWorkItemVO): MyTaskVO[] => {
  422. if (!job.workflowWorkNodeDOList || !Array.isArray(job.workflowWorkNodeDOList)) {
  423. return [];
  424. }
  425. return job.workflowWorkNodeDOList.map((node: any) => {
  426. return {
  427. id: node.id,
  428. nodeId: node.id, // 节点ID作为任务ID
  429. workId: job.id, // 作业ID
  430. name: node.nodeName || node.currentNodeName || node.currentNode || '-',
  431. currentNodeName: node.nodeName || node.currentNodeName || node.currentNode || '-',
  432. currentNode: node.currentNodeName || node.currentNode || '-',
  433. workerUserName: node.workerUserName || node.responsibleName || node.responsible || '-',
  434. responsibleName: node.workerUserName || node.responsibleName || node.responsible || '-',
  435. responsible: node.workerUserName || node.responsibleName || node.responsible || '-',
  436. workTime: node.workTime || node.taskStartTime || node.initiationTime,
  437. taskStartTime: node.workTime || node.taskStartTime || node.initiationTime,
  438. initiationTime: node.workTime || node.taskStartTime || node.initiationTime,
  439. approvalStatus: node.approvalStatus || node.status,
  440. taskStatus: node.approvalStatus || node.status,
  441. status: node.approvalStatus || node.status,
  442. orderNo: job.orderNo,
  443. initiatorName: job.initiatorName || job.initiator || '-',
  444. initiator: job.initiatorName || job.initiator || '-',
  445. } as MyTaskVO;
  446. });
  447. };
  448. // 计算作业的整体进度(已完成任务数 / 总任务数)
  449. const calculateJobProgress = (job: ManagerWorkItemVO): number => {
  450. if (!job.workflowWorkNodeDOList || !Array.isArray(job.workflowWorkNodeDOList)) {
  451. return 0;
  452. }
  453. const totalTasks = job.workflowWorkNodeDOList.length;
  454. if (totalTasks === 0) {
  455. return 0;
  456. }
  457. // 统计已完成的任务数量
  458. const completedTasks = job.workflowWorkNodeDOList.filter((node: any) => {
  459. const status = String(node.approvalStatus || node.status || '').toLowerCase();
  460. // 判断任务是否已完成:状态为 'approved', 'completed', '已完成' 等
  461. return status === 'approved' ||
  462. status === 'completed' ||
  463. status === '已完成' ||
  464. status === '完成' ||
  465. status === 'COMPLETED';
  466. }).length;
  467. // 计算百分比并四舍五入
  468. const progress = Math.round((completedTasks / totalTasks) * 100);
  469. return progress;
  470. };
  471. // 切换作业展开/折叠
  472. const toggleJobExpand = (jobId: number) => {
  473. console.log('Dashboard: toggleJobExpand 被调用', { jobId, currentExpanded: Array.from(expandedJobs) });
  474. const isCurrentlyExpanded = expandedJobs.has(jobId);
  475. if (isCurrentlyExpanded) {
  476. // 如果当前作业已展开,则折叠它
  477. setExpandedJobs(new Set());
  478. setSelectedJob(null);
  479. console.log('Dashboard: 折叠作业', jobId);
  480. } else {
  481. // 展开新作业时,先清空所有已展开的作业,确保每次只展开一个
  482. const newExpanded = new Set<number>();
  483. newExpanded.add(jobId);
  484. setExpandedJobs(newExpanded);
  485. setSelectedJob(jobId); // 展开时选中
  486. console.log('Dashboard: 展开作业', jobId);
  487. // 从作业数据中直接提取任务列表
  488. const job = managerWorkList.find(j => j.id === jobId);
  489. console.log('Dashboard: 查找作业', { jobId, job, jobListLength: managerWorkList.length });
  490. if (job) {
  491. console.log('Dashboard: 从作业数据中提取任务列表', { jobId, workflowWorkNodeDOList: job.workflowWorkNodeDOList });
  492. const taskList = extractTasksFromJob(job);
  493. console.log('Dashboard: 提取的任务列表', taskList, '任务数量:', taskList.length);
  494. setJobTaskMap(prev => new Map(prev).set(jobId, taskList));
  495. // 设置加载状态为完成(因为数据已经在作业数据中了,不需要异步加载)
  496. setTaskLoadingMap(prev => {
  497. const newMap = new Map(prev);
  498. newMap.delete(jobId);
  499. return newMap;
  500. });
  501. } else {
  502. console.warn('Dashboard: 未找到作业', jobId);
  503. setJobTaskMap(prev => new Map(prev).set(jobId, []));
  504. }
  505. }
  506. };
  507. // 选中作业
  508. const handleJobSelect = (jobId: number) => {
  509. setSelectedJob(jobId);
  510. };
  511. // 处理作业行点击(选中并切换展开/折叠)
  512. const handleJobRowClick = (jobId: number) => {
  513. setSelectedJob(jobId);
  514. toggleJobExpand(jobId);
  515. };
  516. const defaultFormConfig = {
  517. name: '',
  518. labelPosition: 'right',
  519. formSize: 'middle',
  520. labelSuffix: '',
  521. labelWidth: 100,
  522. hideRequiredMark: false,
  523. showValidationError: true,
  524. inlineValidation: false,
  525. showSubmitButton: false,
  526. showResetButton: false,
  527. };
  528. // 渲染字段预览(支持嵌套结构)
  529. const renderFieldPreview = (field: any, parentSpanStyle?: React.CSSProperties): React.ReactNode => {
  530. const formConfig = formData.option?.formConfig || defaultFormConfig;
  531. const layoutColumns = formConfig.layoutColumns || 1;
  532. const spanStyle = parentSpanStyle || (layoutColumns > 1 ? { gridColumn: `span ${Math.min(layoutColumns, field.span || 1)}` } : undefined);
  533. // 处理容器类型(card 和 grid)
  534. if (field.type === 'card') {
  535. const children = field.children || [];
  536. const cardTitle = field.label || field.cardTitle || '卡片容器';
  537. return (
  538. <div key={field.id} style={spanStyle} className="mb-4">
  539. <Card title={cardTitle} className="w-full">
  540. <div className="space-y-4">
  541. {children.map((child: any) => renderFieldPreview(child))}
  542. </div>
  543. </Card>
  544. </div>
  545. );
  546. }
  547. if (field.type === 'grid') {
  548. const gridColumns = field.gridColumns || 2;
  549. const children = field.children || [];
  550. return (
  551. <div key={field.id} style={spanStyle} className="mb-4">
  552. <div
  553. style={{
  554. display: 'grid',
  555. gridTemplateColumns: `repeat(${gridColumns}, minmax(0, 1fr))`,
  556. gap: '16px',
  557. }}
  558. >
  559. {children.map((child: any) => {
  560. const childSpanStyle = gridColumns > 1
  561. ? { gridColumn: `span ${Math.min(gridColumns, child.span || 1)}` }
  562. : undefined;
  563. return (
  564. <div key={child.id} style={childSpanStyle}>
  565. {renderFieldPreview(child)}
  566. </div>
  567. );
  568. })}
  569. </div>
  570. </div>
  571. );
  572. }
  573. // 处理 alert 类型
  574. if (field.type === 'alert') {
  575. const themeClass =
  576. field.alertTheme === 'dark'
  577. ? 'bg-gray-800 text-white border-gray-700'
  578. : '';
  579. return (
  580. <div key={field.id} className="mb-4" style={spanStyle}>
  581. <Alert
  582. message={field.alertTitle || field.label}
  583. description={field.alertDescription}
  584. type={field.alertType || 'info'}
  585. showIcon={field.alertShowIcon !== false}
  586. closable={field.alertClosable}
  587. closeText={field.alertCloseText}
  588. className={`${field.alertCentered ? 'text-center' : ''} ${themeClass}`}
  589. banner
  590. style={field.style}
  591. />
  592. {field.hint && <div className="text-xs text-gray-400 mt-1">{field.hint}</div>}
  593. </div>
  594. );
  595. }
  596. // 处理普通字段
  597. switch (field.type) {
  598. case 'textarea':
  599. return (
  600. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  601. <AntdForm.Item
  602. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  603. name={field.name || field.field}
  604. required={field.required && !formConfig.hideRequiredMark}
  605. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  606. help={field.hint}
  607. >
  608. <Input.TextArea
  609. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  610. rows={4}
  611. allowClear={field.showClear}
  612. maxLength={field.maxLength}
  613. readOnly={field.readOnly}
  614. disabled={field.disabled}
  615. size={field.size || 'middle'}
  616. />
  617. </AntdForm.Item>
  618. </div>
  619. );
  620. case 'password':
  621. return (
  622. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  623. <AntdForm.Item
  624. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  625. name={field.name || field.field}
  626. required={field.required && !formConfig.hideRequiredMark}
  627. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  628. help={field.hint}
  629. >
  630. <Input.Password
  631. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  632. allowClear={field.showClear}
  633. maxLength={field.maxLength}
  634. readOnly={field.readOnly}
  635. disabled={field.disabled}
  636. size={field.size || 'middle'}
  637. />
  638. </AntdForm.Item>
  639. </div>
  640. );
  641. case 'number':
  642. return (
  643. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  644. <AntdForm.Item
  645. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  646. name={field.name || field.field}
  647. required={field.required && !formConfig.hideRequiredMark}
  648. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  649. help={field.hint}
  650. >
  651. <InputNumber
  652. style={{ width: '100%' }}
  653. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  654. max={field.maxLength}
  655. readOnly={field.readOnly}
  656. disabled={field.disabled}
  657. size={field.size || 'middle'}
  658. />
  659. </AntdForm.Item>
  660. </div>
  661. );
  662. case 'select':
  663. return (
  664. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  665. <AntdForm.Item
  666. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  667. name={field.name || field.field}
  668. required={field.required && !formConfig.hideRequiredMark}
  669. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  670. help={field.hint}
  671. >
  672. <Select
  673. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择'}
  674. allowClear={field.showClear}
  675. disabled={field.disabled}
  676. size={field.size || 'middle'}
  677. >
  678. {(field.options || []).map((opt: any, idx: number) => (
  679. <Select.Option key={idx} value={opt.value}>{opt.label}</Select.Option>
  680. ))}
  681. </Select>
  682. </AntdForm.Item>
  683. </div>
  684. );
  685. case 'date':
  686. return (
  687. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  688. <AntdForm.Item
  689. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  690. name={field.name || field.field}
  691. required={field.required && !formConfig.hideRequiredMark}
  692. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期' }] : []}
  693. help={field.hint}
  694. getValueFromEvent={(value) => {
  695. if (!value) return undefined;
  696. if (value && typeof value === 'object' && 'valueOf' in value && typeof value.valueOf === 'function' && 'isValid' in value && typeof value.isValid === 'function') {
  697. if (value.isValid()) {
  698. return value.valueOf();
  699. }
  700. return undefined;
  701. }
  702. return value;
  703. }}
  704. normalize={(value) => {
  705. if (!value) return undefined;
  706. if (typeof value === 'number') {
  707. const dayjsValue = dayjs(value);
  708. return dayjsValue.isValid() ? dayjsValue : undefined;
  709. }
  710. if (typeof value === 'string') {
  711. const numValue = Number(value);
  712. if (!isNaN(numValue) && numValue > 0) {
  713. const dayjsValue = dayjs(numValue);
  714. return dayjsValue.isValid() ? dayjsValue : undefined;
  715. }
  716. const dayjsValue = dayjs(value);
  717. return dayjsValue.isValid() ? dayjsValue : undefined;
  718. }
  719. if (value && typeof value === 'object' && 'isValid' in value && typeof value.isValid === 'function') {
  720. return value.isValid() ? value : undefined;
  721. }
  722. return undefined;
  723. }}
  724. >
  725. <DatePicker
  726. style={{ width: '100%' }}
  727. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期'}
  728. allowClear={field.showClear}
  729. disabled={field.disabled}
  730. size={field.size || 'middle'}
  731. />
  732. </AntdForm.Item>
  733. </div>
  734. );
  735. case 'daterange':
  736. return (
  737. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  738. <AntdForm.Item
  739. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  740. name={field.name || field.field}
  741. required={field.required && !formConfig.hideRequiredMark}
  742. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期范围' }] : []}
  743. help={field.hint}
  744. getValueFromEvent={(value) => {
  745. if (!value || !Array.isArray(value) || value.length !== 2) return undefined;
  746. const [start, end] = value;
  747. if (start && typeof start === 'object' && 'valueOf' in start && typeof start.valueOf === 'function' && 'isValid' in start && typeof start.isValid === 'function' &&
  748. end && typeof end === 'object' && 'valueOf' in end && typeof end.valueOf === 'function' && 'isValid' in end && typeof end.isValid === 'function') {
  749. if (start.isValid() && end.isValid()) {
  750. return [start.valueOf(), end.valueOf()];
  751. }
  752. return undefined;
  753. }
  754. return value;
  755. }}
  756. normalize={(value) => {
  757. if (!value) return undefined;
  758. if (Array.isArray(value) && value.length === 2) {
  759. const [start, end] = value;
  760. let startDayjs: any = null;
  761. let endDayjs: any = null;
  762. if (typeof start === 'number') {
  763. startDayjs = dayjs(start);
  764. startDayjs = startDayjs.isValid() ? startDayjs : null;
  765. } else if (typeof start === 'string') {
  766. const numValue = Number(start);
  767. if (!isNaN(numValue) && numValue > 0) {
  768. startDayjs = dayjs(numValue);
  769. startDayjs = startDayjs.isValid() ? startDayjs : null;
  770. } else {
  771. startDayjs = dayjs(start);
  772. startDayjs = startDayjs.isValid() ? startDayjs : null;
  773. }
  774. } else if (start && typeof start === 'object' && 'isValid' in start && typeof start.isValid === 'function') {
  775. startDayjs = start.isValid() ? start : null;
  776. }
  777. if (typeof end === 'number') {
  778. endDayjs = dayjs(end);
  779. endDayjs = endDayjs.isValid() ? endDayjs : null;
  780. } else if (typeof end === 'string') {
  781. const numValue = Number(end);
  782. if (!isNaN(numValue) && numValue > 0) {
  783. endDayjs = dayjs(numValue);
  784. endDayjs = endDayjs.isValid() ? endDayjs : null;
  785. } else {
  786. endDayjs = dayjs(end);
  787. endDayjs = endDayjs.isValid() ? endDayjs : null;
  788. }
  789. } else if (end && typeof end === 'object' && 'isValid' in end && typeof end.isValid === 'function') {
  790. endDayjs = end.isValid() ? end : null;
  791. }
  792. if (startDayjs && endDayjs) {
  793. return [startDayjs, endDayjs];
  794. }
  795. }
  796. return undefined;
  797. }}
  798. >
  799. <DatePicker.RangePicker
  800. style={{ width: '100%' }}
  801. placeholder={Array.isArray(field.placeholder) ? field.placeholder as [string, string] : ['开始日期', '结束日期']}
  802. allowClear={field.showClear}
  803. disabled={field.disabled}
  804. size={field.size || 'middle'}
  805. />
  806. </AntdForm.Item>
  807. </div>
  808. );
  809. case 'datetime':
  810. return (
  811. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  812. <AntdForm.Item
  813. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  814. name={field.name || field.field}
  815. required={field.required && !formConfig.hideRequiredMark}
  816. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期时间' }] : []}
  817. help={field.hint}
  818. getValueFromEvent={(value) => {
  819. if (!value) return undefined;
  820. if (value && typeof value === 'object' && 'valueOf' in value) {
  821. return value.valueOf();
  822. }
  823. return value;
  824. }}
  825. normalize={(value) => {
  826. if (!value) return undefined;
  827. if (typeof value === 'number') {
  828. const dayjsValue = dayjs(value);
  829. return dayjsValue.isValid() ? dayjsValue : undefined;
  830. }
  831. if (typeof value === 'string') {
  832. const numValue = Number(value);
  833. if (!isNaN(numValue) && numValue > 0) {
  834. const dayjsValue = dayjs(numValue);
  835. return dayjsValue.isValid() ? dayjsValue : undefined;
  836. }
  837. const dayjsValue = dayjs(value);
  838. return dayjsValue.isValid() ? dayjsValue : undefined;
  839. }
  840. if (value && typeof value === 'object' && 'isValid' in value && typeof value.isValid === 'function') {
  841. return value.isValid() ? value : undefined;
  842. }
  843. return undefined;
  844. }}
  845. >
  846. <DatePicker
  847. style={{ width: '100%' }}
  848. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期时间'}
  849. allowClear={field.showClear}
  850. showTime={{ format: 'HH:mm:ss' }}
  851. format="YYYY-MM-DD HH:mm:ss"
  852. disabled={field.disabled}
  853. size={field.size || 'middle'}
  854. />
  855. </AntdForm.Item>
  856. </div>
  857. );
  858. case 'switch':
  859. return (
  860. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  861. <AntdForm.Item
  862. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  863. name={field.name || field.field}
  864. required={field.required && !formConfig.hideRequiredMark}
  865. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  866. help={field.hint}
  867. valuePropName="checked"
  868. >
  869. <Switch disabled={field.disabled} />
  870. </AntdForm.Item>
  871. </div>
  872. );
  873. case 'radio':
  874. return (
  875. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  876. <AntdForm.Item
  877. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  878. name={field.name || field.field}
  879. required={field.required && !formConfig.hideRequiredMark}
  880. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  881. help={field.hint}
  882. >
  883. <Radio.Group disabled={field.disabled}>
  884. {(field.options || []).map((opt: any, idx: number) => (
  885. <Radio key={idx} value={opt.value}>{opt.label}</Radio>
  886. ))}
  887. </Radio.Group>
  888. </AntdForm.Item>
  889. </div>
  890. );
  891. case 'checkbox':
  892. return (
  893. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  894. <AntdForm.Item
  895. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  896. name={field.name || field.field}
  897. required={field.required && !formConfig.hideRequiredMark}
  898. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  899. help={field.hint}
  900. >
  901. <Checkbox.Group disabled={field.disabled}>
  902. {(field.options || []).map((opt: any, idx: number) => (
  903. <Checkbox key={idx} value={opt.value}>{opt.label}</Checkbox>
  904. ))}
  905. </Checkbox.Group>
  906. </AntdForm.Item>
  907. </div>
  908. );
  909. case 'cascader':
  910. return (
  911. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  912. <AntdForm.Item
  913. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  914. name={field.name || field.field}
  915. required={field.required && !formConfig.hideRequiredMark}
  916. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  917. help={field.hint}
  918. >
  919. <Cascader
  920. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择'}
  921. options={field.cascaderOptions || []}
  922. className="w-full"
  923. allowClear={field.showClear}
  924. disabled={field.disabled}
  925. size={field.size || 'middle'}
  926. />
  927. </AntdForm.Item>
  928. </div>
  929. );
  930. case 'upload':
  931. return (
  932. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  933. <AntdForm.Item
  934. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  935. name={field.name || field.field}
  936. required={field.required && !formConfig.hideRequiredMark}
  937. rules={field.required ? [{ required: true, message: field.requiredMessage || '请上传' }] : []}
  938. help={field.hint}
  939. >
  940. <Upload
  941. disabled={field.disabled}
  942. maxCount={field.uploadType === 'single-image' ? 1 : field.maxCount || (field.uploadType === 'multiple-image' ? 9 : undefined)}
  943. accept={field.accept || (field.uploadType === 'single-image' || field.uploadType === 'multiple-image' ? 'image/*' : undefined)}
  944. listType={(field.uploadType === 'file' ? 'text' : 'picture-card') as 'text' | 'picture-card' | 'picture'}
  945. multiple={field.uploadType !== 'single-image'}
  946. >
  947. {field.uploadType === 'file' ? (
  948. <Button icon={<UploadOutlined />}>上传文件</Button>
  949. ) : (
  950. <div>
  951. <UploadOutlined />
  952. <div style={{ marginTop: 8 }}>上传</div>
  953. </div>
  954. )}
  955. </Upload>
  956. </AntdForm.Item>
  957. </div>
  958. );
  959. case 'time':
  960. case 'timepicker':
  961. return (
  962. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  963. <AntdForm.Item
  964. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  965. name={field.name || field.field}
  966. required={field.required && !formConfig.hideRequiredMark}
  967. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择时间' }] : []}
  968. help={field.hint}
  969. >
  970. <DatePicker
  971. style={{ width: '100%' }}
  972. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择时间'}
  973. allowClear={field.showClear}
  974. picker="time"
  975. format="HH:mm:ss"
  976. disabled={field.disabled}
  977. size={field.size || 'middle'}
  978. />
  979. </AntdForm.Item>
  980. </div>
  981. );
  982. default:
  983. // 检查 inputType 是否为时间类型
  984. const inputType = field.inputType || field.type;
  985. if (inputType === 'date' || inputType === 'datetime' || inputType === 'time' || inputType === 'timepicker') {
  986. if (inputType === 'datetime') {
  987. return (
  988. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  989. <AntdForm.Item
  990. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  991. name={field.name || field.field}
  992. required={field.required && !formConfig.hideRequiredMark}
  993. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期时间' }] : []}
  994. help={field.hint}
  995. >
  996. <DatePicker
  997. style={{ width: '100%' }}
  998. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期时间'}
  999. allowClear={field.showClear}
  1000. showTime={{ format: 'HH:mm:ss' }}
  1001. format="YYYY-MM-DD HH:mm:ss"
  1002. disabled={field.disabled}
  1003. size={field.size || 'middle'}
  1004. />
  1005. </AntdForm.Item>
  1006. </div>
  1007. );
  1008. } else if (inputType === 'time' || inputType === 'timepicker') {
  1009. return (
  1010. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  1011. <AntdForm.Item
  1012. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  1013. name={field.name || field.field}
  1014. required={field.required && !formConfig.hideRequiredMark}
  1015. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择时间' }] : []}
  1016. help={field.hint}
  1017. >
  1018. <DatePicker
  1019. style={{ width: '100%' }}
  1020. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择时间'}
  1021. allowClear={field.showClear}
  1022. picker="time"
  1023. format="HH:mm:ss"
  1024. disabled={field.disabled}
  1025. size={field.size || 'middle'}
  1026. />
  1027. </AntdForm.Item>
  1028. </div>
  1029. );
  1030. } else {
  1031. return (
  1032. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  1033. <AntdForm.Item
  1034. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  1035. name={field.name || field.field}
  1036. required={field.required && !formConfig.hideRequiredMark}
  1037. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期' }] : []}
  1038. help={field.hint}
  1039. >
  1040. <DatePicker
  1041. style={{ width: '100%' }}
  1042. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期'}
  1043. allowClear={field.showClear}
  1044. disabled={field.disabled}
  1045. size={field.size || 'middle'}
  1046. />
  1047. </AntdForm.Item>
  1048. </div>
  1049. );
  1050. }
  1051. }
  1052. // 其他类型使用 Input
  1053. return (
  1054. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  1055. <AntdForm.Item
  1056. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  1057. name={field.name || field.field}
  1058. required={field.required && !formConfig.hideRequiredMark}
  1059. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  1060. help={field.hint}
  1061. >
  1062. <Input
  1063. type={field.inputType || 'text'}
  1064. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  1065. allowClear={field.showClear}
  1066. maxLength={field.maxLength}
  1067. readOnly={field.readOnly}
  1068. disabled={field.disabled}
  1069. size={field.size || 'middle'}
  1070. />
  1071. </AntdForm.Item>
  1072. </div>
  1073. );
  1074. }
  1075. };
  1076. // 查看任务详情(完整复刻任务管理页面的逻辑)
  1077. const handleViewTaskDetail = async (task: MyTaskVO) => {
  1078. console.log('Dashboard: handleViewTaskDetail 被调用', task);
  1079. if (!task.nodeId) {
  1080. console.warn('Dashboard: 节点ID不存在', task);
  1081. message.warning('节点ID不存在');
  1082. return;
  1083. }
  1084. setTaskDetailLoading(true);
  1085. setTaskDetailVisible(true);
  1086. console.log('Dashboard: 弹框状态已设置为 true (加载中)');
  1087. try {
  1088. console.log('Dashboard: 开始获取节点详情', { nodeId: task.nodeId });
  1089. const response = await taskManagementApi.getAdminWorkNodeDetail(task.nodeId);
  1090. console.log('Dashboard: 节点详情响应', response);
  1091. let data: any = response;
  1092. // 直接从第一层 data.formId 获取 formId
  1093. let extractedFormId: number | undefined = undefined;
  1094. if (data?.formId !== undefined && data?.formId !== null) {
  1095. const formIdValue = data.formId;
  1096. if (typeof formIdValue === 'string') {
  1097. const parsed = parseInt(formIdValue, 10);
  1098. if (!isNaN(parsed)) {
  1099. extractedFormId = parsed;
  1100. console.log('Dashboard: ✅ 从 data.formId (字符串) 提取到 formId', extractedFormId);
  1101. }
  1102. } else if (typeof formIdValue === 'number') {
  1103. extractedFormId = formIdValue;
  1104. console.log('Dashboard: ✅ 从 data.formId (数字) 提取到 formId', extractedFormId);
  1105. }
  1106. }
  1107. if (extractedFormId === undefined) {
  1108. console.warn('Dashboard: ⚠️ 未能提取到 formId', {
  1109. 'data.formId': data?.formId,
  1110. 'data': data
  1111. });
  1112. }
  1113. // 合并列表数据中的作业信息
  1114. const detailDataWithWorkInfo: MyTaskNodeDetailVO = {
  1115. ...(data || {}),
  1116. workName: task.name || data?.workName || data?.name,
  1117. orderNo: task.orderNo || data?.orderNo,
  1118. workerUserName: task.workerUserName || data?.workerUserName,
  1119. initiatorName: task.initiatorName || data?.initiatorName || data?.initiator || task.initiator || '',
  1120. workTime: task.workTime || data?.workTime,
  1121. type: data?.type || data?.nodeType || '',
  1122. nodeType: data?.nodeType || data?.type || '',
  1123. formId: extractedFormId,
  1124. };
  1125. console.log('Dashboard: 合并后的详情数据', detailDataWithWorkInfo);
  1126. // 先设置数据,确保弹框有内容显示
  1127. setTaskDetailData(detailDataWithWorkInfo);
  1128. setTaskDetailVisible(true);
  1129. setTaskDetailLoading(false);
  1130. // 根据节点类型决定是否需要获取表单
  1131. const nodeType = detailDataWithWorkInfo.type || detailDataWithWorkInfo.nodeType || '';
  1132. const isIsolation = nodeType === 'isolation' || nodeType === '隔离' || nodeType === '隔离/方案';
  1133. const isReleaseIsolation = nodeType === 'releaseIsolation' || nodeType === '解除隔离';
  1134. const isReturnLock = nodeType === 'returnLock';
  1135. const isApproved = detailDataWithWorkInfo.approvalStatus === 'approved';
  1136. const formId = detailDataWithWorkInfo.formId;
  1137. const hasFormId = formId !== undefined && formId !== null && (
  1138. typeof formId === 'number' ||
  1139. (typeof formId === 'string' && formId !== '' && formId.trim() !== '')
  1140. );
  1141. const hasFormData = detailDataWithWorkInfo.formData && (
  1142. (typeof detailDataWithWorkInfo.formData === 'string' && detailDataWithWorkInfo.formData.trim() !== '') ||
  1143. (typeof detailDataWithWorkInfo.formData === 'object' && Object.keys(detailDataWithWorkInfo.formData).length > 0)
  1144. );
  1145. // 如果任务状态为"已通过"且有 formData,则从 formData 中解析表单结构
  1146. if (isApproved && hasFormData) {
  1147. console.log('Dashboard: ✅ 任务已通过,从 formData 中解析表单结构');
  1148. setFormLoading(true);
  1149. try {
  1150. let parsedFormData: any;
  1151. if (typeof detailDataWithWorkInfo.formData === 'string') {
  1152. parsedFormData = JSON.parse(detailDataWithWorkInfo.formData);
  1153. } else {
  1154. parsedFormData = detailDataWithWorkInfo.formData;
  1155. }
  1156. const conf = parsedFormData.conf;
  1157. const fields = parsedFormData.fields;
  1158. if (conf && fields) {
  1159. const confString = typeof conf === 'string' ? conf : JSON.stringify(conf);
  1160. const fieldsArray = Array.isArray(fields) ? fields : [];
  1161. const fieldsStringArray = fieldsArray.map((field: any) => {
  1162. return typeof field === 'string' ? field : JSON.stringify(field);
  1163. });
  1164. setOriginalConf(confString);
  1165. setOriginalFields(fieldsStringArray);
  1166. // 解析字段值用于回显
  1167. const formValues: any = {};
  1168. const fieldTypeMap: { [key: string]: string } = {};
  1169. fieldsStringArray.forEach((fieldString: string) => {
  1170. try {
  1171. const fieldObj = typeof fieldString === 'string' ? JSON.parse(fieldString) : fieldString;
  1172. const fieldName = fieldObj.name || fieldObj.field;
  1173. if (fieldName) {
  1174. fieldTypeMap[fieldName] = fieldObj.type;
  1175. }
  1176. } catch (e) {
  1177. console.error('Dashboard: 解析字段 JSON 失败', e, fieldString);
  1178. }
  1179. });
  1180. fieldsStringArray.forEach((fieldString: string) => {
  1181. try {
  1182. const fieldObj = typeof fieldString === 'string' ? JSON.parse(fieldString) : fieldString;
  1183. const fieldName = fieldObj.name || fieldObj.field;
  1184. if (fieldName && fieldObj.value !== undefined && fieldObj.value !== null) {
  1185. const fieldType = fieldTypeMap[fieldName];
  1186. let value = fieldObj.value;
  1187. if (fieldType === 'date' || fieldType === 'datetime') {
  1188. if (typeof value === 'string' && value.trim() !== '') {
  1189. const dayjsValue = dayjs(value);
  1190. formValues[fieldName] = dayjsValue.isValid() ? dayjsValue : undefined;
  1191. } else {
  1192. formValues[fieldName] = undefined;
  1193. }
  1194. } else if (fieldType === 'daterange') {
  1195. if (Array.isArray(value) && value.length === 2) {
  1196. const [start, end] = value;
  1197. const startDayjs = typeof start === 'string' && start ? dayjs(start) : null;
  1198. const endDayjs = typeof end === 'string' && end ? dayjs(end) : null;
  1199. if (startDayjs && startDayjs.isValid() && endDayjs && endDayjs.isValid()) {
  1200. formValues[fieldName] = [startDayjs, endDayjs];
  1201. } else {
  1202. formValues[fieldName] = undefined;
  1203. }
  1204. } else {
  1205. formValues[fieldName] = undefined;
  1206. }
  1207. } else {
  1208. formValues[fieldName] = value;
  1209. }
  1210. }
  1211. } catch (e) {
  1212. console.error('Dashboard: 解析字段 JSON 失败', e, fieldString);
  1213. }
  1214. });
  1215. setConfAndFields2(setFormData, conf, fields);
  1216. const convertedFormValues = convertDateValues(formValues, fieldsStringArray);
  1217. setTimeout(() => {
  1218. taskDetailForm.setFieldsValue(convertedFormValues);
  1219. console.log('Dashboard: 表单数据已回填(从 formData)', convertedFormValues);
  1220. }, 100);
  1221. } else {
  1222. console.warn('Dashboard: formData 中缺少 conf 或 fields', { conf: !!conf, fields: !!fields });
  1223. message.warning('表单数据不完整');
  1224. }
  1225. } catch (e) {
  1226. console.error('Dashboard: 解析 formData 失败', e);
  1227. message.error('解析表单数据失败: ' + (e instanceof Error ? e.message : String(e)));
  1228. } finally {
  1229. setFormLoading(false);
  1230. }
  1231. }
  1232. // 如果不是隔离类型,且有表单ID,且不是从 formData 获取,则从接口获取表单配置
  1233. else if (!isIsolation && !isReleaseIsolation && !isReturnLock && hasFormId) {
  1234. console.log('Dashboard: ✅ 满足条件,开始获取表单', { formId, nodeType });
  1235. setFormData({ rule: [], option: {} });
  1236. setOriginalFields([]);
  1237. setOriginalConf('');
  1238. setFormLoading(true);
  1239. (async () => {
  1240. try {
  1241. const numericFormId = typeof formId === 'string' ? parseInt(formId, 10) : formId;
  1242. if (isNaN(numericFormId)) {
  1243. console.error('Dashboard: formId 不是有效数字', formId);
  1244. message.error('表单ID无效');
  1245. setFormLoading(false);
  1246. return;
  1247. }
  1248. console.log('Dashboard: 🚀 开始调用表单接口', { formId: numericFormId, nodeType });
  1249. const FormApi = await import('../api/bpm/form');
  1250. const formDetailResponse = await FormApi.getForm(numericFormId);
  1251. console.log('Dashboard: ✅ 表单接口调用成功', formDetailResponse);
  1252. let formDetail: any = formDetailResponse;
  1253. if (formDetailResponse && typeof formDetailResponse === 'object' && 'data' in formDetailResponse) {
  1254. formDetail = (formDetailResponse as any).data || formDetailResponse;
  1255. }
  1256. const conf = formDetail?.conf || formDetail?.formConfig;
  1257. const fields = formDetail?.fields || formDetail?.formFields;
  1258. if (conf && fields) {
  1259. const confString = typeof conf === 'string' ? conf : JSON.stringify(conf);
  1260. const fieldsArray = Array.isArray(fields) ? fields : [];
  1261. const fieldsStringArray = fieldsArray.map((field: any) => {
  1262. return typeof field === 'string' ? field : JSON.stringify(field);
  1263. });
  1264. setOriginalConf(confString);
  1265. setOriginalFields(fieldsStringArray);
  1266. setConfAndFields2(setFormData, conf, fields);
  1267. // 如果有表单数据值,回填到表单
  1268. if (detailDataWithWorkInfo.formData) {
  1269. try {
  1270. let formValues = typeof detailDataWithWorkInfo.formData === 'string'
  1271. ? JSON.parse(detailDataWithWorkInfo.formData)
  1272. : detailDataWithWorkInfo.formData;
  1273. formValues = convertDateValues(formValues, fieldsArray);
  1274. const cleanedFormValues: any = {};
  1275. Object.keys(formValues).forEach(key => {
  1276. const value = formValues[key];
  1277. const field = fieldsArray.find((f: any) => {
  1278. const fieldObj = typeof f === 'string' ? JSON.parse(f) : f;
  1279. return (fieldObj.name || fieldObj.field) === key;
  1280. });
  1281. if (field) {
  1282. const fieldObj = typeof field === 'string' ? JSON.parse(field) : field;
  1283. const fieldType = fieldObj.type;
  1284. if (fieldType === 'date' || fieldType === 'datetime' || fieldType === 'timepicker') {
  1285. if (isValidDayjs(value)) {
  1286. cleanedFormValues[key] = value;
  1287. } else if (value !== null && value !== undefined) {
  1288. const dayjsValue = safeToDayjs(value);
  1289. cleanedFormValues[key] = dayjsValue || undefined;
  1290. } else {
  1291. cleanedFormValues[key] = undefined;
  1292. }
  1293. } else if (fieldType === 'daterange') {
  1294. if (Array.isArray(value) && value.length === 2) {
  1295. const [start, end] = value;
  1296. let startDayjs: dayjs.Dayjs | null = null;
  1297. if (isValidDayjs(start)) {
  1298. startDayjs = start;
  1299. } else {
  1300. startDayjs = safeToDayjs(start);
  1301. }
  1302. let endDayjs: dayjs.Dayjs | null = null;
  1303. if (isValidDayjs(end)) {
  1304. endDayjs = end;
  1305. } else {
  1306. endDayjs = safeToDayjs(end);
  1307. }
  1308. cleanedFormValues[key] = (startDayjs && endDayjs) ? [startDayjs, endDayjs] : undefined;
  1309. } else {
  1310. cleanedFormValues[key] = undefined;
  1311. }
  1312. } else {
  1313. cleanedFormValues[key] = value;
  1314. }
  1315. } else {
  1316. cleanedFormValues[key] = value;
  1317. }
  1318. });
  1319. setTimeout(() => {
  1320. try {
  1321. taskDetailForm.setFieldsValue(cleanedFormValues);
  1322. console.log('Dashboard: 表单数据已回填', cleanedFormValues);
  1323. } catch (e) {
  1324. console.error('Dashboard: 设置表单值失败', e);
  1325. }
  1326. }, 100);
  1327. } catch (e) {
  1328. console.error('Dashboard: 解析表单数据失败', e);
  1329. }
  1330. }
  1331. } else {
  1332. console.warn('Dashboard: 表单详情缺少配置或字段', { conf: !!conf, fields: !!fields });
  1333. message.warning('表单配置不完整');
  1334. }
  1335. } catch (e) {
  1336. console.error('Dashboard: 获取表单详情失败', e);
  1337. message.error('获取表单详情失败: ' + (e instanceof Error ? e.message : String(e)));
  1338. } finally {
  1339. setFormLoading(false);
  1340. }
  1341. })();
  1342. } else {
  1343. setFormLoading(false);
  1344. }
  1345. // 回显审核意见
  1346. if (detailDataWithWorkInfo.approvalOpinion && detailDataWithWorkInfo.approvalOpinion !== 'pending') {
  1347. setApprovalComment(detailDataWithWorkInfo.approvalOpinion);
  1348. } else {
  1349. setApprovalComment('');
  1350. }
  1351. console.log('Dashboard: 弹框已打开', { detailVisible: true, detailData: detailDataWithWorkInfo });
  1352. } catch (error: any) {
  1353. console.error('Dashboard: 获取节点详情失败', error);
  1354. toast.error(error.message || '获取节点详情失败');
  1355. if (!taskDetailData) {
  1356. setTaskDetailData({
  1357. id: task.nodeId,
  1358. nodeId: task.nodeId,
  1359. workId: task.workId,
  1360. workName: task.name,
  1361. orderNo: task.orderNo,
  1362. workerUserName: task.workerUserName,
  1363. workTime: task.workTime,
  1364. type: '',
  1365. } as MyTaskNodeDetailVO);
  1366. }
  1367. setTaskDetailVisible(true);
  1368. } finally {
  1369. setTaskDetailLoading(false);
  1370. }
  1371. };
  1372. // 获取任务状态文本(使用字典)
  1373. const getTaskStatusText = (status: string | number | undefined): string => {
  1374. if (!status) return '未知';
  1375. const statusStr = String(status).toLowerCase();
  1376. const statusItem = approvalStatusDictList.find(item => String(item.value).toLowerCase() === statusStr);
  1377. if (statusItem) {
  1378. return statusItem.label || '未知';
  1379. }
  1380. // 如果没有找到字典值,使用默认映射
  1381. const statusMap: Record<string, string> = {
  1382. 'pending': '待审核',
  1383. 'approved': '已通过',
  1384. 'rejected': '已驳回',
  1385. 'unaudited': '未审核',
  1386. };
  1387. return statusMap[statusStr] || '未知';
  1388. };
  1389. // 获取任务状态样式(审批状态)
  1390. const getTaskStatusStyle = (status: string | number | undefined): React.CSSProperties => {
  1391. if (!status) {
  1392. return {
  1393. backgroundColor: '#e5e5e5',
  1394. color: '#333333',
  1395. };
  1396. }
  1397. const statusStr = String(status).toLowerCase();
  1398. // 先获取状态文本,根据文本判断颜色
  1399. const statusText = getTaskStatusText(status);
  1400. const statusTextLower = statusText.toLowerCase();
  1401. // 根据状态文本判断颜色
  1402. // 待执行、待发布:灰色 #e5e5e5
  1403. if (statusTextLower.includes('待执行') || statusTextLower.includes('待发布') ||
  1404. statusTextLower.includes('未开始') || statusTextLower.includes('未审核') ||
  1405. statusTextLower.includes('待开始') || statusTextLower.includes('unaudited')) {
  1406. return {
  1407. backgroundColor: '#e5e5e5',
  1408. color: '#333333',
  1409. };
  1410. }
  1411. // 进行中:蓝色 #1677ff
  1412. if (statusTextLower.includes('进行中') || statusTextLower.includes('待审核') ||
  1413. statusTextLower.includes('pending') || statusTextLower.includes('执行中')) {
  1414. return {
  1415. backgroundColor: '#1677ff',
  1416. color: '#ffffff',
  1417. };
  1418. }
  1419. // 已完成:绿色 #0acb57
  1420. if (statusTextLower.includes('已完成') || statusTextLower.includes('已通过') ||
  1421. statusTextLower.includes('approved') || statusTextLower.includes('执行完成')) {
  1422. return {
  1423. backgroundColor: '#0acb57',
  1424. color: '#ffffff',
  1425. };
  1426. }
  1427. // 已退回、已驳回:橙色 #ff9500
  1428. if (statusTextLower.includes('已退回') || statusTextLower.includes('已驳回') ||
  1429. statusTextLower.includes('rejected')) {
  1430. return {
  1431. backgroundColor: '#ff9500',
  1432. color: '#ffffff',
  1433. };
  1434. }
  1435. // 默认样式
  1436. return {
  1437. backgroundColor: '#e5e5e5',
  1438. color: '#333333',
  1439. };
  1440. };
  1441. // 获取任务状态文本和样式(兼容函数)
  1442. const getTaskStatusInfo = (status: string | number | undefined) => {
  1443. return {
  1444. label: getTaskStatusText(status),
  1445. style: getTaskStatusStyle(status),
  1446. };
  1447. };
  1448. // 获取作业状态标签样式
  1449. const getStatusBadge = (status: string) => {
  1450. const statusMap: Record<string, { label: string; className: string }> = {
  1451. '待执行': { label: '待执行', className: 'bg-gray-100 text-gray-700 border-gray-200' },
  1452. 'pending': { label: '待执行', className: 'bg-gray-100 text-gray-700 border-gray-200' },
  1453. 'PENDING': { label: '待执行', className: 'bg-gray-100 text-gray-700 border-gray-200' },
  1454. '进行中': { label: '进行中', className: 'bg-blue-100 text-blue-700 border-blue-200' },
  1455. 'in_progress': { label: '进行中', className: 'bg-blue-100 text-blue-700 border-blue-200' },
  1456. 'IN_PROGRESS': { label: '进行中', className: 'bg-blue-100 text-blue-700 border-blue-200' },
  1457. '已完成': { label: '已完成', className: 'bg-green-100 text-green-700 border-green-200' },
  1458. 'completed': { label: '已完成', className: 'bg-green-100 text-green-700 border-green-200' },
  1459. 'COMPLETED': { label: '已完成', className: 'bg-green-100 text-green-700 border-green-200' },
  1460. '逾期': { label: '逾期', className: 'bg-red-100 text-red-700 border-red-200' },
  1461. 'overdue': { label: '逾期', className: 'bg-red-100 text-red-700 border-red-200' },
  1462. 'OVERDUE': { label: '逾期', className: 'bg-red-100 text-red-700 border-red-200' },
  1463. };
  1464. return statusMap[status] || { label: status || '未知', className: 'bg-gray-100 text-gray-700 border-gray-200' };
  1465. };
  1466. // 从ticketName中提取作业类型
  1467. const getJobType = (ticketName: string): string => {
  1468. if (ticketName.includes('换产') || ticketName.includes('投产')) {
  1469. return '投产';
  1470. } else if (ticketName.includes('维修')) {
  1471. return '维修';
  1472. } else if (ticketName.includes('PM')) {
  1473. return 'PM';
  1474. }
  1475. return '未知';
  1476. };
  1477. // 获取作业状态显示文本和样式(从字典获取)
  1478. const getJobStatusInfo = (status: string | number | undefined) => {
  1479. if (!status) {
  1480. return { label: '未知', className: 'bg-gray-100 text-gray-700 border-gray-200' };
  1481. }
  1482. // 从字典中查找状态文本
  1483. const statusItem = jobStatusDictList.find(item => String(item.value) === String(status));
  1484. const statusText = statusItem ? (statusItem.label || '') : String(status || '未知');
  1485. const statusTextLower = statusText.toLowerCase();
  1486. // 根据状态文本判断样式
  1487. // 待执行、待发布:灰色
  1488. if (statusTextLower.includes('待执行') || statusTextLower.includes('待发布') ||
  1489. statusTextLower.includes('pending') || statusTextLower.includes('unreleased')) {
  1490. return { label: statusText, className: 'bg-gray-100 text-gray-700 border-gray-200' };
  1491. }
  1492. // 进行中、执行中:蓝色
  1493. if (statusTextLower.includes('进行中') || statusTextLower.includes('执行中') ||
  1494. statusTextLower.includes('running') || statusTextLower.includes('in_progress')) {
  1495. return { label: statusText, className: 'bg-blue-100 text-blue-700 border-blue-200' };
  1496. }
  1497. // 已完成、执行完成:绿色
  1498. if (statusTextLower.includes('已完成') || statusTextLower.includes('执行完成') ||
  1499. statusTextLower.includes('completed') || statusTextLower.includes('完成')) {
  1500. return { label: statusText, className: 'bg-green-100 text-green-700 border-green-200' };
  1501. }
  1502. // 已退回:橙色
  1503. if (statusTextLower.includes('已退回') || statusTextLower.includes('rejected')) {
  1504. return { label: statusText, className: 'bg-orange-100 text-orange-700 border-orange-200' };
  1505. }
  1506. // 已跳过:灰色
  1507. if (statusTextLower.includes('已跳过') || statusTextLower.includes('skipped')) {
  1508. return { label: statusText, className: 'bg-gray-100 text-gray-700 border-gray-200' };
  1509. }
  1510. // 如果没有匹配到,使用字典中的文本,默认灰色样式
  1511. return { label: statusText, className: 'bg-gray-100 text-gray-700 border-gray-200' };
  1512. };
  1513. if (loading) {
  1514. return (
  1515. <div className="flex items-center justify-center h-96">
  1516. <div className="text-gray-500">加载中...</div>
  1517. </div>
  1518. );
  1519. }
  1520. return (
  1521. <div className="p-6 space-y-6 bg-gray-50 min-h-screen">
  1522. <style>{`
  1523. .custom-collapse .ant-collapse-item {
  1524. border: none !important;
  1525. border-bottom: 1px solid #e5e7eb !important;
  1526. }
  1527. .custom-collapse .ant-collapse-item:last-child {
  1528. border-bottom: none !important;
  1529. }
  1530. .custom-collapse .ant-collapse-item .ant-collapse-header {
  1531. padding: 0 !important;
  1532. display: flex !important;
  1533. flex-direction: row !important;
  1534. align-items: center !important;
  1535. position: relative !important;
  1536. }
  1537. .custom-collapse .ant-collapse-item .ant-collapse-header .ant-collapse-arrow {
  1538. display: none !important;
  1539. }
  1540. .custom-collapse .ant-collapse-content {
  1541. border-top: 1px solid #e5e7eb !important;
  1542. }
  1543. `}</style>
  1544. {/* 作业管理部分 */}
  1545. <div>
  1546. {/* 作业状态统计卡片 */}
  1547. <div className="flex mb-6 gap-8">
  1548. {/* 待发布作业 - 灰色主题 */}
  1549. <div
  1550. 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"
  1551. onClick={() => {
  1552. // 保存status到 sessionStorage
  1553. sessionStorage.setItem('workManagementStatus', 'unreleased');
  1554. // 触发自定义事件切换菜单
  1555. window.dispatchEvent(new CustomEvent('switchToMenu', {
  1556. detail: {
  1557. menu: 'isolationWork',
  1558. subMenu: 'workManagement'
  1559. }
  1560. }));
  1561. }}
  1562. >
  1563. <div className="flex items-start justify-between mb-3">
  1564. <div className="text-sm text-gray-500 font-medium">待发布作业</div>
  1565. <div className="p-3 rounded-xl flex items-center justify-center flex-shrink-0" style={{ backgroundColor: 'rgba(188, 185, 183, 0.2)' }}>
  1566. <Clock className="w-6 h-6 text-black-700" strokeWidth={2} />
  1567. </div>
  1568. </div>
  1569. <div>
  1570. <div className="text-2xl font-bold text-gray-900 leading-none">
  1571. {managerWorkCount.unreleasedCount ?? statistics.pendingJobsCount ?? 0} <span className="text-base font-normal text-gray-500">项</span>
  1572. </div>
  1573. </div>
  1574. </div>
  1575. {/* 进行中作业 - 蓝色主题 */}
  1576. <div
  1577. 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"
  1578. onClick={() => {
  1579. // 保存status到 sessionStorage
  1580. sessionStorage.setItem('workManagementStatus', 'running');
  1581. // 触发自定义事件切换菜单
  1582. window.dispatchEvent(new CustomEvent('switchToMenu', {
  1583. detail: {
  1584. menu: 'isolationWork',
  1585. subMenu: 'workManagement'
  1586. }
  1587. }));
  1588. }}
  1589. >
  1590. <div className="flex items-start justify-between mb-3">
  1591. <div className="text-sm text-gray-500 font-medium">进行中作业</div>
  1592. <div className="p-3 rounded-xl flex items-center justify-center flex-shrink-0" style={{ backgroundColor: 'rgba(59, 130, 246, 0.2)' }}>
  1593. <PlayCircle className="w-6 h-6 text-blue-700" strokeWidth={2} />
  1594. </div>
  1595. </div>
  1596. <div>
  1597. <div className="text-2xl font-bold text-gray-900 leading-none">
  1598. {managerWorkCount.runningCount ?? statistics.inProgressJobsCount ?? 0} <span className="text-base font-normal text-gray-500">项</span>
  1599. </div>
  1600. </div>
  1601. </div>
  1602. {/* 已完成作业 - 绿色主题 */}
  1603. <div
  1604. 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"
  1605. onClick={() => {
  1606. // 保存status到 sessionStorage
  1607. sessionStorage.setItem('workManagementStatus', 'completed');
  1608. // 触发自定义事件切换菜单
  1609. window.dispatchEvent(new CustomEvent('switchToMenu', {
  1610. detail: {
  1611. menu: 'isolationWork',
  1612. subMenu: 'workManagement'
  1613. }
  1614. }));
  1615. }}
  1616. >
  1617. <div className="flex items-start justify-between mb-3">
  1618. <div className="text-sm text-gray-500 font-medium">已完成作业</div>
  1619. <div className="p-3 rounded-xl flex items-center justify-center flex-shrink-0" style={{ backgroundColor: 'rgba(34, 197, 94, 0.2)' }}>
  1620. <CheckCircle className="w-6 h-6 text-green-700" strokeWidth={2} />
  1621. </div>
  1622. </div>
  1623. <div>
  1624. <div className="text-2xl font-bold text-gray-900 leading-none">
  1625. {managerWorkCount.completedCount ?? statistics.completedJobsCount ?? 0} <span className="text-base font-normal text-gray-500">项</span>
  1626. </div>
  1627. </div>
  1628. </div>
  1629. {/* 逾期作业 - 红色主题 */}
  1630. {showOverdueJobs && (
  1631. <div className="flex-1 bg-white rounded-lg border border-red-200 p-4 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
  1632. <div className="flex items-start justify-between mb-3">
  1633. <div className="text-sm text-gray-500 font-medium">逾期作业</div>
  1634. <div className="p-3 rounded-xl flex items-center justify-center flex-shrink-0" style={{ backgroundColor: 'rgba(239, 68, 68, 0.2)' }}>
  1635. <AlertTriangle className="w-6 h-6 text-red-700" strokeWidth={2} />
  1636. </div>
  1637. </div>
  1638. <div className="flex items-center justify-between">
  1639. <div>
  1640. <div className="text-2xl font-bold text-gray-900 leading-none">
  1641. {statistics.overdueJobsCount ?? 0} <span className="text-base font-normal text-gray-500">项</span>
  1642. </div>
  1643. </div>
  1644. <button
  1645. onClick={() => setShowOverdueJobs(false)}
  1646. className="text-red-600 text-sm font-medium hover:text-red-700 transition-colors"
  1647. >
  1648. 隐藏
  1649. </button>
  1650. </div>
  1651. </div>
  1652. )}
  1653. </div>
  1654. {/* 作业列表 */}
  1655. <Card
  1656. title={
  1657. <div className="flex items-center justify-between">
  1658. <span>作业列表 (含任务明细)</span>
  1659. <button
  1660. onClick={() => {
  1661. // 通过 sessionStorage 传递参数,让 Dashboard 自动切换到作业管理菜单
  1662. sessionStorage.setItem('navigateToMenu', JSON.stringify({
  1663. menu: 'isolationWork',
  1664. subMenu: 'workManagement'
  1665. }));
  1666. // 如果当前已经在 dashboard 页面,触发自定义事件来切换菜单
  1667. if (window.location.pathname === '/dashboard') {
  1668. // 触发自定义事件,通知 Dashboard 切换菜单
  1669. window.dispatchEvent(new CustomEvent('switchToMenu', {
  1670. detail: { menu: 'isolationWork', subMenu: 'workManagement' }
  1671. }));
  1672. } else {
  1673. // 如果不在 dashboard 页面,导航过去
  1674. navigate('/dashboard');
  1675. }
  1676. }}
  1677. className="text-sm font-medium border-0 bg-transparent p-0 cursor-pointer"
  1678. style={{ color: '#2563eb' }}
  1679. onMouseEnter={(e) => e.currentTarget.style.color = '#1d4ed8'}
  1680. onMouseLeave={(e) => e.currentTarget.style.color = '#2563eb'}
  1681. >
  1682. 查看全部作业
  1683. </button>
  1684. </div>
  1685. }
  1686. >
  1687. <div className="border border-gray-200 rounded-lg overflow-hidden">
  1688. {managerDataLoading ? (
  1689. <div className="p-8 text-center text-gray-500">加载中...</div>
  1690. ) : managerWorkList.length > 0 ? (
  1691. <Collapse
  1692. activeKey={Array.from(expandedJobs).map(id => String(id))}
  1693. onChange={(keys) => {
  1694. const keyArray = Array.isArray(keys) ? keys : [keys];
  1695. if (keyArray.length === 0) {
  1696. // 全部折叠
  1697. setExpandedJobs(new Set());
  1698. setSelectedJob(null);
  1699. } else {
  1700. // 只展开一个
  1701. const jobId = Number(keyArray[keyArray.length - 1]);
  1702. const newExpanded = new Set<number>();
  1703. newExpanded.add(jobId);
  1704. setExpandedJobs(newExpanded);
  1705. setSelectedJob(jobId);
  1706. // 从作业数据中直接提取任务列表
  1707. const job = managerWorkList.find(j => j.id === jobId);
  1708. if (job) {
  1709. console.log('Dashboard: 从作业数据中提取任务列表', { jobId, workflowWorkNodeDOList: job.workflowWorkNodeDOList });
  1710. const taskList = extractTasksFromJob(job);
  1711. console.log('Dashboard: 提取的任务列表', taskList, '任务数量:', taskList.length);
  1712. setJobTaskMap(prev => new Map(prev).set(jobId, taskList));
  1713. // 设置加载状态为完成(因为数据已经在作业数据中了,不需要异步加载)
  1714. setTaskLoadingMap(prev => {
  1715. const newMap = new Map(prev);
  1716. newMap.delete(jobId);
  1717. return newMap;
  1718. });
  1719. } else {
  1720. console.warn('Dashboard: 未找到作业', jobId);
  1721. setJobTaskMap(prev => new Map(prev).set(jobId, []));
  1722. }
  1723. }
  1724. }}
  1725. ghost
  1726. style={{ background: 'transparent' }}
  1727. className="custom-collapse"
  1728. >
  1729. {managerWorkList.map((job: ManagerWorkItemVO, index: number) => {
  1730. const isExpanded = expandedJobs.has(job.id!);
  1731. const isSelected = selectedJob === job.id;
  1732. const statusInfo = getTaskStatusInfo(job.status);
  1733. const jobName = job.name || job.orderNo || job.code || `作业#${job.id}`;
  1734. const responsiblePerson = job.initiatorName || job.initiator || '未分配';
  1735. return (
  1736. <Panel
  1737. key={String(job.id)}
  1738. showArrow={false}
  1739. header={
  1740. <div
  1741. className="p-3 flex items-center relative w-full"
  1742. style={isSelected ? { backgroundColor: '#eff6ff' } : { backgroundColor: 'white' }}
  1743. onClick={(e) => {
  1744. e.stopPropagation();
  1745. handleJobRowClick(job.id!);
  1746. }}
  1747. >
  1748. {/* 作业名称 - 固定宽度100px,超出显示省略号 */}
  1749. <div className="flex-shrink-0 min-w-0" style={{ width: '300px' }}>
  1750. <span className={`text-base truncate block ${
  1751. isSelected ? '' : 'text-gray-900'
  1752. }`} style={isSelected ? { color: '#2563eb' } : { color: '#111827' }}>
  1753. {jobName}
  1754. </span>
  1755. </div>
  1756. {/* 状态标签 - 固定宽度 */}
  1757. <div className="flex-shrink-0 ml-6" style={{ width: '80px',marginRight:'50px' }}>
  1758. <span className="inline-flex px-2 py-0.5 rounded text-xs font-medium whitespace-nowrap" style={statusInfo.style}>
  1759. {statusInfo.label}
  1760. </span>
  1761. </div>
  1762. {/* 作业发起人 - 固定宽度 */}
  1763. <div className="flex-shrink-0 ml-6" style={{ width: '160px' }}>
  1764. <span className={`text-sm truncate block ${
  1765. isSelected ? '' : 'text-gray-900'
  1766. }`} style={isSelected ? { color: '#2563eb' } : { color: '#111827' }}>
  1767. <span>作业发起人:</span>{responsiblePerson}
  1768. </span>
  1769. </div>
  1770. {/* 整体进度 - 固定宽度和位置,右对齐 */}
  1771. <div className="flex-shrink-0" style={{ width: '128px', textAlign: 'right' }}>
  1772. <span className={`text-sm whitespace-nowrap ${
  1773. isSelected ? '' : 'text-gray-900'
  1774. }`} style={isSelected ? { color: '#2563eb' } : { color: '#111827' }}>
  1775. <span>整体进度:</span>{calculateJobProgress(job)}%
  1776. </span>
  1777. </div>
  1778. {/* 右侧弹性空间 */}
  1779. <div className="flex-1"></div>
  1780. {/* 自定义箭头 - 最右侧 */}
  1781. <div className="flex-shrink-0 ml-4" style={{ width: '24px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
  1782. {isExpanded ? (
  1783. <ChevronUp className="w-5 h-5 text-gray-600" />
  1784. ) : (
  1785. <ChevronDown className="w-5 h-5 text-gray-600" />
  1786. )}
  1787. </div>
  1788. </div>
  1789. }
  1790. className="custom-panel"
  1791. >
  1792. <div className="px-3 pb-3 pt-3 bg-white">
  1793. <div className="overflow-x-auto">
  1794. {taskLoadingMap.get(job.id!) ? (
  1795. <div className="px-4 py-8 text-center text-sm text-gray-500">加载中...</div>
  1796. ) : (
  1797. <table className="w-full border-collapse">
  1798. <thead>
  1799. <tr className="bg-gray-50 border-b border-gray-200">
  1800. <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">任务名称</th>
  1801. <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">任务负责人</th>
  1802. <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">开始时间</th>
  1803. <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">状态</th>
  1804. <th className="px-4 py-3 text-center text-xs font-medium text-gray-700">操作</th>
  1805. </tr>
  1806. </thead>
  1807. <tbody className="bg-white">
  1808. {(() => {
  1809. const tasks = jobTaskMap.get(job.id!);
  1810. console.log('Dashboard: 渲染任务列表', { jobId: job.id, tasks, tasksLength: tasks?.length });
  1811. if (tasks && tasks.length > 0) {
  1812. return tasks.map((task: MyTaskVO, taskIndex: number) => {
  1813. const taskStatus = task.approvalStatus || task.taskStatus || task.status;
  1814. const statusInfo = getTaskStatusInfo(taskStatus);
  1815. const taskName = task.currentNodeName || task.currentNode || '-';
  1816. const responsiblePerson = task.workerUserName || task.responsibleName || task.responsible || '-';
  1817. const startTime = task.workTime || task.taskStartTime || task.initiationTime;
  1818. return (
  1819. <tr key={task.id || taskIndex} className="border-b border-gray-100 hover:bg-gray-50">
  1820. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
  1821. {taskName}
  1822. </td>
  1823. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">
  1824. {responsiblePerson}
  1825. </td>
  1826. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">
  1827. {startTime ? dateFormatter(startTime) : '-'}
  1828. </td>
  1829. <td className="px-4 py-3 whitespace-nowrap text-sm">
  1830. <span className="inline-flex px-2 py-0.5 rounded text-xs font-medium" style={statusInfo.style}>
  1831. {statusInfo.label}
  1832. </span>
  1833. </td>
  1834. <td className="px-4 py-3 whitespace-nowrap text-sm text-center">
  1835. <button
  1836. className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
  1837. style={{ backgroundColor: '#1677ff', color: '#ffffff' }}
  1838. onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#0958d9'}
  1839. onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#1677ff'}
  1840. onClick={() => {
  1841. handleViewTaskDetail(task);
  1842. }}
  1843. >
  1844. 查看
  1845. </button>
  1846. </td>
  1847. </tr>
  1848. );
  1849. });
  1850. } else {
  1851. return (
  1852. <tr>
  1853. <td colSpan={5} className="px-4 py-8 text-center text-sm text-gray-500">
  1854. 暂无任务明细
  1855. </td>
  1856. </tr>
  1857. );
  1858. }
  1859. })()}
  1860. </tbody>
  1861. </table>
  1862. )}
  1863. </div>
  1864. </div>
  1865. </Panel>
  1866. );
  1867. })}
  1868. </Collapse>
  1869. ) : (
  1870. <div className="p-8 text-center text-gray-500">暂无作业数据</div>
  1871. )}
  1872. </div>
  1873. </Card>
  1874. </div>
  1875. {/* 全系统作业完成趋势图表 */}
  1876. <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
  1877. <h3 className="text-lg font-semibold text-gray-900 mb-6">
  1878. 全系统作业完成趋势 (最近30天)
  1879. </h3>
  1880. <div className="h-80">
  1881. <ResponsiveContainer width="100%" height="100%">
  1882. <AreaChart
  1883. data={managerDayWorkCount.length > 0
  1884. ? managerDayWorkCount.map(item => ({
  1885. date: item.day || item.date || '',
  1886. completedCount: item.completedCount || item.count || 0
  1887. }))
  1888. : (statistics.jobTrendData || [])
  1889. }
  1890. margin={{ top: 10, right: 30, left: 20, bottom: 20 }}
  1891. >
  1892. <defs>
  1893. <linearGradient id="colorCompleted" x1="0" y1="0" x2="0" y2="1">
  1894. <stop offset="5%" stopColor="#3b82f6" stopOpacity={0.8}/>
  1895. <stop offset="95%" stopColor="#3b82f6" stopOpacity={0.1}/>
  1896. </linearGradient>
  1897. </defs>
  1898. <CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
  1899. <XAxis
  1900. dataKey="date"
  1901. stroke="#6b7280"
  1902. tick={{ fontSize: 12, fill: '#6b7280' }}
  1903. axisLine={{ stroke: '#e5e7eb' }}
  1904. tickFormatter={(value) => {
  1905. // 格式化日期显示,只显示月-日
  1906. if (!value) return '';
  1907. const date = new Date(value);
  1908. if (isNaN(date.getTime())) return value;
  1909. const month = String(date.getMonth() + 1).padStart(2, '0');
  1910. const day = String(date.getDate()).padStart(2, '0');
  1911. return `${month}-${day}`;
  1912. }}
  1913. />
  1914. <YAxis
  1915. stroke="#6b7280"
  1916. tick={{ fontSize: 12, fill: '#6b7280' }}
  1917. axisLine={{ stroke: '#e5e7eb' }}
  1918. domain={[0, 5]}
  1919. ticks={[0, 1, 2, 3, 4, 5]}
  1920. />
  1921. <Tooltip
  1922. contentStyle={{
  1923. backgroundColor: '#fff',
  1924. border: '1px solid #e5e7eb',
  1925. borderRadius: '6px',
  1926. fontSize: '12px',
  1927. boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
  1928. }}
  1929. />
  1930. <Legend
  1931. wrapperStyle={{ paddingTop: '20px' }}
  1932. iconType="square"
  1933. align="right"
  1934. verticalAlign="top"
  1935. formatter={(value) => <span style={{ fontSize: '12px', color: '#6b7280' }}>□ {value}</span>}
  1936. />
  1937. <Area
  1938. type="monotone"
  1939. dataKey="completedCount"
  1940. stroke="#3b82f6"
  1941. strokeWidth={2}
  1942. fill="url(#colorCompleted)"
  1943. dot={{ fill: '#3b82f6', r: 4, strokeWidth: 2, stroke: '#fff' }}
  1944. name="每日完成作业数"
  1945. />
  1946. </AreaChart>
  1947. </ResponsiveContainer>
  1948. </div>
  1949. </div>
  1950. {/* 物料管理部分 */}
  1951. <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
  1952. <h3 className="text-lg font-semibold text-gray-900 mb-4">物料管理</h3>
  1953. <div className="flex gap-4">
  1954. {/* 可用物料 - 绿色 */}
  1955. <div className="flex-1 bg-green-50 rounded-lg border border-green-200 p-5 text-center">
  1956. <div className="text-3xl font-bold text-green-900 mb-2">
  1957. {statistics.availableMaterialsCount || 0}
  1958. </div>
  1959. <div className="text-sm text-green-700">可用物料</div>
  1960. </div>
  1961. {/* 借用中物料 - 橙色 */}
  1962. <div className="flex-1 bg-orange-50 rounded-lg border border-orange-200 p-5 text-center">
  1963. <div className="text-3xl font-bold text-orange-900 mb-2">
  1964. {statistics.loanMaterialsCount || 0}
  1965. </div>
  1966. <div className="text-sm text-orange-700">借用中物料</div>
  1967. </div>
  1968. {/* 异常物料 - 红色 */}
  1969. <div className="flex-1 bg-red-50 rounded-lg border border-red-200 p-5 text-center">
  1970. <div className="text-3xl font-bold text-red-900 mb-2">
  1971. {statistics.exceptionMaterialsCount || 0}
  1972. </div>
  1973. <div className="text-sm text-red-700">异常物料</div>
  1974. </div>
  1975. {/* 待归还物料 - 灰色 */}
  1976. <div className="flex-1 bg-gray-50 rounded-lg border border-gray-200 p-5 text-center">
  1977. <div className="text-3xl font-bold text-gray-900 mb-2">
  1978. {statistics.returnMaterialsCount || 0}
  1979. </div>
  1980. <div className="text-sm text-gray-600">待归还物料</div>
  1981. </div>
  1982. </div>
  1983. </div>
  1984. {/* 物料状态列表 */}
  1985. <Card
  1986. title={
  1987. <div className="flex items-center justify-between">
  1988. <div className="flex items-center gap-2">
  1989. <List className="w-5 h-5 text-gray-600" />
  1990. <span>物料状态列表</span>
  1991. </div>
  1992. <button
  1993. className="text-sm font-medium border-0 bg-transparent p-0 cursor-pointer"
  1994. style={{ color: '#2563eb' }}
  1995. onMouseEnter={(e) => e.currentTarget.style.color = '#1d4ed8'}
  1996. onMouseLeave={(e) => e.currentTarget.style.color = '#2563eb'}
  1997. >
  1998. 查看全部物料
  1999. </button>
  2000. </div>
  2001. }
  2002. >
  2003. <div className="overflow-x-auto">
  2004. <table className="w-full border-collapse">
  2005. <thead>
  2006. <tr className="bg-gray-50 border-b border-gray-200">
  2007. <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">物料编号</th>
  2008. <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">物料名称</th>
  2009. <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">物料类型</th>
  2010. <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">状态</th>
  2011. <th className="px-4 py-3 text-left text-xs font-medium text-gray-700">借用人员</th>
  2012. <th className="px-4 py-3 text-center text-xs font-medium text-gray-700">操作</th>
  2013. </tr>
  2014. </thead>
  2015. <tbody className="bg-white">
  2016. {/* 示例数据 - 实际应该从API获取 */}
  2017. <tr className="border-b border-gray-100 hover:bg-gray-50">
  2018. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">LOCK-001</td>
  2019. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">隔离锁具 (通用型)</td>
  2020. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">锁具</td>
  2021. <td className="px-4 py-3 whitespace-nowrap text-sm">
  2022. <span className="px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-gray-900">借用中</span>
  2023. </td>
  2024. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">张三</td>
  2025. <td className="px-4 py-3 whitespace-nowrap text-sm text-center">
  2026. <div className="flex items-center justify-center gap-2">
  2027. <button
  2028. className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
  2029. style={{ backgroundColor: '#1677ff', color: '#ffffff' }}
  2030. onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#0958d9'}
  2031. onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#1677ff'}
  2032. >
  2033. 查看
  2034. </button>
  2035. <button
  2036. className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
  2037. style={{ backgroundColor: '#fa8c16', color: '#ffffff' }}
  2038. onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#d46b08'}
  2039. onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#fa8c16'}
  2040. >
  2041. 催还
  2042. </button>
  2043. </div>
  2044. </td>
  2045. </tr>
  2046. <tr className="border-b border-gray-100 hover:bg-gray-50">
  2047. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">TAG-002</td>
  2048. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">警示标签 (大号)</td>
  2049. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">标签</td>
  2050. <td className="px-4 py-3 whitespace-nowrap text-sm">
  2051. <span className="px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700">异常</span>
  2052. </td>
  2053. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">-</td>
  2054. <td className="px-4 py-3 whitespace-nowrap text-sm text-center">
  2055. <div className="flex items-center justify-center gap-2">
  2056. <button
  2057. className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
  2058. style={{ backgroundColor: '#1677ff', color: '#ffffff' }}
  2059. onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#0958d9'}
  2060. onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#1677ff'}
  2061. >
  2062. 查看
  2063. </button>
  2064. <button
  2065. className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
  2066. style={{ backgroundColor: '#ff4d4f', color: '#ffffff' }}
  2067. onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#cf1322'}
  2068. onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#ff4d4f'}
  2069. >
  2070. 处理异常
  2071. </button>
  2072. </div>
  2073. </td>
  2074. </tr>
  2075. <tr className="border-b border-gray-100 hover:bg-gray-50">
  2076. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">DEV-003</td>
  2077. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">断电测试仪</td>
  2078. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">仪器</td>
  2079. <td className="px-4 py-3 whitespace-nowrap text-sm">
  2080. <span className="px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-gray-900">可用</span>
  2081. </td>
  2082. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">-</td>
  2083. <td className="px-4 py-3 whitespace-nowrap text-sm text-center">
  2084. <div className="flex items-center justify-center gap-2">
  2085. <button
  2086. className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
  2087. style={{ backgroundColor: '#1677ff', color: '#ffffff' }}
  2088. onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#0958d9'}
  2089. onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#1677ff'}
  2090. >
  2091. 查看
  2092. </button>
  2093. <button
  2094. className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
  2095. style={{ backgroundColor: '#52c41a', color: '#ffffff' }}
  2096. onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#389e0d'}
  2097. onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#52c41a'}
  2098. >
  2099. 申领
  2100. </button>
  2101. </div>
  2102. </td>
  2103. </tr>
  2104. <tr className="border-b border-gray-100 hover:bg-gray-50">
  2105. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">LOCK-004</td>
  2106. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">隔离锁具 (防爆型)</td>
  2107. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">锁具</td>
  2108. <td className="px-4 py-3 whitespace-nowrap text-sm">
  2109. <span className="px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700">待归还</span>
  2110. </td>
  2111. <td className="px-4 py-3 whitespace-nowrap text-sm text-gray-600">李四</td>
  2112. <td className="px-4 py-3 whitespace-nowrap text-sm text-center">
  2113. <div className="flex items-center justify-center gap-2">
  2114. <button
  2115. className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
  2116. style={{ backgroundColor: '#1677ff', color: '#ffffff' }}
  2117. onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#0958d9'}
  2118. onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#1677ff'}
  2119. >
  2120. 查看
  2121. </button>
  2122. <button
  2123. className="px-3 py-1.5 text-xs font-medium rounded transition-colors"
  2124. style={{ backgroundColor: '#fa8c16', color: '#ffffff' }}
  2125. onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#d46b08'}
  2126. onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#fa8c16'}
  2127. >
  2128. 催还
  2129. </button>
  2130. </div>
  2131. </td>
  2132. </tr>
  2133. </tbody>
  2134. </table>
  2135. </div>
  2136. </Card>
  2137. {/* 物料管理快捷操作 */}
  2138. <Card
  2139. title={
  2140. <div className="flex items-center gap-2">
  2141. <Rocket className="w-5 h-5 text-gray-600" />
  2142. <span>物料管理快捷操作</span>
  2143. </div>
  2144. }
  2145. >
  2146. <div className="flex gap-4">
  2147. <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">
  2148. <Plus className="w-6 h-6 text-gray-600" />
  2149. <span className="text-sm font-medium text-gray-700">新增物料</span>
  2150. </button>
  2151. <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">
  2152. <RefreshCw className="w-6 h-6 text-gray-600" />
  2153. <span className="text-sm font-medium text-gray-700">刷新物料状态</span>
  2154. </button>
  2155. <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">
  2156. <AlertCircle className="w-6 h-6 text-gray-600" />
  2157. <span className="text-sm font-medium text-gray-700">处理异常物料</span>
  2158. </button>
  2159. <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">
  2160. <FileText className="w-6 h-6 text-gray-600" />
  2161. <span className="text-sm font-medium text-gray-700">物料台账导出</span>
  2162. </button>
  2163. </div>
  2164. </Card>
  2165. {/* 任务详情弹框 */}
  2166. <Modal
  2167. title={null}
  2168. open={taskDetailVisible}
  2169. onCancel={() => {
  2170. setTaskDetailVisible(false);
  2171. setTaskDetailData(null);
  2172. }}
  2173. footer={null}
  2174. width={1000}
  2175. destroyOnClose
  2176. maskClosable={false}
  2177. styles={{
  2178. body: {
  2179. minHeight: '600px',
  2180. maxHeight: '700px',
  2181. height: '700px',
  2182. padding: 0,
  2183. display: 'flex',
  2184. flexDirection: 'column',
  2185. overflow: 'hidden'
  2186. }
  2187. }}
  2188. >
  2189. {taskDetailLoading ? (
  2190. <div className="py-8 text-center text-gray-500">加载中...</div>
  2191. ) : taskDetailData ? (
  2192. <div style={{
  2193. display: 'flex',
  2194. flexDirection: 'column',
  2195. height: '100%',
  2196. overflow: 'hidden'
  2197. }}>
  2198. {/* 标题区域 */}
  2199. <div className="mb-4" style={{ padding: '20px 24px 0' }}>
  2200. <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
  2201. <div style={{
  2202. width: '3px',
  2203. height: '24px',
  2204. backgroundColor: '#025fff',
  2205. borderRadius: '2px',
  2206. flexShrink: 0
  2207. }}></div>
  2208. <h2 className="text-xl font-semibold mb-2" style={{ color: '#025fff', marginBottom: 0 }}>
  2209. {taskDetailData.workName || taskDetailData.name || '任务详情'}
  2210. </h2>
  2211. <span
  2212. className="inline-flex px-3 py-1 rounded-full text-xs font-medium"
  2213. style={getTaskStatusStyle(taskDetailData.approvalStatus || taskDetailData.taskStatus || taskDetailData.status)}
  2214. >
  2215. {getTaskStatusText(taskDetailData.approvalStatus || taskDetailData.taskStatus || taskDetailData.status)}
  2216. </span>
  2217. </div>
  2218. <div className="text-sm flex gap-4" style={{ color: '#898f9a', marginTop: '12px' }}>
  2219. <span>工单编号:{taskDetailData.orderNo || '-'}</span>
  2220. <span>作业发起人:{taskDetailData.initiatorName || '-'}</span>
  2221. <span>任务负责人:{taskDetailData.workerUserName || '-'}</span>
  2222. <span>开始时间:{taskDetailData.workTime ? dateFormatter(taskDetailData.workTime) : '-'}</span>
  2223. </div>
  2224. </div>
  2225. {/* 内容区域 - 可滚动 */}
  2226. <div className="mt-6" style={{
  2227. padding: '0 24px',
  2228. flex: 1,
  2229. overflowY: 'auto',
  2230. minHeight: 0
  2231. }}>
  2232. {(() => {
  2233. const isApproved = taskDetailData?.approvalStatus === 'approved';
  2234. const nodeType = String(taskDetailData?.type || taskDetailData?.nodeType || '').trim();
  2235. const isReview = nodeType === 'review';
  2236. const isIsolation = nodeType === 'isolation' || nodeType === '隔离' || nodeType === '隔离/方案';
  2237. const isReleaseIsolation = nodeType === 'releaseIsolation' || nodeType === '解除隔离';
  2238. const isReturnLock = nodeType === 'returnLock';
  2239. // 隔离/方案节点、解除隔离节点和还锁节点
  2240. if (isIsolation || isReleaseIsolation || isReturnLock) {
  2241. return (
  2242. <div
  2243. style={{
  2244. display: 'flex',
  2245. justifyContent: 'center',
  2246. alignItems: 'center',
  2247. minHeight: '400px',
  2248. padding: '40px 20px'
  2249. }}
  2250. >
  2251. <Card
  2252. style={{
  2253. width: '100%',
  2254. maxWidth: '500px',
  2255. textAlign: 'center',
  2256. borderRadius: '12px',
  2257. boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
  2258. border: '1px solid #e8e8e8'
  2259. }}
  2260. bodyStyle={{
  2261. padding: '40px 30px'
  2262. }}
  2263. >
  2264. <div style={{ marginBottom: '24px' }}>
  2265. <LockOutlined
  2266. style={{
  2267. fontSize: '64px',
  2268. color: '#1890ff',
  2269. display: 'block'
  2270. }}
  2271. />
  2272. </div>
  2273. <div
  2274. style={{
  2275. fontSize: '20px',
  2276. fontWeight: 500,
  2277. color: '#333',
  2278. lineHeight: '1.6'
  2279. }}
  2280. >
  2281. 此节点需要在锁柜系统中进行操作
  2282. </div>
  2283. </Card>
  2284. </div>
  2285. );
  2286. }
  2287. // 审核类型节点 (review)
  2288. if (isReview) {
  2289. return (
  2290. <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
  2291. <div className="space-y-6" style={{ padding: '0 24px', flex: 1, overflowY: 'auto', minHeight: 0 }}>
  2292. {/* 自定义表单 */}
  2293. {formLoading ? (
  2294. <div className="py-8 text-center text-gray-500">表单加载中...</div>
  2295. ) : formData.rule && formData.rule.length > 0 ? (
  2296. <div>
  2297. {(() => {
  2298. const formConfig = formData.option?.formConfig || defaultFormConfig;
  2299. const layoutColumns = formConfig.layoutColumns || 1;
  2300. const gridStyle = layoutColumns > 1 ? {
  2301. display: 'grid',
  2302. gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
  2303. gap: '12px',
  2304. rowGap: '16px',
  2305. } : undefined;
  2306. return (
  2307. <AntdForm
  2308. form={taskDetailForm}
  2309. layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
  2310. size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
  2311. requiredMark={formConfig.hideRequiredMark ? false : undefined}
  2312. labelCol={formConfig.labelWidth ? {
  2313. style: {
  2314. width: `${formConfig.labelWidth}px`,
  2315. textAlign: formConfig.labelPosition === 'left' ? 'left' : formConfig.labelPosition === 'right' ? 'right' : 'left'
  2316. }
  2317. } : undefined}
  2318. >
  2319. <div
  2320. style={gridStyle}
  2321. className={layoutColumns === 1 ? 'space-y-4' : 'form-detail-grid'}
  2322. >
  2323. <style>{`
  2324. .form-detail-grid .ant-form-item {
  2325. margin-bottom: 12px;
  2326. }
  2327. `}</style>
  2328. {(formData.rule || []).map((field: any) => {
  2329. const fieldWithDisabled = isApproved ? { ...field, disabled: true, readOnly: true } : field;
  2330. return renderFieldPreview(fieldWithDisabled);
  2331. })}
  2332. </div>
  2333. </AntdForm>
  2334. );
  2335. })()}
  2336. </div>
  2337. ) : (
  2338. <div
  2339. style={{
  2340. display: 'flex',
  2341. justifyContent: 'center',
  2342. alignItems: 'center',
  2343. minHeight: '400px',
  2344. padding: '40px 20px'
  2345. }}
  2346. >
  2347. <Card
  2348. style={{
  2349. width: '100%',
  2350. maxWidth: '500px',
  2351. textAlign: 'center',
  2352. borderRadius: '12px',
  2353. boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
  2354. border: '1px solid #e8e8e8'
  2355. }}
  2356. bodyStyle={{
  2357. padding: '40px 30px'
  2358. }}
  2359. >
  2360. <div style={{ marginBottom: '24px' }}>
  2361. <CheckCircleOutlined
  2362. style={{
  2363. fontSize: '64px',
  2364. color: '#1890ff',
  2365. display: 'block'
  2366. }}
  2367. />
  2368. </div>
  2369. <div
  2370. style={{
  2371. fontSize: '20px',
  2372. fontWeight: 500,
  2373. color: '#333',
  2374. lineHeight: '1.6'
  2375. }}
  2376. >
  2377. 无需填写表单,直接点击底部按钮提交即可
  2378. </div>
  2379. </Card>
  2380. </div>
  2381. )}
  2382. {/* 审核意见 */}
  2383. <div className="flex items-start" style={{ gap: '16px', backgroundColor: '#f5f5f5', padding: '12px 12px 22px 12px', borderRadius: '12px', border: '1px solid #e0e0e0', marginBottom: '12px' }}>
  2384. <label
  2385. className="text-sm font-medium text-gray-700 whitespace-nowrap pt-2"
  2386. style={{
  2387. width: defaultFormConfig.labelWidth ? `${defaultFormConfig.labelWidth - 20}px` : '80px',
  2388. textAlign: defaultFormConfig.labelPosition === 'left' ? 'left' : defaultFormConfig.labelPosition === 'right' ? 'right' : 'left',
  2389. flexShrink: 0
  2390. }}
  2391. >
  2392. 审核意见
  2393. </label>
  2394. <div className="flex-1">
  2395. <Input.TextArea
  2396. rows={4}
  2397. value={approvalComment}
  2398. onChange={(e) => setApprovalComment(e.target.value)}
  2399. placeholder="请输入审核意见(可选)"
  2400. maxLength={500}
  2401. showCount
  2402. disabled={isApproved}
  2403. readOnly={isApproved}
  2404. />
  2405. </div>
  2406. </div>
  2407. </div>
  2408. {/* 底部按钮 - 审核节点不显示按钮(只读模式) */}
  2409. </div>
  2410. );
  2411. }
  2412. // 其他节点类型(需要根据 formId 获取表单)
  2413. return (
  2414. <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
  2415. <div className="space-y-6" style={{ padding: '0 24px', flex: 1, overflowY: 'auto', minHeight: 0 }}>
  2416. {/* 自定义表单 */}
  2417. {formLoading ? (
  2418. <div className="py-8 text-center text-gray-500">表单加载中...</div>
  2419. ) : formData.rule && formData.rule.length > 0 ? (
  2420. <div>
  2421. {(() => {
  2422. const formConfig = formData.option?.formConfig || defaultFormConfig;
  2423. const layoutColumns = formConfig.layoutColumns || 1;
  2424. const gridStyle = layoutColumns > 1 ? {
  2425. display: 'grid',
  2426. gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
  2427. gap: '12px',
  2428. rowGap: '16px',
  2429. } : undefined;
  2430. return (
  2431. <AntdForm
  2432. form={taskDetailForm}
  2433. layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
  2434. size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
  2435. requiredMark={formConfig.hideRequiredMark ? false : undefined}
  2436. labelCol={formConfig.labelWidth ? {
  2437. style: {
  2438. width: `${formConfig.labelWidth}px`,
  2439. textAlign: formConfig.labelPosition === 'left' ? 'left' : formConfig.labelPosition === 'right' ? 'right' : 'left'
  2440. }
  2441. } : undefined}
  2442. >
  2443. <div
  2444. style={gridStyle}
  2445. className={layoutColumns === 1 ? 'space-y-4' : 'form-detail-grid'}
  2446. >
  2447. <style>{`
  2448. .form-detail-grid .ant-form-item {
  2449. margin-bottom: 12px;
  2450. }
  2451. `}</style>
  2452. {(formData.rule || []).map((field: any) => {
  2453. const fieldWithDisabled = isApproved ? { ...field, disabled: true, readOnly: true } : field;
  2454. return renderFieldPreview(fieldWithDisabled);
  2455. })}
  2456. </div>
  2457. </AntdForm>
  2458. );
  2459. })()}
  2460. </div>
  2461. ) : (
  2462. <div
  2463. style={{
  2464. display: 'flex',
  2465. justifyContent: 'center',
  2466. alignItems: 'center',
  2467. minHeight: '400px',
  2468. padding: '40px 20px'
  2469. }}
  2470. >
  2471. <Card
  2472. style={{
  2473. width: '100%',
  2474. maxWidth: '500px',
  2475. textAlign: 'center',
  2476. borderRadius: '12px',
  2477. boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
  2478. border: '1px solid #e8e8e8'
  2479. }}
  2480. bodyStyle={{
  2481. padding: '40px 30px'
  2482. }}
  2483. >
  2484. <div style={{ marginBottom: '24px' }}>
  2485. <CheckCircleOutlined
  2486. style={{
  2487. fontSize: '64px',
  2488. color: '#1890ff',
  2489. display: 'block'
  2490. }}
  2491. />
  2492. </div>
  2493. <div
  2494. style={{
  2495. fontSize: '20px',
  2496. fontWeight: 500,
  2497. color: '#333',
  2498. lineHeight: '1.6'
  2499. }}
  2500. >
  2501. 无需填写表单,直接点击底部按钮提交即可
  2502. </div>
  2503. </Card>
  2504. </div>
  2505. )}
  2506. </div>
  2507. {/* 底部按钮 - 其他节点类型不显示按钮(只读模式) */}
  2508. </div>
  2509. );
  2510. })()}
  2511. </div>
  2512. </div>
  2513. ) : (
  2514. <div className="py-8 text-center text-gray-500">暂无数据</div>
  2515. )}
  2516. </Modal>
  2517. </div>
  2518. );
  2519. }