TaskManagement.tsx 177 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375
  1. import React, { useState, useEffect } from 'react';
  2. import { useNavigate } from 'react-router-dom';
  3. import { Eye, Search, RotateCcw } from 'lucide-react';
  4. import { Button, Space, Table as AntdTable, Input, message, Modal, Form as AntdForm, Card, Alert, Select, DatePicker, InputNumber, Switch, Radio, Checkbox, Cascader, Upload, Image, Avatar } from 'antd';
  5. import { UploadOutlined, LockOutlined, KeyOutlined, WarningOutlined, CheckCircleOutlined, BarcodeOutlined, SendOutlined, EnvironmentOutlined, CloseCircleOutlined } from '@ant-design/icons';
  6. import type { ColumnsType } from 'antd/es/table';
  7. import { toast } from 'sonner';
  8. import dayjs, { Dayjs } from 'dayjs';
  9. import { taskManagementApi, MyTaskVO, MyTaskPageParam, PageResponse, MyTaskNodeDetailVO, UpdateNodeApprovalParam } from '../api/mytask';
  10. import { fileApi } from '../api/file';
  11. import { dateFormatter } from '../utils/formatTime';
  12. import { DICT_TYPE, getDictLabel } from '../utils/dict';
  13. import { setConfAndFields2, FormCreateData } from '../utils/formCreate';
  14. import urgecy1Icon from '../assets/urgecy1.png';
  15. import urgecy2Icon from '../assets/urgecy2.png';
  16. import urgecy3Icon from '../assets/urgecy3.png';
  17. import { useTranslation } from 'react-i18next';
  18. import FormUploadField from './FormUploadField';
  19. // 辅助函数:安全地将值转换为 dayjs 对象
  20. const safeToDayjs = (value: any): dayjs.Dayjs | null => {
  21. if (!value && value !== 0) {
  22. return null;
  23. }
  24. // 如果是数字(时间戳)
  25. if (typeof value === 'number') {
  26. // dayjs 默认将数字当作毫秒级时间戳处理
  27. // 但如果数字小于 10000000000(10位),可能是秒级时间戳,需要转换为毫秒
  28. let timestamp = value;
  29. const originalTimestamp = timestamp;
  30. if (timestamp > 0 && timestamp < 10000000000) {
  31. // 秒级时间戳,转换为毫秒
  32. timestamp = timestamp * 1000;
  33. console.log(`时间戳转换: 秒级 -> 毫秒级`, { original: originalTimestamp, converted: timestamp });
  34. }
  35. const dayjsValue = dayjs(timestamp);
  36. if (dayjsValue.isValid()) {
  37. console.log(`时间戳解析成功:`, {
  38. original: value,
  39. timestamp,
  40. date: dayjsValue.format('YYYY-MM-DD HH:mm:ss'),
  41. year: dayjsValue.year()
  42. });
  43. return dayjsValue;
  44. } else {
  45. console.warn(`时间戳解析失败:`, { original: value, timestamp });
  46. return null;
  47. }
  48. }
  49. // 如果是字符串
  50. if (typeof value === 'string' && value.trim() !== '') {
  51. // 尝试判断是否为时间戳字符串
  52. const numValue = Number(value);
  53. if (!isNaN(numValue) && numValue > 0 && numValue.toString() === value.trim()) {
  54. // 是纯数字字符串,可能是时间戳
  55. // 判断是秒级还是毫秒级时间戳
  56. let timestamp = numValue;
  57. const originalTimestamp = timestamp;
  58. if (timestamp < 10000000000) {
  59. // 秒级时间戳(10位数字),转换为毫秒
  60. timestamp = timestamp * 1000;
  61. console.log(`时间戳转换: 秒级 -> 毫秒级`, { original: originalTimestamp, converted: timestamp });
  62. } else {
  63. console.log(`时间戳转换: 毫秒级时间戳`, { timestamp });
  64. }
  65. // 否则是毫秒级时间戳(13位或更多),直接使用
  66. const dayjsValue = dayjs(timestamp);
  67. if (dayjsValue.isValid()) {
  68. console.log(`时间戳解析成功:`, {
  69. original: value,
  70. timestamp,
  71. date: dayjsValue.format('YYYY-MM-DD HH:mm:ss'),
  72. year: dayjsValue.year()
  73. });
  74. return dayjsValue;
  75. } else {
  76. console.warn(`时间戳解析失败:`, { original: value, timestamp });
  77. return null;
  78. }
  79. } else {
  80. // 普通日期字符串
  81. const dayjsValue = dayjs(value);
  82. return dayjsValue.isValid() ? dayjsValue : null;
  83. }
  84. }
  85. // 如果已经是 dayjs 对象,检查是否有 isValid 方法
  86. if (value && typeof value === 'object' && typeof value.isValid === 'function') {
  87. try {
  88. return value.isValid() ? value : null;
  89. } catch (e) {
  90. // isValid 调用失败,尝试重新创建
  91. }
  92. }
  93. // 如果是对象,检查是否有 isValid 方法(可能是序列化后的 dayjs 对象)
  94. if (value && typeof value === 'object') {
  95. // 尝试重新创建 dayjs 对象
  96. try {
  97. // 如果有 $d 属性,可能是 dayjs 对象
  98. if (value.$d) {
  99. const dayjsValue = dayjs(value.$d);
  100. return dayjsValue.isValid() ? dayjsValue : null;
  101. }
  102. // 如果有时间戳相关的属性
  103. if (value.valueOf && typeof value.valueOf === 'function') {
  104. const timestamp = value.valueOf();
  105. if (typeof timestamp === 'number') {
  106. const dayjsValue = dayjs(timestamp);
  107. return dayjsValue.isValid() ? dayjsValue : null;
  108. }
  109. }
  110. } catch (e) {
  111. // 转换失败,返回 null
  112. return null;
  113. }
  114. }
  115. return null;
  116. };
  117. // 辅助函数:验证 dayjs 对象是否有效
  118. const isValidDayjs = (value: any): boolean => {
  119. if (!value) return false;
  120. if (typeof value !== 'object') return false;
  121. if (typeof value.isValid !== 'function') return false;
  122. try {
  123. return value.isValid();
  124. } catch (e) {
  125. return false;
  126. }
  127. };
  128. // 辅助函数:将表单值中的日期字符串转换为 dayjs 对象
  129. const convertDateValues = (formValues: any, fields: any[]): any => {
  130. const fieldTypeMap: { [key: string]: string } = {};
  131. // 建立字段名到类型的映射
  132. fields.forEach((field: any) => {
  133. try {
  134. const fieldObj = typeof field === 'string' ? JSON.parse(field) : field;
  135. const fieldName = fieldObj.name || fieldObj.field;
  136. if (fieldName) {
  137. fieldTypeMap[fieldName] = fieldObj.type;
  138. }
  139. } catch (e) {
  140. // 解析失败,跳过
  141. console.warn('解析字段配置失败:', e);
  142. }
  143. });
  144. // 转换日期值
  145. const convertedValues: any = {};
  146. Object.keys(formValues).forEach((fieldName) => {
  147. const fieldType = fieldTypeMap[fieldName];
  148. const value = formValues[fieldName];
  149. if (fieldType === 'date' || fieldType === 'datetime' || fieldType === 'timepicker') {
  150. // 日期、日期时间或时间选择器类型
  151. convertedValues[fieldName] = safeToDayjs(value) || undefined;
  152. } else if (fieldType === 'daterange') {
  153. // 日期范围类型
  154. if (Array.isArray(value) && value.length === 2) {
  155. const [start, end] = value;
  156. const startDayjs = safeToDayjs(start);
  157. const endDayjs = safeToDayjs(end);
  158. // 调试日志
  159. if (startDayjs && endDayjs) {
  160. console.log(`日期范围字段 ${fieldName} 转换成功:`, {
  161. original: { start, end },
  162. converted: {
  163. start: startDayjs.format('YYYY-MM-DD HH:mm:ss'),
  164. end: endDayjs.format('YYYY-MM-DD HH:mm:ss')
  165. }
  166. });
  167. convertedValues[fieldName] = [startDayjs, endDayjs];
  168. } else {
  169. console.warn(`日期范围字段 ${fieldName} 转换失败:`, { start, end, startDayjs, endDayjs });
  170. convertedValues[fieldName] = undefined;
  171. }
  172. } else {
  173. convertedValues[fieldName] = undefined;
  174. }
  175. } else {
  176. // 其他类型直接使用原值
  177. convertedValues[fieldName] = value;
  178. }
  179. });
  180. return convertedValues;
  181. };
  182. export default function TaskManagement() {
  183. const { t } = useTranslation();
  184. const navigate = useNavigate();
  185. const [loading, setLoading] = useState(true);
  186. const [list, setList] = useState<MyTaskVO[]>([]);
  187. const [total, setTotal] = useState(0);
  188. // 从 sessionStorage 读取 status 参数
  189. const getInitialStatus = () => {
  190. const status = sessionStorage.getItem('taskManagementStatus');
  191. return status || undefined;
  192. };
  193. const [queryParams, setQueryParams] = useState<MyTaskPageParam>({
  194. pageNo: 1,
  195. pageSize: 10,
  196. key: undefined,
  197. status: getInitialStatus(),
  198. });
  199. const [searchOrderNo, setSearchOrderNo] = useState('');
  200. const [searchName, setSearchName] = useState('');
  201. const [approvalStatusDictList, setApprovalStatusDictList] = useState<any[]>([]);
  202. const [urgencyLevelDictList, setUrgencyLevelDictList] = useState<any[]>([]);
  203. const [jobCategoryDictList, setJobCategoryDictList] = useState<any[]>([]); // 作业分类 work_type
  204. // 节点详情弹框相关状态
  205. const [detailVisible, setDetailVisible] = useState(false);
  206. const [detailLoading, setDetailLoading] = useState(false);
  207. const [detailData, setDetailData] = useState<MyTaskNodeDetailVO | null>(null);
  208. const [formData, setFormData] = useState<FormCreateData>({
  209. rule: [],
  210. option: {}
  211. });
  212. const [formLoading, setFormLoading] = useState(false); // 表单加载状态
  213. const [originalFields, setOriginalFields] = useState<string[]>([]); // 保存原始的 fields 数组(JSON 字符串数组)
  214. const [originalConf, setOriginalConf] = useState<string>(''); // 保存原始的 conf(JSON 字符串)
  215. const [detailForm] = AntdForm.useForm();
  216. const [approvalComment, setApprovalComment] = useState(''); // 审核意见
  217. const [approvalLoading, setApprovalLoading] = useState(false); // 审核操作loading状态
  218. const [submitLoading, setSubmitLoading] = useState(false); // 提交操作loading状态
  219. // 隔离方式字典(isolation_method:0=盲板,1=上锁挂牌,2=拆除)
  220. const [isolationMethodDictList, setIsolationMethodDictList] = useState<any[]>([]);
  221. // 盲板/拆除类型时的设备编号与附件(仅隔离/方案、解除隔离且隔离方式为盲板或拆除时使用)
  222. const [isolationDeviceNumber, setIsolationDeviceNumber] = useState('');
  223. const [isolationFileList, setIsolationFileList] = useState<any[]>([]);
  224. const [isolationSubmitLoading, setIsolationSubmitLoading] = useState(false);
  225. // 组件挂载时打印调试信息,并从 sessionStorage 读取 status
  226. useEffect(() => {
  227. console.log('TaskManagement 组件已加载');
  228. const status = sessionStorage.getItem('taskManagementStatus');
  229. if (status) {
  230. console.log('从 sessionStorage 读取到 status:', status);
  231. setQueryParams(prev => ({
  232. ...prev,
  233. status: status,
  234. pageNo: 1, // 重置到第一页
  235. }));
  236. // 读取后清除 sessionStorage,避免下次进入时自动应用
  237. sessionStorage.removeItem('taskManagementStatus');
  238. }
  239. }, []);
  240. // 监听弹框状态变化
  241. useEffect(() => {
  242. console.log('TaskManagement: detailVisible 状态变化', detailVisible, 'detailData:', detailData);
  243. }, [detailVisible, detailData]);
  244. // 获取审批状态字典
  245. const getApprovalStatusDictList = async () => {
  246. try {
  247. const { dictDataApi } = await import('../api/DictData');
  248. const response = await dictDataApi.getDictDataPage({
  249. pageNo: 1,
  250. pageSize: -1,
  251. dictType: 'approval_status',
  252. });
  253. const data = (response as any)?.data || response;
  254. const dictList = data?.list || [];
  255. setApprovalStatusDictList(dictList);
  256. console.log('TaskManagement: 获取审批状态字典成功', dictList);
  257. } catch (error: any) {
  258. console.error('获取审批状态字典失败:', error);
  259. }
  260. };
  261. // 获取紧急程度字典
  262. const getUrgencyLevelDictList = async () => {
  263. try {
  264. const { dictDataApi } = await import('../api/DictData');
  265. const response = await dictDataApi.getDictDataPage({
  266. pageNo: 1,
  267. pageSize: -1,
  268. dictType: 'urgency_level',
  269. });
  270. const data = (response as any)?.data || response;
  271. const dictList = data?.list || [];
  272. setUrgencyLevelDictList(dictList);
  273. console.log('TaskManagement: 获取紧急程度字典成功', dictList);
  274. } catch (error: any) {
  275. console.error('获取紧急程度字典失败:', error);
  276. }
  277. };
  278. // 获取隔离方式字典(isolation_method)
  279. const getIsolationMethodDictList = async () => {
  280. try {
  281. const { dictDataApi } = await import('../api/DictData');
  282. const response = await dictDataApi.getDictDataPage({
  283. pageNo: 1,
  284. pageSize: -1,
  285. dictType: 'isolation_method',
  286. });
  287. const data = (response as any)?.data || response;
  288. const dictList = data?.list || [];
  289. setIsolationMethodDictList(dictList);
  290. } catch (error: any) {
  291. console.error('获取隔离方式字典失败:', error);
  292. }
  293. };
  294. // 获取作业分类字典(work_type)
  295. const getJobCategoryDictList = async () => {
  296. try {
  297. const { dictDataApi } = await import('../api/DictData');
  298. const response = await dictDataApi.getDictDataPage({
  299. pageNo: 1,
  300. pageSize: -1,
  301. dictType: 'work_type',
  302. });
  303. const data = (response as any)?.data || response;
  304. setJobCategoryDictList(data?.list || []);
  305. } catch (error: any) {
  306. console.error('获取作业分类字典失败:', error);
  307. setJobCategoryDictList([]);
  308. }
  309. };
  310. useEffect(() => {
  311. getApprovalStatusDictList();
  312. getUrgencyLevelDictList();
  313. getIsolationMethodDictList();
  314. getJobCategoryDictList();
  315. }, []);
  316. /** 查询列表 */
  317. const getList = async (params?: MyTaskPageParam) => {
  318. const searchParams = params || queryParams;
  319. setLoading(true);
  320. try {
  321. console.log('TaskManagement: 开始获取任务管理列表', searchParams);
  322. const response = await taskManagementApi.getAdminWorkPage(searchParams);
  323. console.log('TaskManagement: 接口响应', response);
  324. const data = (response as any)?.data || response;
  325. const pageData = (data as PageResponse<MyTaskVO>);
  326. console.log('TaskManagement: 解析后的数据', pageData);
  327. setList(pageData.list || []);
  328. setTotal(pageData.total || 0);
  329. } catch (error: any) {
  330. console.error('TaskManagement: 获取任务管理列表失败', error);
  331. toast.error(error.message || t('form.fetchTaskListFailed'));
  332. setList([]);
  333. setTotal(0);
  334. } finally {
  335. setLoading(false);
  336. }
  337. };
  338. useEffect(() => {
  339. console.log('TaskManagement: useEffect 触发,queryParams:', queryParams);
  340. getList();
  341. // eslint-disable-next-line react-hooks/exhaustive-deps
  342. }, [queryParams.pageNo, queryParams.pageSize, queryParams.key, queryParams.status]);
  343. // 搜索:两个条件都通过 key 传参,值为具体输入(多个用空格拼接)
  344. const handleSearch = () => {
  345. const parts = [searchOrderNo.trim(), searchName.trim()].filter(Boolean);
  346. setQueryParams({
  347. ...queryParams,
  348. pageNo: 1,
  349. key: parts.length ? parts.join(' ') : undefined,
  350. });
  351. };
  352. // 重置
  353. const handleReset = () => {
  354. setSearchOrderNo('');
  355. setSearchName('');
  356. setQueryParams({
  357. ...queryParams,
  358. pageNo: 1,
  359. key: undefined,
  360. status: undefined, // 重置时也清除 status
  361. });
  362. };
  363. // 默认表单配置
  364. const defaultFormConfig = {
  365. name: '',
  366. labelPosition: 'right',
  367. formSize: 'middle',
  368. labelSuffix: '',
  369. labelWidth: 100,
  370. hideRequiredMark: false,
  371. showValidationError: true,
  372. inlineValidation: false,
  373. showSubmitButton: false,
  374. showResetButton: false,
  375. };
  376. // 渲染字段预览(支持嵌套结构)
  377. const renderFieldPreview = (field: any, parentSpanStyle?: React.CSSProperties): React.ReactNode => {
  378. const formConfig = formData.option?.formConfig || defaultFormConfig;
  379. const layoutColumns = formConfig.layoutColumns || 1;
  380. const spanStyle = parentSpanStyle || (layoutColumns > 1 ? { gridColumn: `span ${Math.min(layoutColumns, field.span || 1)}` } : undefined);
  381. // 处理容器类型(card 和 grid)
  382. if (field.type === 'card') {
  383. const children = field.children || [];
  384. // 优先使用 label(字段名称),如果没有则使用 cardTitle,最后使用默认值
  385. const cardTitle = field.label || field.cardTitle || '卡片容器';
  386. return (
  387. <div key={field.id} style={spanStyle} className="mb-4">
  388. <Card title={cardTitle} className="w-full">
  389. <div className="space-y-4">
  390. {children.map((child: any) => renderFieldPreview(child))}
  391. </div>
  392. </Card>
  393. </div>
  394. );
  395. }
  396. if (field.type === 'grid') {
  397. const gridColumns = field.gridColumns || 2;
  398. const children = field.children || [];
  399. return (
  400. <div key={field.id} style={spanStyle} className="mb-4">
  401. <div
  402. style={{
  403. display: 'grid',
  404. gridTemplateColumns: `repeat(${gridColumns}, minmax(0, 1fr))`,
  405. gap: '16px',
  406. }}
  407. >
  408. {children.map((child: any) => {
  409. const childSpanStyle = gridColumns > 1
  410. ? { gridColumn: `span ${Math.min(gridColumns, child.span || 1)}` }
  411. : undefined;
  412. return (
  413. <div key={child.id} style={childSpanStyle}>
  414. {renderFieldPreview(child)}
  415. </div>
  416. );
  417. })}
  418. </div>
  419. </div>
  420. );
  421. }
  422. // 处理 alert 类型
  423. if (field.type === 'alert') {
  424. const themeClass =
  425. field.alertTheme === 'dark'
  426. ? 'bg-gray-800 text-white border-gray-700'
  427. : '';
  428. return (
  429. <div key={field.id} className="mb-4" style={spanStyle}>
  430. <Alert
  431. message={field.alertTitle || field.label}
  432. description={field.alertDescription}
  433. type={field.alertType || 'info'}
  434. showIcon={field.alertShowIcon !== false}
  435. closable={field.alertClosable}
  436. closeText={field.alertCloseText}
  437. className={`${field.alertCentered ? 'text-center' : ''} ${themeClass}`}
  438. banner
  439. style={field.style}
  440. />
  441. {field.hint && <div className="text-xs text-gray-400 mt-1">{field.hint}</div>}
  442. </div>
  443. );
  444. }
  445. // 与表单设计器一致:每字段 labelCol/wrapperCol
  446. const getItemLayout = (f: any) => {
  447. const isTop = formConfig.labelPosition === 'top';
  448. const w = f.labelWidth ?? formConfig.labelWidth ?? 100;
  449. const effective = (typeof w === 'number' && w > 0) ? w : 100;
  450. return {
  451. labelCol: isTop ? undefined : { flex: `${effective}px`, style: { minWidth: `${effective}px`, textAlign: formConfig.labelPosition === 'right' ? 'right' : 'left' } },
  452. wrapperCol: isTop ? undefined : { flex: 'auto', style: { minWidth: 0 } },
  453. };
  454. };
  455. const itemLayout = getItemLayout(field);
  456. // 处理普通字段
  457. switch (field.type) {
  458. case 'textarea':
  459. return (
  460. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  461. <AntdForm.Item
  462. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  463. name={field.name || field.field}
  464. required={field.required && !formConfig.hideRequiredMark}
  465. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  466. help={field.hint}
  467. labelCol={itemLayout.labelCol}
  468. wrapperCol={itemLayout.wrapperCol}
  469. >
  470. <Input.TextArea
  471. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  472. rows={4}
  473. allowClear={field.showClear}
  474. maxLength={field.maxLength}
  475. readOnly={field.readOnly}
  476. disabled={field.disabled}
  477. size={field.size || 'middle'}
  478. />
  479. </AntdForm.Item>
  480. </div>
  481. );
  482. case 'password':
  483. return (
  484. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  485. <AntdForm.Item
  486. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  487. name={field.name || field.field}
  488. required={field.required && !formConfig.hideRequiredMark}
  489. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  490. help={field.hint}
  491. labelCol={itemLayout.labelCol}
  492. wrapperCol={itemLayout.wrapperCol}
  493. >
  494. <Input.Password
  495. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  496. allowClear={field.showClear}
  497. maxLength={field.maxLength}
  498. readOnly={field.readOnly}
  499. disabled={field.disabled}
  500. size={field.size || 'middle'}
  501. />
  502. </AntdForm.Item>
  503. </div>
  504. );
  505. case 'number':
  506. return (
  507. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  508. <AntdForm.Item
  509. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  510. name={field.name || field.field}
  511. required={field.required && !formConfig.hideRequiredMark}
  512. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  513. help={field.hint}
  514. labelCol={itemLayout.labelCol}
  515. wrapperCol={itemLayout.wrapperCol}
  516. >
  517. <InputNumber
  518. style={{ width: '100%' }}
  519. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  520. max={field.maxLength}
  521. readOnly={field.readOnly}
  522. disabled={field.disabled}
  523. size={field.size || 'middle'}
  524. />
  525. </AntdForm.Item>
  526. </div>
  527. );
  528. case 'select':
  529. return (
  530. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  531. <AntdForm.Item
  532. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  533. name={field.name || field.field}
  534. required={field.required && !formConfig.hideRequiredMark}
  535. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  536. help={field.hint}
  537. labelCol={itemLayout.labelCol}
  538. wrapperCol={itemLayout.wrapperCol}
  539. >
  540. <Select
  541. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择'}
  542. allowClear={field.showClear}
  543. disabled={field.disabled}
  544. size={field.size || 'middle'}
  545. >
  546. {(field.options || []).map((opt: any, idx: number) => (
  547. <Select.Option key={idx} value={opt.value}>{opt.label}</Select.Option>
  548. ))}
  549. </Select>
  550. </AntdForm.Item>
  551. </div>
  552. );
  553. case 'date':
  554. return (
  555. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  556. <AntdForm.Item
  557. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  558. name={field.name || field.field}
  559. required={field.required && !formConfig.hideRequiredMark}
  560. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期' }] : []}
  561. help={field.hint}
  562. labelCol={itemLayout.labelCol}
  563. wrapperCol={itemLayout.wrapperCol}
  564. getValueFromEvent={(value) => {
  565. // 选择日期时,将 dayjs 对象转换为毫秒级时间戳
  566. if (!value) return undefined;
  567. if (value && typeof value === 'object' && 'valueOf' in value && typeof value.valueOf === 'function' && 'isValid' in value && typeof value.isValid === 'function') {
  568. // 确保是有效的 dayjs 对象
  569. if (value.isValid()) {
  570. return value.valueOf(); // dayjs 的 valueOf() 返回毫秒级时间戳
  571. }
  572. return undefined;
  573. }
  574. return value;
  575. }}
  576. normalize={(value) => {
  577. // 回显时,将毫秒级时间戳转换为 dayjs 对象
  578. if (!value) return undefined;
  579. if (typeof value === 'number') {
  580. const dayjsValue = dayjs(value);
  581. return dayjsValue.isValid() ? dayjsValue : undefined;
  582. }
  583. if (typeof value === 'string') {
  584. const numValue = Number(value);
  585. if (!isNaN(numValue) && numValue > 0) {
  586. const dayjsValue = dayjs(numValue);
  587. return dayjsValue.isValid() ? dayjsValue : undefined;
  588. }
  589. const dayjsValue = dayjs(value);
  590. return dayjsValue.isValid() ? dayjsValue : undefined;
  591. }
  592. if (value && typeof value === 'object' && 'isValid' in value && typeof value.isValid === 'function') {
  593. return value.isValid() ? value : undefined;
  594. }
  595. return undefined;
  596. }}
  597. >
  598. <DatePicker
  599. style={{ width: '100%' }}
  600. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期'}
  601. allowClear={field.showClear}
  602. disabled={field.disabled}
  603. size={field.size || 'middle'}
  604. />
  605. </AntdForm.Item>
  606. </div>
  607. );
  608. case 'daterange':
  609. return (
  610. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  611. <AntdForm.Item
  612. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  613. name={field.name || field.field}
  614. required={field.required && !formConfig.hideRequiredMark}
  615. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期范围' }] : []}
  616. help={field.hint}
  617. labelCol={itemLayout.labelCol}
  618. wrapperCol={itemLayout.wrapperCol}
  619. getValueFromEvent={(value) => {
  620. // 选择日期范围时,将 dayjs 对象数组转换为毫秒级时间戳数组
  621. if (!value || !Array.isArray(value) || value.length !== 2) return undefined;
  622. const [start, end] = value;
  623. if (start && typeof start === 'object' && 'valueOf' in start && typeof start.valueOf === 'function' && 'isValid' in start && typeof start.isValid === 'function' &&
  624. end && typeof end === 'object' && 'valueOf' in end && typeof end.valueOf === 'function' && 'isValid' in end && typeof end.isValid === 'function') {
  625. // 确保都是有效的 dayjs 对象
  626. if (start.isValid() && end.isValid()) {
  627. return [start.valueOf(), end.valueOf()]; // 返回毫秒级时间戳数组
  628. }
  629. return undefined;
  630. }
  631. return value;
  632. }}
  633. normalize={(value) => {
  634. // 回显时,将毫秒级时间戳数组转换为 dayjs 对象数组
  635. if (!value) return undefined;
  636. if (Array.isArray(value) && value.length === 2) {
  637. const [start, end] = value;
  638. let startDayjs: any = null;
  639. let endDayjs: any = null;
  640. if (typeof start === 'number') {
  641. startDayjs = dayjs(start);
  642. startDayjs = startDayjs.isValid() ? startDayjs : null;
  643. } else if (typeof start === 'string') {
  644. const numValue = Number(start);
  645. if (!isNaN(numValue) && numValue > 0) {
  646. startDayjs = dayjs(numValue);
  647. startDayjs = startDayjs.isValid() ? startDayjs : null;
  648. } else {
  649. startDayjs = dayjs(start);
  650. startDayjs = startDayjs.isValid() ? startDayjs : null;
  651. }
  652. } else if (start && typeof start === 'object' && 'isValid' in start && typeof start.isValid === 'function') {
  653. startDayjs = start.isValid() ? start : null;
  654. }
  655. if (typeof end === 'number') {
  656. endDayjs = dayjs(end);
  657. endDayjs = endDayjs.isValid() ? endDayjs : null;
  658. } else if (typeof end === 'string') {
  659. const numValue = Number(end);
  660. if (!isNaN(numValue) && numValue > 0) {
  661. endDayjs = dayjs(numValue);
  662. endDayjs = endDayjs.isValid() ? endDayjs : null;
  663. } else {
  664. endDayjs = dayjs(end);
  665. endDayjs = endDayjs.isValid() ? endDayjs : null;
  666. }
  667. } else if (end && typeof end === 'object' && 'isValid' in end && typeof end.isValid === 'function') {
  668. endDayjs = end.isValid() ? end : null;
  669. }
  670. if (startDayjs && endDayjs) {
  671. return [startDayjs, endDayjs];
  672. }
  673. }
  674. return undefined;
  675. }}
  676. >
  677. <DatePicker.RangePicker
  678. style={{ width: '100%' }}
  679. placeholder={Array.isArray(field.placeholder) ? field.placeholder as [string, string] : ['开始日期', '结束日期']}
  680. allowClear={field.showClear}
  681. disabled={field.disabled}
  682. size={field.size || 'middle'}
  683. />
  684. </AntdForm.Item>
  685. </div>
  686. );
  687. case 'datetime':
  688. return (
  689. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  690. <AntdForm.Item
  691. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  692. name={field.name || field.field}
  693. required={field.required && !formConfig.hideRequiredMark}
  694. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期时间' }] : []}
  695. help={field.hint}
  696. labelCol={itemLayout.labelCol}
  697. wrapperCol={itemLayout.wrapperCol}
  698. getValueFromEvent={(value) => {
  699. // 选择日期时间时,将 dayjs 对象转换为毫秒级时间戳
  700. if (!value) return undefined;
  701. if (value && typeof value === 'object' && 'valueOf' in value) {
  702. return value.valueOf(); // dayjs 的 valueOf() 返回毫秒级时间戳
  703. }
  704. return value;
  705. }}
  706. normalize={(value) => {
  707. // 回显时,将毫秒级时间戳转换为 dayjs 对象
  708. if (!value) return undefined;
  709. if (typeof value === 'number') {
  710. const dayjsValue = dayjs(value);
  711. return dayjsValue.isValid() ? dayjsValue : undefined;
  712. }
  713. if (typeof value === 'string') {
  714. const numValue = Number(value);
  715. if (!isNaN(numValue) && numValue > 0) {
  716. const dayjsValue = dayjs(numValue);
  717. return dayjsValue.isValid() ? dayjsValue : undefined;
  718. }
  719. const dayjsValue = dayjs(value);
  720. return dayjsValue.isValid() ? dayjsValue : undefined;
  721. }
  722. if (value && typeof value === 'object' && 'isValid' in value && typeof value.isValid === 'function') {
  723. return value.isValid() ? value : undefined;
  724. }
  725. return undefined;
  726. }}
  727. >
  728. <DatePicker
  729. style={{ width: '100%' }}
  730. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期时间'}
  731. allowClear={field.showClear}
  732. showTime={{ format: 'HH:mm:ss' }}
  733. format="YYYY-MM-DD HH:mm:ss"
  734. disabled={field.disabled}
  735. size={field.size || 'middle'}
  736. />
  737. </AntdForm.Item>
  738. </div>
  739. );
  740. case 'switch':
  741. return (
  742. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  743. <AntdForm.Item
  744. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  745. name={field.name || field.field}
  746. required={field.required && !formConfig.hideRequiredMark}
  747. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  748. help={field.hint}
  749. valuePropName="checked"
  750. labelCol={itemLayout.labelCol}
  751. wrapperCol={itemLayout.wrapperCol}
  752. >
  753. <Switch disabled={field.disabled} />
  754. </AntdForm.Item>
  755. </div>
  756. );
  757. case 'radio':
  758. return (
  759. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  760. <AntdForm.Item
  761. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  762. name={field.name || field.field}
  763. required={field.required && !formConfig.hideRequiredMark}
  764. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  765. help={field.hint}
  766. labelCol={itemLayout.labelCol}
  767. wrapperCol={itemLayout.wrapperCol}
  768. >
  769. <Radio.Group disabled={field.disabled}>
  770. {(field.options || []).map((opt: any, idx: number) => (
  771. <Radio key={idx} value={opt.value}>{opt.label}</Radio>
  772. ))}
  773. </Radio.Group>
  774. </AntdForm.Item>
  775. </div>
  776. );
  777. case 'checkbox':
  778. return (
  779. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  780. <AntdForm.Item
  781. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  782. name={field.name || field.field}
  783. required={field.required && !formConfig.hideRequiredMark}
  784. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  785. help={field.hint}
  786. labelCol={itemLayout.labelCol}
  787. wrapperCol={itemLayout.wrapperCol}
  788. >
  789. <Checkbox.Group disabled={field.disabled}>
  790. {(field.options || []).map((opt: any, idx: number) => (
  791. <Checkbox key={idx} value={opt.value}>{opt.label}</Checkbox>
  792. ))}
  793. </Checkbox.Group>
  794. </AntdForm.Item>
  795. </div>
  796. );
  797. case 'cascader':
  798. return (
  799. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  800. <AntdForm.Item
  801. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  802. name={field.name || field.field}
  803. required={field.required && !formConfig.hideRequiredMark}
  804. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  805. help={field.hint}
  806. labelCol={itemLayout.labelCol}
  807. wrapperCol={itemLayout.wrapperCol}
  808. >
  809. <Cascader
  810. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择'}
  811. options={field.cascaderOptions || []}
  812. className="w-full"
  813. allowClear={field.showClear}
  814. disabled={field.disabled}
  815. size={field.size || 'middle'}
  816. />
  817. </AntdForm.Item>
  818. </div>
  819. );
  820. case 'upload':
  821. return (
  822. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  823. <AntdForm.Item
  824. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  825. name={field.name || field.field}
  826. required={field.required && !formConfig.hideRequiredMark}
  827. rules={field.required ? [{ required: true, message: field.requiredMessage || '请上传' }] : []}
  828. help={field.hint}
  829. labelCol={itemLayout.labelCol}
  830. wrapperCol={itemLayout.wrapperCol}
  831. >
  832. <FormUploadField
  833. uploadType={field.uploadType}
  834. maxCount={field.maxCount}
  835. accept={field.accept}
  836. disabled={field.disabled}
  837. />
  838. </AntdForm.Item>
  839. </div>
  840. );
  841. case 'time':
  842. case 'timepicker':
  843. return (
  844. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  845. <AntdForm.Item
  846. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  847. name={field.name || field.field}
  848. required={field.required && !formConfig.hideRequiredMark}
  849. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择时间' }] : []}
  850. help={field.hint}
  851. labelCol={itemLayout.labelCol}
  852. wrapperCol={itemLayout.wrapperCol}
  853. >
  854. <DatePicker
  855. style={{ width: '100%' }}
  856. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择时间'}
  857. allowClear={field.showClear}
  858. picker="time"
  859. format="HH:mm:ss"
  860. disabled={field.disabled}
  861. size={field.size || 'middle'}
  862. />
  863. </AntdForm.Item>
  864. </div>
  865. );
  866. default:
  867. // 检查 inputType 是否为时间类型
  868. const inputType = field.inputType || field.type;
  869. if (inputType === 'date' || inputType === 'datetime' || inputType === 'time' || inputType === 'timepicker') {
  870. // 如果是时间类型,使用 DatePicker
  871. if (inputType === 'datetime') {
  872. return (
  873. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  874. <AntdForm.Item
  875. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  876. name={field.name || field.field}
  877. required={field.required && !formConfig.hideRequiredMark}
  878. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期时间' }] : []}
  879. help={field.hint}
  880. labelCol={itemLayout.labelCol}
  881. wrapperCol={itemLayout.wrapperCol}
  882. >
  883. <DatePicker
  884. style={{ width: '100%' }}
  885. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期时间'}
  886. allowClear={field.showClear}
  887. showTime={{ format: 'HH:mm:ss' }}
  888. format="YYYY-MM-DD HH:mm:ss"
  889. disabled={field.disabled}
  890. size={field.size || 'middle'}
  891. />
  892. </AntdForm.Item>
  893. </div>
  894. );
  895. } else if (inputType === 'time' || inputType === 'timepicker') {
  896. return (
  897. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  898. <AntdForm.Item
  899. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  900. name={field.name || field.field}
  901. required={field.required && !formConfig.hideRequiredMark}
  902. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择时间' }] : []}
  903. help={field.hint}
  904. labelCol={itemLayout.labelCol}
  905. wrapperCol={itemLayout.wrapperCol}
  906. >
  907. <DatePicker
  908. style={{ width: '100%' }}
  909. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择时间'}
  910. allowClear={field.showClear}
  911. picker="time"
  912. format="HH:mm:ss"
  913. disabled={field.disabled}
  914. size={field.size || 'middle'}
  915. />
  916. </AntdForm.Item>
  917. </div>
  918. );
  919. } else {
  920. // date 类型
  921. return (
  922. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  923. <AntdForm.Item
  924. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  925. name={field.name || field.field}
  926. required={field.required && !formConfig.hideRequiredMark}
  927. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期' }] : []}
  928. help={field.hint}
  929. labelCol={itemLayout.labelCol}
  930. wrapperCol={itemLayout.wrapperCol}
  931. >
  932. <DatePicker
  933. style={{ width: '100%' }}
  934. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期'}
  935. allowClear={field.showClear}
  936. disabled={field.disabled}
  937. size={field.size || 'middle'}
  938. />
  939. </AntdForm.Item>
  940. </div>
  941. );
  942. }
  943. }
  944. // 其他类型使用 Input
  945. return (
  946. <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
  947. <AntdForm.Item
  948. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  949. name={field.name || field.field}
  950. required={field.required && !formConfig.hideRequiredMark}
  951. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  952. help={field.hint}
  953. labelCol={itemLayout.labelCol}
  954. wrapperCol={itemLayout.wrapperCol}
  955. >
  956. <Input
  957. type={field.inputType || 'text'}
  958. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  959. allowClear={field.showClear}
  960. maxLength={field.maxLength}
  961. readOnly={field.readOnly}
  962. disabled={field.disabled}
  963. size={field.size || 'middle'}
  964. />
  965. </AntdForm.Item>
  966. </div>
  967. );
  968. }
  969. };
  970. // 查看节点详情(节点详情弹框)
  971. const handleViewDetail = async (record: MyTaskVO) => {
  972. console.log('TaskManagement: handleViewDetail 被调用', record);
  973. if (!record.nodeId) {
  974. console.warn('TaskManagement: 节点ID不存在', record);
  975. message.warning('节点ID不存在');
  976. return;
  977. }
  978. setDetailLoading(true);
  979. setDetailVisible(true); // 先打开弹框显示加载状态
  980. console.log('TaskManagement: 弹框状态已设置为 true (加载中)');
  981. try {
  982. console.log('TaskManagement: 开始获取节点详情', { nodeId: record.nodeId });
  983. const response = await taskManagementApi.getAdminWorkNodeDetail(record.nodeId);
  984. console.log('TaskManagement: 节点详情响应', response);
  985. let data: any = response;
  986. // 直接从第一层 data.formId 获取 formId
  987. let extractedFormId: number | undefined = undefined;
  988. if (data?.formId !== undefined && data?.formId !== null) {
  989. const formIdValue = data.formId;
  990. if (typeof formIdValue === 'string') {
  991. const parsed = parseInt(formIdValue, 10);
  992. if (!isNaN(parsed)) {
  993. extractedFormId = parsed;
  994. console.log('TaskManagement: ✅ 从 data.formId (字符串) 提取到 formId', extractedFormId);
  995. }
  996. } else if (typeof formIdValue === 'number') {
  997. extractedFormId = formIdValue;
  998. console.log('TaskManagement: ✅ 从 data.formId (数字) 提取到 formId', extractedFormId);
  999. }
  1000. }
  1001. console.log('TaskManagement: formId 提取结果', {
  1002. extractedFormId,
  1003. '原始 data.formId': data?.formId,
  1004. 'data.formId 类型': typeof data?.formId
  1005. });
  1006. if (extractedFormId === undefined) {
  1007. console.warn('TaskManagement: ⚠️ 未能提取到 formId', {
  1008. 'data.formId': data?.formId,
  1009. 'data': data
  1010. });
  1011. }
  1012. // 合并列表数据中的作业信息(作业名称、编号、负责人、时间等)
  1013. // 直接使用第一层 data,不解析 data.data
  1014. const detailDataWithWorkInfo: MyTaskNodeDetailVO = {
  1015. ...(data || {}),
  1016. workName: record.name || data?.workName || data?.name,
  1017. orderNo: record.orderNo || data?.orderNo,
  1018. workerUserName: record.workerUserName || data?.workerUserName,
  1019. initiatorName: data?.initiatorName || data?.initiator || record.initiatorName || record.initiator || '',
  1020. workTime: record.workTime || data?.workTime,
  1021. // 确保 type 字段存在,使用第一层 data.type
  1022. type: data?.type || data?.nodeType || '',
  1023. nodeType: data?.nodeType || data?.type || '', // 同时设置 nodeType 作为兼容
  1024. // 使用提取到的 formId
  1025. formId: extractedFormId,
  1026. };
  1027. console.log('TaskManagement: 合并后的详情数据 - type 和 formId 字段', {
  1028. 'data.type': data?.type,
  1029. 'data.nodeType': data?.nodeType,
  1030. 'data.formId': data?.formId,
  1031. 'detailDataWithWorkInfo.type': detailDataWithWorkInfo.type,
  1032. 'detailDataWithWorkInfo.nodeType': detailDataWithWorkInfo.nodeType,
  1033. 'detailDataWithWorkInfo.formId': detailDataWithWorkInfo.formId,
  1034. });
  1035. console.log('TaskManagement: 合并后的详情数据', detailDataWithWorkInfo);
  1036. console.log('TaskManagement: 节点类型', detailDataWithWorkInfo.type);
  1037. // 先设置数据,确保弹框有内容显示
  1038. setDetailData(detailDataWithWorkInfo);
  1039. console.log('TaskManagement: detailData 已设置', detailDataWithWorkInfo);
  1040. // 确保弹框是打开状态
  1041. setDetailVisible(true);
  1042. setDetailLoading(false);
  1043. console.log('TaskManagement: 弹框状态设置为 true,loading 设置为 false');
  1044. // 根据节点类型决定是否需要获取表单
  1045. // isolation、releaseIsolation 和 returnLock 不需要表单,其他类型(review 和其他)需要根据 formId 获取表单
  1046. const nodeType = detailDataWithWorkInfo.type || detailDataWithWorkInfo.nodeType || '';
  1047. const isIsolation = nodeType === 'isolation' || nodeType === '隔离' || nodeType === '隔离/方案';
  1048. const isReleaseIsolation = nodeType === 'releaseIsolation' || nodeType === '解除隔离';
  1049. const isReturnLock = nodeType === 'returnLock';
  1050. const isolationType = String(detailDataWithWorkInfo?.isolationType ?? '').trim();
  1051. const isBlindOrDismantle = isolationType === '0' || isolationType === '2'; // 0=盲板 2=拆除
  1052. // 检查任务状态是否为"已通过"
  1053. const isApproved = detailDataWithWorkInfo.approvalStatus === 'approved';
  1054. // 获取 formId(可能为 0,所以需要明确检查 undefined 和 null)
  1055. const formId = detailDataWithWorkInfo.formId;
  1056. // formId 可能是数字(包括 0)或字符串,只要不是 undefined、null 或空字符串,就认为有 formId
  1057. const hasFormId = formId !== undefined && formId !== null && (
  1058. typeof formId === 'number' ||
  1059. (typeof formId === 'string' && formId !== '' && formId.trim() !== '')
  1060. );
  1061. // 检查是否有 formData(已提交的表单数据)
  1062. const hasFormData = detailDataWithWorkInfo.formData && (
  1063. (typeof detailDataWithWorkInfo.formData === 'string' && detailDataWithWorkInfo.formData.trim() !== '') ||
  1064. (typeof detailDataWithWorkInfo.formData === 'object' && Object.keys(detailDataWithWorkInfo.formData).length > 0)
  1065. );
  1066. console.log('TaskManagement: 表单获取判断', {
  1067. nodeType,
  1068. isIsolation,
  1069. isReleaseIsolation,
  1070. isReturnLock,
  1071. isApproved,
  1072. hasFormData,
  1073. formId,
  1074. formIdType: typeof formId,
  1075. hasFormId,
  1076. '从formData获取': isApproved && hasFormData,
  1077. '从formId获取': !isIsolation && !isReleaseIsolation && !isReturnLock && hasFormId && !(isApproved && hasFormData),
  1078. 'detailDataWithWorkInfo': detailDataWithWorkInfo,
  1079. });
  1080. // 当前节点无表单(无 formId 且不会从 formData 解析出表单)时清空表单 state,避免完成/结束节点误展示上一节点的自定义表单
  1081. if (!hasFormId && !(isApproved && hasFormData)) {
  1082. setFormData({ rule: [], option: {} });
  1083. setOriginalFields([]);
  1084. setOriginalConf('');
  1085. }
  1086. // 打开盲板/拆除详情时先清空设备编号和附件,后续若有 formData 再回填
  1087. if (isIsolation || isReleaseIsolation) {
  1088. const isolationType = String(detailDataWithWorkInfo.isolationType ?? '').trim();
  1089. if (isolationType === '0' || isolationType === '2') {
  1090. setIsolationDeviceNumber('');
  1091. setIsolationFileList([]);
  1092. }
  1093. }
  1094. // 如果任务状态为"已通过"且有 formData,则从 formData 中解析表单结构
  1095. if (isApproved && hasFormData) {
  1096. console.log('TaskManagement: ✅ 任务已通过,从 formData 中解析表单结构');
  1097. setFormLoading(true);
  1098. try {
  1099. // 解析 formData(可能是 JSON 字符串或对象)
  1100. let parsedFormData: any;
  1101. if (typeof detailDataWithWorkInfo.formData === 'string') {
  1102. parsedFormData = JSON.parse(detailDataWithWorkInfo.formData);
  1103. } else {
  1104. parsedFormData = detailDataWithWorkInfo.formData;
  1105. }
  1106. console.log('TaskManagement: 解析后的 formData', parsedFormData);
  1107. // 从 formData 中提取 conf 和 fields
  1108. const conf = parsedFormData.conf;
  1109. const fields = parsedFormData.fields;
  1110. if (conf && fields) {
  1111. // 保存原始的 conf 和 fields(JSON 字符串格式)
  1112. const confString = typeof conf === 'string' ? conf : JSON.stringify(conf);
  1113. const fieldsArray = Array.isArray(fields) ? fields : [];
  1114. const fieldsStringArray = fieldsArray.map((field: any) => {
  1115. return typeof field === 'string' ? field : JSON.stringify(field);
  1116. });
  1117. setOriginalConf(confString);
  1118. setOriginalFields(fieldsStringArray);
  1119. console.log('TaskManagement: 从 formData 保存原始表单数据', {
  1120. conf: confString,
  1121. fieldsCount: fieldsStringArray.length,
  1122. fields: fieldsStringArray
  1123. });
  1124. // 解析 fields 中的每个字段,提取 value 值用于回显
  1125. const formValues: any = {};
  1126. const fieldTypeMap: { [key: string]: string } = {}; // 存储字段名到类型的映射
  1127. // 先解析所有字段,建立字段类型映射
  1128. fieldsStringArray.forEach((fieldString: string) => {
  1129. try {
  1130. const fieldObj = typeof fieldString === 'string' ? JSON.parse(fieldString) : fieldString;
  1131. const fieldName = fieldObj.name || fieldObj.field;
  1132. if (fieldName) {
  1133. fieldTypeMap[fieldName] = fieldObj.type;
  1134. }
  1135. } catch (e) {
  1136. console.error('TaskManagement: 解析字段 JSON 失败', e, fieldString);
  1137. }
  1138. });
  1139. // 提取值并转换日期类型
  1140. fieldsStringArray.forEach((fieldString: string) => {
  1141. try {
  1142. const fieldObj = typeof fieldString === 'string' ? JSON.parse(fieldString) : fieldString;
  1143. const fieldName = fieldObj.name || fieldObj.field;
  1144. if (fieldName && fieldObj.value !== undefined && fieldObj.value !== null) {
  1145. const fieldType = fieldTypeMap[fieldName];
  1146. let value = fieldObj.value;
  1147. // 如果是日期类型,转换为 dayjs 对象
  1148. if (fieldType === 'date' || fieldType === 'datetime') {
  1149. if (typeof value === 'string' && value.trim() !== '') {
  1150. const dayjsValue = dayjs(value);
  1151. formValues[fieldName] = dayjsValue.isValid() ? dayjsValue : undefined;
  1152. } else {
  1153. formValues[fieldName] = undefined;
  1154. }
  1155. } else if (fieldType === 'daterange') {
  1156. // 日期范围类型,转换为 dayjs 数组
  1157. if (Array.isArray(value) && value.length === 2) {
  1158. const [start, end] = value;
  1159. const startDayjs = typeof start === 'string' && start ? dayjs(start) : null;
  1160. const endDayjs = typeof end === 'string' && end ? dayjs(end) : null;
  1161. if (startDayjs && startDayjs.isValid() && endDayjs && endDayjs.isValid()) {
  1162. formValues[fieldName] = [startDayjs, endDayjs];
  1163. } else {
  1164. formValues[fieldName] = undefined;
  1165. }
  1166. } else {
  1167. formValues[fieldName] = undefined;
  1168. }
  1169. } else {
  1170. // 其他类型直接使用原值
  1171. formValues[fieldName] = value;
  1172. }
  1173. }
  1174. } catch (e) {
  1175. console.error('TaskManagement: 解析字段 JSON 失败', e, fieldString);
  1176. }
  1177. });
  1178. console.log('TaskManagement: 从 formData 提取的表单值', formValues);
  1179. // 设置表单配置和字段
  1180. setConfAndFields2(setFormData, conf, fields);
  1181. console.log('TaskManagement: 表单配置已设置(从 formData)');
  1182. // 转换日期值为 dayjs 对象
  1183. const convertedFormValues = convertDateValues(formValues, fieldsStringArray);
  1184. // 回填表单值
  1185. setTimeout(() => {
  1186. detailForm.setFieldsValue(convertedFormValues);
  1187. console.log('TaskManagement: 表单数据已回填(从 formData)', convertedFormValues);
  1188. }, 100);
  1189. // 有 conf/fields 时也回显 deviceNumber、attachments(解除隔离/隔离方案完成查看详情);attachments 可能是 JSON 字符串
  1190. const normAttachments = (att: any): any[] => {
  1191. if (att == null) return [];
  1192. if (Array.isArray(att)) return att;
  1193. if (typeof att === 'string' && att.trim()) { try { const p = JSON.parse(att); return Array.isArray(p) ? p : []; } catch { return []; } }
  1194. return [];
  1195. };
  1196. const listInConf = normAttachments(parsedFormData.attachments).length ? normAttachments(parsedFormData.attachments) : normAttachments((detailDataWithWorkInfo as any).attachments);
  1197. const deviceNumInConf = parsedFormData.deviceNumber ?? (detailDataWithWorkInfo as any).deviceNumber;
  1198. const hasDeviceInConf = deviceNumInConf !== undefined && deviceNumInConf !== null;
  1199. if (hasDeviceInConf || listInConf.length > 0) {
  1200. setIsolationDeviceNumber(deviceNumInConf ?? '');
  1201. setIsolationFileList(
  1202. listInConf.map((item: any, idx: number) => ({
  1203. uid: `echo-${idx}-${item.url || item.name || idx}`,
  1204. name: item.name || `文件${idx + 1}`,
  1205. url: item.url || item.response,
  1206. status: 'done',
  1207. response: item.url || item.response,
  1208. }))
  1209. );
  1210. console.log('TaskManagement: 盲板/拆除 deviceNumber/attachments 已回显', { deviceNumber: parsedFormData.deviceNumber, attachmentsCount: listInConf.length });
  1211. }
  1212. } else if (
  1213. parsedFormData.deviceNumber !== undefined ||
  1214. (() => { const a = parsedFormData.attachments; if (a == null) return false; if (Array.isArray(a)) return a.length > 0; if (typeof a === 'string' && a.trim()) { try { const p = JSON.parse(a); return Array.isArray(p) && p.length > 0; } catch { return false; } } return false; })()
  1215. ) {
  1216. // 盲板/拆除 仅 deviceNumber/attachments 无 conf/fields 时的回显
  1217. const listElse = (() => {
  1218. const a = parsedFormData.attachments;
  1219. if (a == null) return [];
  1220. if (Array.isArray(a)) return a;
  1221. if (typeof a === 'string' && a.trim()) { try { const p = JSON.parse(a); return Array.isArray(p) ? p : []; } catch { return []; } }
  1222. return [];
  1223. })();
  1224. setIsolationDeviceNumber(parsedFormData.deviceNumber ?? '');
  1225. setIsolationFileList(
  1226. listElse.map((item: any, idx: number) => ({
  1227. uid: `echo-${idx}-${item.url || item.name || idx}`,
  1228. name: item.name || `文件${idx + 1}`,
  1229. url: item.url || item.response,
  1230. status: 'done',
  1231. response: item.url || item.response,
  1232. }))
  1233. );
  1234. console.log('TaskManagement: 盲板/拆除 formData 已回显', { deviceNumber: parsedFormData.deviceNumber, attachmentsCount: listElse.length });
  1235. } else {
  1236. // 完成/结束等节点可能无表单内容,不提示“表单数据不完整”,有则显示、无则不显示即可
  1237. console.warn('TaskManagement: formData 中缺少 conf 或 fields', { conf: !!conf, fields: !!fields });
  1238. }
  1239. } catch (e) {
  1240. console.error('TaskManagement: 解析 formData 失败', e);
  1241. message.error('解析表单数据失败: ' + (e instanceof Error ? e.message : String(e)));
  1242. } finally {
  1243. setFormLoading(false);
  1244. }
  1245. }
  1246. // 有 formId 时从接口获取表单:① 审核等非隔离节点 ② 盲板/拆除节点(隔离或解除隔离且隔离方式为盲板或拆除)
  1247. else if (hasFormId && (
  1248. (!isIsolation && !isReleaseIsolation && !isReturnLock) ||
  1249. ((isIsolation || isReleaseIsolation) && isBlindOrDismantle)
  1250. )) {
  1251. console.log('TaskManagement: ✅ 满足条件,开始获取表单', { formId, nodeType, isBlindOrDismantle });
  1252. // 重置表单数据
  1253. setFormData({ rule: [], option: {} });
  1254. setOriginalFields([]);
  1255. setOriginalConf('');
  1256. setFormLoading(true);
  1257. // 立即调用接口,不使用 setTimeout,确保接口被调用
  1258. (async () => {
  1259. try {
  1260. // 确保 formId 是数字类型
  1261. const numericFormId = typeof formId === 'string' ? parseInt(formId, 10) : formId;
  1262. if (isNaN(numericFormId)) {
  1263. console.error('TaskManagement: formId 不是有效数字', formId);
  1264. message.error('表单ID无效');
  1265. setFormLoading(false);
  1266. return;
  1267. }
  1268. console.log('TaskManagement: 🚀 开始调用表单接口', { formId: numericFormId, nodeType, url: `/bpm/form/get?id=${numericFormId}` });
  1269. const FormApi = await import('../api/bpm/form');
  1270. const formDetailResponse = await FormApi.getForm(numericFormId);
  1271. console.log('TaskManagement: ✅ 表单接口调用成功,原始响应', formDetailResponse);
  1272. // 处理响应数据(可能包含 code 和 data 包装)
  1273. let formDetail: any = formDetailResponse;
  1274. if (formDetailResponse && typeof formDetailResponse === 'object' && 'data' in formDetailResponse) {
  1275. // 如果响应有 data 字段,使用 data
  1276. formDetail = (formDetailResponse as any).data || formDetailResponse;
  1277. }
  1278. console.log('TaskManagement: 处理后的表单详情', formDetail);
  1279. // 获取 conf 和 fields
  1280. const conf = formDetail?.conf || formDetail?.formConfig;
  1281. const fields = formDetail?.fields || formDetail?.formFields;
  1282. console.log('TaskManagement: 表单配置和字段', {
  1283. hasConf: !!conf,
  1284. confType: typeof conf,
  1285. hasFields: !!fields,
  1286. fieldsType: Array.isArray(fields) ? 'array' : typeof fields,
  1287. fieldsLength: Array.isArray(fields) ? fields.length : 0
  1288. });
  1289. // 解析表单配置和字段
  1290. if (conf && fields) {
  1291. // 保存原始的 conf 和 fields(JSON 字符串格式)
  1292. const confString = typeof conf === 'string' ? conf : JSON.stringify(conf);
  1293. const fieldsArray = Array.isArray(fields) ? fields : [];
  1294. const fieldsStringArray = fieldsArray.map((field: any) => {
  1295. return typeof field === 'string' ? field : JSON.stringify(field);
  1296. });
  1297. setOriginalConf(confString);
  1298. setOriginalFields(fieldsStringArray);
  1299. console.log('TaskManagement: 保存原始表单数据', {
  1300. conf: confString,
  1301. fieldsCount: fieldsStringArray.length,
  1302. fields: fieldsStringArray
  1303. });
  1304. // setConfAndFields2 会自动处理 JSON 字符串解析
  1305. setConfAndFields2(setFormData, conf, fields);
  1306. console.log('TaskManagement: 表单配置已设置', {
  1307. conf: typeof conf === 'string' ? 'JSON字符串' : '对象',
  1308. fieldsCount: Array.isArray(fields) ? fields.length : 0
  1309. });
  1310. // 仅任务已完成(approved)时回显表单数据;进行中一律不回显并清空表单
  1311. const shouldEchoFormData = detailDataWithWorkInfo.formData && isApproved;
  1312. if (shouldEchoFormData) {
  1313. try {
  1314. let formValues = typeof detailDataWithWorkInfo.formData === 'string'
  1315. ? JSON.parse(detailDataWithWorkInfo.formData)
  1316. : detailDataWithWorkInfo.formData;
  1317. // 转换日期值为 dayjs 对象
  1318. const fieldsArray = Array.isArray(fields) ? fields : [];
  1319. formValues = convertDateValues(formValues, fieldsArray);
  1320. // 清理无效的日期值,确保所有日期字段都是有效的 dayjs 对象或 undefined
  1321. const cleanedFormValues: any = {};
  1322. Object.keys(formValues).forEach(key => {
  1323. const value = formValues[key];
  1324. const field = fieldsArray.find((f: any) => {
  1325. const fieldObj = typeof f === 'string' ? JSON.parse(f) : f;
  1326. return (fieldObj.name || fieldObj.field) === key;
  1327. });
  1328. if (field) {
  1329. const fieldObj = typeof field === 'string' ? JSON.parse(field) : field;
  1330. const fieldType = fieldObj.type;
  1331. if (fieldType === 'date' || fieldType === 'datetime' || fieldType === 'timepicker') {
  1332. // 日期、日期时间或时间选择器类型:确保是有效的 dayjs 对象或 undefined
  1333. if (isValidDayjs(value)) {
  1334. // 已经是有效的 dayjs 对象
  1335. cleanedFormValues[key] = value;
  1336. } else if (value !== null && value !== undefined) {
  1337. // 尝试转换
  1338. const dayjsValue = safeToDayjs(value);
  1339. cleanedFormValues[key] = dayjsValue || undefined;
  1340. } else {
  1341. cleanedFormValues[key] = undefined;
  1342. }
  1343. } else if (fieldType === 'daterange') {
  1344. // 日期范围类型:确保数组中的每个元素都是有效的 dayjs 对象
  1345. if (Array.isArray(value) && value.length === 2) {
  1346. const [start, end] = value;
  1347. // 验证开始时间
  1348. let startDayjs: dayjs.Dayjs | null = null;
  1349. if (isValidDayjs(start)) {
  1350. startDayjs = start;
  1351. } else {
  1352. startDayjs = safeToDayjs(start);
  1353. }
  1354. // 验证结束时间
  1355. let endDayjs: dayjs.Dayjs | null = null;
  1356. if (isValidDayjs(end)) {
  1357. endDayjs = end;
  1358. } else {
  1359. endDayjs = safeToDayjs(end);
  1360. }
  1361. cleanedFormValues[key] = (startDayjs && endDayjs) ? [startDayjs, endDayjs] : undefined;
  1362. } else {
  1363. cleanedFormValues[key] = undefined;
  1364. }
  1365. } else {
  1366. // 其他类型直接使用
  1367. cleanedFormValues[key] = value;
  1368. }
  1369. } else {
  1370. // 没有找到字段定义,直接使用原值
  1371. cleanedFormValues[key] = value;
  1372. }
  1373. });
  1374. // 延迟一下,确保表单字段已经渲染
  1375. setTimeout(() => {
  1376. try {
  1377. detailForm.setFieldsValue(cleanedFormValues);
  1378. console.log('TaskManagement: 表单数据已回填', cleanedFormValues);
  1379. } catch (e) {
  1380. console.error('TaskManagement: 设置表单值失败', e);
  1381. // 如果设置失败,尝试只设置非日期字段
  1382. const safeValues: any = {};
  1383. Object.keys(cleanedFormValues).forEach(key => {
  1384. const value = cleanedFormValues[key];
  1385. const field = fieldsArray.find((f: any) => {
  1386. const fieldObj = typeof f === 'string' ? JSON.parse(f) : f;
  1387. return (fieldObj.name || fieldObj.field) === key;
  1388. });
  1389. if (field) {
  1390. const fieldObj = typeof field === 'string' ? JSON.parse(field) : field;
  1391. const fieldType = fieldObj.type;
  1392. if (fieldType !== 'date' && fieldType !== 'datetime' && fieldType !== 'daterange' && fieldType !== 'timepicker') {
  1393. safeValues[key] = value;
  1394. }
  1395. } else {
  1396. safeValues[key] = value;
  1397. }
  1398. });
  1399. detailForm.setFieldsValue(safeValues);
  1400. }
  1401. }, 100);
  1402. } catch (e) {
  1403. console.error('TaskManagement: 解析表单数据失败', e);
  1404. }
  1405. } else if (!isApproved) {
  1406. // 任务进行中:清空自定义表单,避免带出历史或隔离方案等填写内容
  1407. setTimeout(() => { try { detailForm.resetFields(); } catch (_) { /* ignore */ } }, 100);
  1408. }
  1409. // 隔离/解除隔离完成查看详情:从 formData 或节点详情顶层回显 deviceNumber、attachments(部分解除隔离节点仅顶层有数据)
  1410. if (isApproved && (isIsolation || isReleaseIsolation)) {
  1411. try {
  1412. const normAtt = (att: any): any[] => {
  1413. if (att == null) return []; if (Array.isArray(att)) return att;
  1414. if (typeof att === 'string' && att.trim()) { try { const p = JSON.parse(att); return Array.isArray(p) ? p : []; } catch { return []; } } return [];
  1415. };
  1416. let deviceNum: any;
  1417. let listF: any[];
  1418. if (detailDataWithWorkInfo.formData) {
  1419. const parsed = typeof detailDataWithWorkInfo.formData === 'string'
  1420. ? JSON.parse(detailDataWithWorkInfo.formData)
  1421. : detailDataWithWorkInfo.formData;
  1422. listF = normAtt(parsed.attachments).length ? normAtt(parsed.attachments) : normAtt((detailDataWithWorkInfo as any).attachments);
  1423. deviceNum = parsed.deviceNumber ?? (detailDataWithWorkInfo as any).deviceNumber;
  1424. } else {
  1425. deviceNum = (detailDataWithWorkInfo as any).deviceNumber;
  1426. listF = normAtt((detailDataWithWorkInfo as any).attachments);
  1427. }
  1428. const hasDev = deviceNum !== undefined && deviceNum !== null;
  1429. if (hasDev || listF.length > 0) {
  1430. setIsolationDeviceNumber(deviceNum ?? '');
  1431. setIsolationFileList(
  1432. listF.map((item: any, idx: number) => ({
  1433. uid: `echo-${idx}-${item.url || item.name || idx}`,
  1434. name: item.name || `文件${idx + 1}`,
  1435. url: item.url || item.response,
  1436. status: 'done',
  1437. response: item.url || item.response,
  1438. }))
  1439. );
  1440. console.log('TaskManagement: formId 分支 deviceNumber/attachments 已回显', { deviceNumber: deviceNum, attachmentsCount: listF.length });
  1441. }
  1442. } catch (_) { /* ignore */ }
  1443. }
  1444. } else {
  1445. console.warn('TaskManagement: 表单详情缺少配置或字段', { conf: !!conf, fields: !!fields, formDetail });
  1446. message.warning('表单配置不完整');
  1447. }
  1448. } catch (e) {
  1449. console.error('TaskManagement: 获取表单详情失败', e);
  1450. message.error('获取表单详情失败: ' + (e instanceof Error ? e.message : String(e)));
  1451. // 即使获取表单失败,也继续显示弹框
  1452. } finally {
  1453. setFormLoading(false);
  1454. }
  1455. })();
  1456. } else {
  1457. setFormLoading(false);
  1458. // 隔离/解除隔离已完成但未走 formData 或 formId 分支时,从节点详情顶层回显 deviceNumber、attachments
  1459. if (isApproved && (isIsolation || isReleaseIsolation)) {
  1460. try {
  1461. const normAtt = (att: any): any[] => {
  1462. if (att == null) return []; if (Array.isArray(att)) return att;
  1463. if (typeof att === 'string' && att.trim()) { try { const p = JSON.parse(att); return Array.isArray(p) ? p : []; } catch { return []; } } return [];
  1464. };
  1465. const deviceNum = (detailDataWithWorkInfo as any).deviceNumber;
  1466. const listF = normAtt((detailDataWithWorkInfo as any).attachments);
  1467. if ((deviceNum !== undefined && deviceNum !== null) || listF.length > 0) {
  1468. setIsolationDeviceNumber(deviceNum ?? '');
  1469. setIsolationFileList(
  1470. listF.map((item: any, idx: number) => ({
  1471. uid: `echo-${idx}-${item.url || item.name || idx}`,
  1472. name: item.name || `文件${idx + 1}`,
  1473. url: item.url || item.response,
  1474. status: 'done',
  1475. response: item.url || item.response,
  1476. }))
  1477. );
  1478. console.log('TaskManagement: 隔离/解除隔离(无formId分支) deviceNumber/attachments 已从顶层回显', { deviceNumber: deviceNum, attachmentsCount: listF.length });
  1479. }
  1480. } catch (_) { /* ignore */ }
  1481. }
  1482. if (isIsolation || isReleaseIsolation || isReturnLock) {
  1483. console.log('TaskManagement: ⚠️ 隔离类型节点,跳过表单配置获取', { nodeType, isIsolation, isReleaseIsolation, isReturnLock });
  1484. } else if (!hasFormId) {
  1485. console.warn('TaskManagement: ⚠️ 没有表单ID,跳过表单配置获取', {
  1486. formId,
  1487. formIdType: typeof formId,
  1488. hasFormId,
  1489. nodeType,
  1490. 'detailDataWithWorkInfo': detailDataWithWorkInfo
  1491. });
  1492. // 不显示提示,静默处理
  1493. } else {
  1494. console.warn('TaskManagement: ⚠️ 未知原因未获取表单', {
  1495. nodeType,
  1496. isIsolation,
  1497. isReleaseIsolation,
  1498. isReturnLock,
  1499. hasFormId,
  1500. formId
  1501. });
  1502. }
  1503. }
  1504. // 注意:表单数据回填已移到表单配置加载完成后,确保表单字段已渲染
  1505. // 回显审核意见(如果有 approvalOpinion 字段,且不是 "pending")
  1506. if (detailDataWithWorkInfo.approvalOpinion && detailDataWithWorkInfo.approvalOpinion !== 'pending') {
  1507. setApprovalComment(detailDataWithWorkInfo.approvalOpinion);
  1508. console.log('TaskManagement: 回显审核意见', detailDataWithWorkInfo.approvalOpinion);
  1509. } else {
  1510. setApprovalComment(''); // 重置审核意见(包括 pending 的情况)
  1511. }
  1512. setDetailLoading(false);
  1513. console.log('TaskManagement: 弹框已打开', { detailVisible: true, detailData: detailDataWithWorkInfo });
  1514. } catch (error: any) {
  1515. console.error('TaskManagement: 获取节点详情失败', error);
  1516. toast.error(error.message || '获取节点详情失败');
  1517. // 即使接口失败,也显示弹框(显示错误信息)
  1518. if (!detailData) {
  1519. setDetailData({
  1520. id: record.nodeId,
  1521. nodeId: record.nodeId,
  1522. workId: record.workId,
  1523. workName: record.name,
  1524. orderNo: record.orderNo,
  1525. workerUserName: record.workerUserName,
  1526. workTime: record.workTime,
  1527. type: '',
  1528. } as MyTaskNodeDetailVO);
  1529. }
  1530. setDetailVisible(true);
  1531. } finally {
  1532. setDetailLoading(false);
  1533. }
  1534. };
  1535. // 获取任务状态显示文本(使用 approval_status 字典)
  1536. const getTaskStatusText = (status: string | number | undefined): string => {
  1537. if (!status) return '未知';
  1538. const statusStr = String(status).toLowerCase();
  1539. const statusItem = approvalStatusDictList.find(item => String(item.value).toLowerCase() === statusStr);
  1540. if (statusItem) {
  1541. return statusItem.label || statusItem.name || '未知';
  1542. }
  1543. // 如果没有找到字典值,使用默认映射
  1544. const statusMap: Record<string, string> = {
  1545. 'pending': '待审核',
  1546. 'approved': '已通过',
  1547. 'rejected': '已驳回',
  1548. 'unaudited': '未审核',
  1549. };
  1550. return statusMap[statusStr] || '未知';
  1551. };
  1552. // 获取任务状态样式(审批状态)
  1553. const getTaskStatusStyle = (status: string | number | undefined): React.CSSProperties => {
  1554. if (!status) {
  1555. return {
  1556. backgroundColor: '#e5e5e5',
  1557. color: '#333333',
  1558. };
  1559. }
  1560. const statusStr = String(status).toLowerCase();
  1561. // 先获取状态文本,根据文本判断颜色
  1562. const statusText = getTaskStatusText(status);
  1563. const statusTextLower = statusText.toLowerCase();
  1564. // 根据状态文本判断颜色
  1565. // 待执行、待发布:灰色 #e5e5e5
  1566. if (statusTextLower.includes('待执行') || statusTextLower.includes('待发布') ||
  1567. statusTextLower.includes('未开始') || statusTextLower.includes('未审核') ||
  1568. statusTextLower.includes('待开始') || statusTextLower.includes('unaudited')) {
  1569. return {
  1570. backgroundColor: '#e5e5e5',
  1571. color: '#333333',
  1572. };
  1573. }
  1574. // 进行中:蓝色 #1677ff
  1575. if (statusTextLower.includes('进行中') || statusTextLower.includes('待审核') ||
  1576. statusTextLower.includes('pending') || statusTextLower.includes('执行中')) {
  1577. return {
  1578. backgroundColor: '#1677ff',
  1579. color: '#ffffff',
  1580. };
  1581. }
  1582. // 已完成:深绿色 #15803d
  1583. if (statusTextLower.includes('已完成') || statusTextLower.includes('已通过') ||
  1584. statusTextLower.includes('approved') || statusTextLower.includes('完成') ||
  1585. statusTextLower.includes('执行完成')) {
  1586. return {
  1587. backgroundColor: '#15803d',
  1588. color: '#ffffff',
  1589. };
  1590. }
  1591. // 如果没有匹配到,默认灰色
  1592. return {
  1593. backgroundColor: '#e5e5e5',
  1594. color: '#333333',
  1595. };
  1596. };
  1597. // 作业状态文案(workDetail.status:作业整体状态,与任务状态 approvalStatus/nodeProgress 不同)
  1598. const getWorkStatusText = (status: string | number | undefined): string => {
  1599. if (status == null || String(status).trim() === '') return t('workJobDetail.statusUnknown');
  1600. const s = String(status).toLowerCase();
  1601. // 后端可能直接返回「执行完成」「已完成」等中文文案,这里统一映射为「已完成」
  1602. if (s.includes('执行完成') || s.includes('已完成') || s === 'completed') {
  1603. return '已完成';
  1604. }
  1605. const map: Record<string, string> = {
  1606. pending: t('workJobDetail.statusPending'),
  1607. running: t('workJobDetail.statusRunning'),
  1608. completed: t('workJobDetail.statusCompleted'),
  1609. rejected: t('workJobDetail.statusRejected'),
  1610. skipped: t('workJobDetail.statusSkipped'),
  1611. cancelled: t('workJobDetail.statusCancelled'),
  1612. };
  1613. return map[s] ?? String(status) ?? t('workJobDetail.statusUnknown');
  1614. };
  1615. const getWorkStatusStyle = (status: string | number | undefined): React.CSSProperties => {
  1616. const s = String(status || '').toLowerCase();
  1617. if (['running', '2'].includes(s)) return { backgroundColor: '#1677ff', color: '#fff' };
  1618. if (['completed', 'approved'].includes(s)) return { backgroundColor: '#15803d', color: '#fff' };
  1619. if (['rejected', 'cancelled'].includes(s)) return { backgroundColor: '#ff4d4f', color: '#fff' };
  1620. if (['pending', '1'].includes(s)) return { backgroundColor: '#e5e5e5', color: '#333' };
  1621. return { backgroundColor: '#e2e8f0', color: '#64748b' };
  1622. };
  1623. // 获取紧急程度样式
  1624. // 获取紧急程度图标和样式(根据字典 value 判断:0=一般,1=紧急,2=非常紧急)
  1625. const getUrgencyLevelIconAndStyle = (urgencyValue: string | number | undefined): { icon: React.ReactNode; style: React.CSSProperties } => {
  1626. if (urgencyValue === null || urgencyValue === undefined || urgencyValue === '') {
  1627. return {
  1628. icon: null,
  1629. style: {
  1630. backgroundColor: '#e5e5e5',
  1631. color: '#333333',
  1632. },
  1633. };
  1634. }
  1635. const value = Number(urgencyValue);
  1636. // 0 = 一般:使用 urgecy1.png 图标 + 黑色文字
  1637. if (value === 0) {
  1638. return {
  1639. icon: (
  1640. <img
  1641. src={urgecy1Icon}
  1642. alt="一般"
  1643. className="w-5 h-5 flex-shrink-0 mr-1.5"
  1644. style={{ objectFit: 'contain' }}
  1645. />
  1646. ),
  1647. style: {
  1648. backgroundColor: 'transparent',
  1649. color: '#000000',
  1650. },
  1651. };
  1652. }
  1653. // 1 = 紧急:使用 urgecy2.png 图标 + 橙色加粗文字
  1654. if (value === 1) {
  1655. return {
  1656. icon: (
  1657. <img
  1658. src={urgecy2Icon}
  1659. alt="紧急"
  1660. className="w-5 h-5 flex-shrink-0 mr-1.5"
  1661. style={{ objectFit: 'contain' }}
  1662. />
  1663. ),
  1664. style: {
  1665. backgroundColor: 'transparent',
  1666. color: '#fa8c16',
  1667. fontWeight: 'bold',
  1668. },
  1669. };
  1670. }
  1671. // 2 = 非常紧急:使用 urgecy3.png 图标 + 红色加粗文字
  1672. if (value === 2) {
  1673. return {
  1674. icon: (
  1675. <img
  1676. src={urgecy3Icon}
  1677. alt="非常紧急"
  1678. className="w-5 h-5 flex-shrink-0 mr-1.5"
  1679. style={{ objectFit: 'contain' }}
  1680. />
  1681. ),
  1682. style: {
  1683. backgroundColor: 'transparent',
  1684. color: '#ff4d4f',
  1685. fontWeight: 'bold',
  1686. },
  1687. };
  1688. }
  1689. // 如果没有匹配到,默认灰色
  1690. return {
  1691. icon: null,
  1692. style: {
  1693. backgroundColor: '#e5e5e5',
  1694. color: '#333333',
  1695. },
  1696. };
  1697. };
  1698. // 获取紧急程度样式(保留用于向后兼容)
  1699. const getUrgencyLevelStyle = (urgencyValue: string | number | undefined): React.CSSProperties => {
  1700. return getUrgencyLevelIconAndStyle(urgencyValue).style;
  1701. };
  1702. // 表格列配置
  1703. const columns: ColumnsType<MyTaskVO> = [
  1704. {
  1705. title: t('form.taskId'),
  1706. dataIndex: 'orderNo',
  1707. width: 180,
  1708. align: 'center',
  1709. render: (text: string) => text || '-',
  1710. },
  1711. {
  1712. title: t('form.taskName'),
  1713. dataIndex: 'name',
  1714. width: 220,
  1715. align: 'center',
  1716. ellipsis: true,
  1717. render: (text: string) => text || '-',
  1718. },
  1719. {
  1720. title: t('form.currentTask'),
  1721. dataIndex: 'currentNodeName',
  1722. width: 180,
  1723. align: 'center',
  1724. ellipsis: true,
  1725. render: (text: string, record: MyTaskVO) => {
  1726. const displayText = text || record.currentNode || '-';
  1727. if (!displayText || displayText === '-') return displayText;
  1728. return (
  1729. <span
  1730. className="cursor-pointer hover:underline"
  1731. style={{ color: '#1677ff' }}
  1732. onMouseEnter={(e) => {
  1733. e.currentTarget.style.textDecoration = 'underline';
  1734. }}
  1735. onMouseLeave={(e) => {
  1736. e.currentTarget.style.textDecoration = 'none';
  1737. }}
  1738. onClick={(e) => {
  1739. e.stopPropagation();
  1740. e.preventDefault();
  1741. handleViewDetail(record);
  1742. }}
  1743. title={displayText}
  1744. >
  1745. {displayText}
  1746. </span>
  1747. );
  1748. },
  1749. },
  1750. {
  1751. title: t('form.taskStatus'),
  1752. dataIndex: 'approvalStatus',
  1753. width: 120,
  1754. align: 'center',
  1755. render: (status: string | number | undefined, record: MyTaskVO) => {
  1756. // 优先使用 approvalStatus,如果没有则使用其他状态字段
  1757. const taskStatus = status || record.taskStatus || record.status;
  1758. // 列表里“已完成/已通过”使用浅绿,弹框/详情仍保持深绿
  1759. const baseStyle = getTaskStatusStyle(taskStatus);
  1760. const statusTextLower = String(getTaskStatusText(taskStatus) || '').toLowerCase();
  1761. const listStyle =
  1762. statusTextLower.includes('已完成') ||
  1763. statusTextLower.includes('已通过') ||
  1764. statusTextLower.includes('approved') ||
  1765. statusTextLower.includes('完成') ||
  1766. statusTextLower.includes('执行完成')
  1767. ? { ...baseStyle, backgroundColor: '#0acb57', color: '#ffffff' }
  1768. : baseStyle;
  1769. return (
  1770. <span
  1771. className="inline-flex px-3 py-1 rounded-lg text-xs"
  1772. style={listStyle}
  1773. >
  1774. {getTaskStatusText(taskStatus)}
  1775. </span>
  1776. );
  1777. },
  1778. },
  1779. {
  1780. title: t('form.responsiblePerson'),
  1781. dataIndex: 'workerUserName',
  1782. width: 150,
  1783. align: 'center',
  1784. render: (text: string, record: MyTaskVO) => {
  1785. // 优先使用 workerUserName,如果没有则尝试从其他字段获取
  1786. return text || record.responsibleName || record.responsible || record.initiatorName || record.initiator || '-';
  1787. },
  1788. },
  1789. {
  1790. title: t('form.taskStartTime'),
  1791. dataIndex: 'workTime',
  1792. width: 180,
  1793. align: 'center',
  1794. render: (text: string | number | Date | undefined, record: MyTaskVO) => {
  1795. // 优先使用 workTime,如果没有则使用其他时间字段
  1796. const time = text || record.taskStartTime || record.initiationTime || record.initiateTime;
  1797. return time ? dateFormatter(time) : '-';
  1798. },
  1799. },
  1800. {
  1801. title: t('form.urgencyLevel'),
  1802. dataIndex: 'urgencyLevel',
  1803. width: 120,
  1804. align: 'center',
  1805. render: (urgencyLevel: string | number | undefined, record: MyTaskVO) => {
  1806. const urgencyValue = urgencyLevel || record.urgencyLevel;
  1807. const urgencyItem = urgencyLevelDictList.find(item => String(item.value) === String(urgencyValue));
  1808. const urgencyText = urgencyItem ? (urgencyItem.label || '') : (urgencyValue ? String(urgencyValue) : '-');
  1809. if (!urgencyValue || urgencyValue === null || urgencyValue === undefined || urgencyValue === '') {
  1810. return <span>-</span>;
  1811. }
  1812. const { icon, style } = getUrgencyLevelIconAndStyle(urgencyValue);
  1813. return (
  1814. <span
  1815. className="inline-flex items-center justify-center gap-1.5"
  1816. >
  1817. {icon}
  1818. <span style={style}>{urgencyText}</span>
  1819. </span>
  1820. );
  1821. },
  1822. },
  1823. {
  1824. title: t('common.operation'),
  1825. width: 120,
  1826. align: 'center',
  1827. fixed: 'right',
  1828. render: (_: any, record: MyTaskVO) => {
  1829. // 获取任务状态文本
  1830. const statusText = getTaskStatusText(record.approvalStatus);
  1831. const statusTextLower = statusText.toLowerCase();
  1832. // 判断是否为进行中状态
  1833. const isInProgress = statusTextLower.includes('进行中') ||
  1834. statusTextLower.includes('待审核') ||
  1835. statusTextLower.includes('pending') ||
  1836. statusTextLower.includes('执行中');
  1837. // 判断是否为已完成状态
  1838. const isCompleted = statusTextLower.includes('已完成') ||
  1839. statusTextLower.includes('已通过') ||
  1840. statusTextLower.includes('approved') ||
  1841. statusTextLower.includes('完成') ||
  1842. statusTextLower.includes('执行完成');
  1843. // 根据状态显示不同的文字
  1844. const buttonText = isInProgress ? t('form.handleNow') : isCompleted ? t('form.viewDetail') : t('form.viewDetail');
  1845. return (
  1846. <Space size="small">
  1847. <Button
  1848. type="link"
  1849. size="small"
  1850. icon={<Eye className="w-4 h-4" style={{ color: '#000000' }} />}
  1851. onClick={(e) => {
  1852. e.stopPropagation();
  1853. e.preventDefault();
  1854. handleViewDetail(record);
  1855. }}
  1856. style={{ color: '#000000' }}
  1857. className="transition-colors hover:text-[#1677ff] hover:underline"
  1858. onMouseEnter={(e) => {
  1859. e.currentTarget.style.color = '#1677ff';
  1860. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  1861. }}
  1862. onMouseLeave={(e) => {
  1863. e.currentTarget.style.color = '#000000';
  1864. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  1865. }}
  1866. >
  1867. {buttonText}
  1868. </Button>
  1869. </Space>
  1870. );
  1871. },
  1872. },
  1873. ];
  1874. // 计算分页数据
  1875. const totalPages = Math.ceil(total / queryParams.pageSize) || 1;
  1876. return (
  1877. <div className="space-y-6">
  1878. {/* 操作栏和表格容器 */}
  1879. <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
  1880. {/* 查询与操作栏 */}
  1881. <div className="p-4 lg:p-5 border-b border-gray-200/50">
  1882. <div className="flex flex-wrap items-center gap-2 lg:gap-3 min-w-0">
  1883. {/* 搜索条件:放宽到 280px,可缩小但不挤压不可见 */}
  1884. <div className="flex items-center gap-2 lg:gap-3 w-[280px] min-w-[180px] flex-shrink">
  1885. <span className="text-sm font-medium text-gray-700 whitespace-nowrap flex-shrink-0">{t('form.workOrderNo')}:</span>
  1886. <Input
  1887. value={searchOrderNo}
  1888. onChange={(e) => setSearchOrderNo(e.target.value)}
  1889. placeholder={t('form.workOrderNoPlaceholder')}
  1890. allowClear
  1891. className="min-w-0 w-full"
  1892. onPressEnter={handleSearch}
  1893. />
  1894. </div>
  1895. <div className="flex items-center gap-2 lg:gap-3 w-[280px] min-w-[180px] flex-shrink">
  1896. <span className="text-sm font-medium text-gray-700 whitespace-nowrap flex-shrink-0">{t('form.workName')}:</span>
  1897. <Input
  1898. value={searchName}
  1899. onChange={(e) => setSearchName(e.target.value)}
  1900. placeholder={t('form.workNamePlaceholder')}
  1901. allowClear
  1902. className="min-w-0 w-full"
  1903. onPressEnter={handleSearch}
  1904. />
  1905. </div>
  1906. <Space size="small">
  1907. <Button type="primary" icon={<Search className="w-4 h-4" />} onClick={handleSearch}>
  1908. {t('common.search')}
  1909. </Button>
  1910. <Button icon={<RotateCcw className="w-4 h-4" />} onClick={handleReset}>
  1911. {t('common.reset')}
  1912. </Button>
  1913. </Space>
  1914. </div>
  1915. </div>
  1916. {/* 表格容器:不设 overflow,避免出现第二层纵向滚动条,仅用 Table 的 scroll.x 做横向滚动 */}
  1917. <div className="min-w-0">
  1918. <AntdTable
  1919. loading={loading}
  1920. columns={columns}
  1921. dataSource={list}
  1922. rowKey={(record) => record.id || Math.random()}
  1923. pagination={false}
  1924. scroll={{ x: 1200 }}
  1925. locale={{
  1926. emptyText: t('form.noData'),
  1927. }}
  1928. />
  1929. </div>
  1930. </div>
  1931. {/* 分页 */}
  1932. {total > 0 && (
  1933. <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
  1934. <div className="flex items-center justify-between">
  1935. <div className="text-sm text-gray-600">
  1936. {t('form.totalRecords')} <span className="text-blue-600 font-medium">{total}</span> {t('form.recordsUnit')}
  1937. </div>
  1938. <div className="flex gap-2">
  1939. <Button
  1940. onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo - 1 })}
  1941. disabled={queryParams.pageNo <= 1}
  1942. >
  1943. {t('common.prevPage')}
  1944. </Button>
  1945. <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
  1946. {queryParams.pageNo} / {totalPages}
  1947. </span>
  1948. <Button
  1949. onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo + 1 })}
  1950. disabled={queryParams.pageNo >= totalPages}
  1951. >
  1952. {t('common.nextPage')}
  1953. </Button>
  1954. </div>
  1955. </div>
  1956. </div>
  1957. )}
  1958. {/* 详情弹框 */}
  1959. <Modal
  1960. title={null}
  1961. open={detailVisible}
  1962. onCancel={() => {
  1963. console.log('TaskManagement: 弹框关闭');
  1964. setDetailVisible(false);
  1965. setDetailData(null);
  1966. setFormData({ rule: [], option: {} });
  1967. setFormLoading(false);
  1968. setOriginalFields([]);
  1969. setOriginalConf('');
  1970. detailForm.resetFields();
  1971. setApprovalComment('');
  1972. setIsolationDeviceNumber('');
  1973. setIsolationFileList([]);
  1974. }}
  1975. footer={null}
  1976. width={1000}
  1977. destroyOnClose
  1978. confirmLoading={detailLoading}
  1979. maskClosable={false}
  1980. zIndex={1000}
  1981. styles={{
  1982. body: {
  1983. minHeight: '600px',
  1984. maxHeight: '700px',
  1985. height: '700px',
  1986. padding: 0,
  1987. display: 'flex',
  1988. flexDirection: 'column',
  1989. overflow: 'hidden'
  1990. }
  1991. }}
  1992. >
  1993. {(() => {
  1994. console.log('TaskManagement: Modal 内容渲染', { detailLoading, detailData, detailVisible });
  1995. if (detailLoading) {
  1996. return <div className="py-8 text-center">{t('form.loading')}</div>;
  1997. }
  1998. if (detailData) {
  1999. return (
  2000. <div style={{
  2001. display: 'flex',
  2002. flexDirection: 'column',
  2003. height: '100%',
  2004. overflow: 'hidden'
  2005. }}>
  2006. {/* 标题区域:左上角显示作业状态(有 workDetail 时),否则显示任务状态 */}
  2007. <div style={{
  2008. padding: '24px 28px 20px',
  2009. background: 'linear-gradient(180deg, #fafbfd 0%, #ffffff 100%)',
  2010. borderBottom: '1px solid #eef0f4',
  2011. }}>
  2012. <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
  2013. <div style={{
  2014. width: '4px',
  2015. height: '22px',
  2016. backgroundColor: '#2563eb',
  2017. borderRadius: '2px',
  2018. flexShrink: 0,
  2019. }} />
  2020. <h2 style={{ color: '#1e293b', fontSize: 20, fontWeight: 600, margin: 0, letterSpacing: '-0.02em' }}>
  2021. {detailData.workName || detailData.name || t('form.workDetail')}
  2022. </h2>
  2023. {(() => {
  2024. const workStatus = (detailData as any).workDetail?.status;
  2025. if (workStatus != null && String(workStatus).trim() !== '') {
  2026. const baseStyle = getWorkStatusStyle(workStatus);
  2027. const workText = getWorkStatusText(workStatus);
  2028. const isCompleted = String(workText || '').toLowerCase().includes('已完成');
  2029. const headerStyle = isCompleted ? { ...baseStyle, backgroundColor: '#0acb57', color: '#ffffff' } : baseStyle;
  2030. return (
  2031. <span className="inline-flex px-3 py-1 rounded-full text-xs font-medium" style={headerStyle}>
  2032. {workText}
  2033. </span>
  2034. );
  2035. }
  2036. return (
  2037. <span
  2038. className="inline-flex px-3 py-1 rounded-full text-xs font-medium"
  2039. style={(() => {
  2040. const s = detailData.approvalStatus ?? detailData.taskStatus ?? (detailData as any).nodeProgress;
  2041. const baseStyle = getTaskStatusStyle(s);
  2042. const textLower = String(getTaskStatusText(s) || '').toLowerCase();
  2043. const isCompleted =
  2044. textLower.includes('已完成') ||
  2045. textLower.includes('已通过') ||
  2046. textLower.includes('approved') ||
  2047. textLower.includes('完成') ||
  2048. textLower.includes('执行完成');
  2049. return isCompleted ? { ...baseStyle, backgroundColor: '#0acb57', color: '#ffffff' } : baseStyle;
  2050. })()}
  2051. >
  2052. {getTaskStatusText(detailData.approvalStatus ?? detailData.taskStatus ?? (detailData as any).nodeProgress)}
  2053. </span>
  2054. );
  2055. })()}
  2056. </div>
  2057. <div className="text-sm flex flex-wrap gap-y-2" style={{ color: '#64748b', marginTop: 14 }}>
  2058. <span>{t('form.workOrderNo')}:{detailData.orderNo || '-'}</span>
  2059. <span style={{ paddingLeft: 16, marginLeft: 12, borderLeft: '1px solid #e2e8f0' }}>
  2060. {t('form.workResponsible')}:{detailData.initiatorName || '-'}</span>
  2061. <span style={{ paddingLeft: 16, marginLeft: 12, borderLeft: '1px solid #e2e8f0' }}>
  2062. {t('form.taskResponsible')}:{detailData.workerUserName || '-'}</span>
  2063. <span style={{ paddingLeft: 16, marginLeft: 12, borderLeft: '1px solid #e2e8f0' }}>
  2064. {t('form.initiationTime')}:{(detailData as any).workDetail?.initiationTime != null ? dateFormatter((detailData as any).workDetail.initiationTime) : (detailData.initiationTime != null ? dateFormatter(detailData.initiationTime) : (detailData.workTime ? dateFormatter(detailData.workTime) : '-'))}
  2065. </span>
  2066. </div>
  2067. </div>
  2068. {/* 内容区域 - 可滚动 */}
  2069. <div
  2070. style={{
  2071. padding: '20px 24px 24px',
  2072. flex: 1,
  2073. overflowY: 'auto',
  2074. minHeight: 0,
  2075. background: '#f8fafc',
  2076. }}
  2077. >
  2078. {(() => {
  2079. // 检查节点状态是否为已通过(approved),如果是则禁用所有输入和按钮
  2080. const isApproved = detailData?.approvalStatus === 'approved';
  2081. // 从 detailData.type 或 detailData.nodeType 获取节点类型
  2082. const nodeType = String(detailData?.type || detailData?.nodeType || '').trim();
  2083. console.log('TaskManagement: 渲染内容区域 - 节点类型', nodeType, 'detailData:', detailData);
  2084. // 创建作业类型节点:展示作业详情(作业类型、作业名称、作业发起人、作业分类、作业内容等)
  2085. if (nodeType === 'createJob') {
  2086. const wd = (detailData as any).workDetail || {};
  2087. const getLabelFromList = (list: any[], val: any) => list?.find((d: any) => String(d.value) === String(val))?.label ?? val;
  2088. const taskStartTime = (detailData as any).startTime ?? (detailData as any).createTime ?? detailData.workTime;
  2089. const taskStatus = detailData.approvalStatus ?? (detailData as any).taskStatus ?? (detailData as any).nodeProgress;
  2090. const jobCategoryVal = (wd as any).workCategory ?? (wd as any).jobCategory ?? wd.type;
  2091. const jobCategoryLabel = jobCategoryVal != null ? (getLabelFromList(jobCategoryDictList, jobCategoryVal) || jobCategoryVal) : '-';
  2092. return (
  2093. <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
  2094. {/* 任务概览卡片 */}
  2095. <div style={{
  2096. background: '#ffffff',
  2097. borderRadius: 12,
  2098. padding: '18px 20px',
  2099. boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
  2100. border: '1px solid #eef0f4',
  2101. }}>
  2102. <div style={{ display: 'flex', flexWrap: 'wrap', gap: '16px 28px', alignItems: 'center' }}>
  2103. <span style={{ display: 'inline-flex', alignItems: 'center', fontSize: 14, color: '#64748b' }}>
  2104. 任务名称:<span style={{ color: '#1e293b', fontWeight: 600 }}>{detailData?.nodeName ?? '-'}</span>
  2105. <span
  2106. className="inline-flex px-3 py-1 rounded-full text-xs font-medium"
  2107. style={(() => {
  2108. const baseStyle = getTaskStatusStyle(taskStatus);
  2109. const textLower = String(getTaskStatusText(taskStatus) || '').toLowerCase();
  2110. const isCompleted = textLower.includes('已完成') || textLower.includes('已通过') || textLower.includes('approved') || textLower.includes('完成') || textLower.includes('执行完成');
  2111. const badgeStyle = isCompleted ? { ...baseStyle, backgroundColor: '#0acb57', color: '#ffffff', marginLeft: 10 } : { ...baseStyle, marginLeft: 10 };
  2112. return badgeStyle;
  2113. })()}
  2114. >
  2115. {getTaskStatusText(taskStatus)}
  2116. </span>
  2117. </span>
  2118. <span style={{ fontSize: 14, color: '#64748b' }}>负责人:<span style={{ color: '#1e293b', fontWeight: 600 }}>{detailData?.workerUserName ?? '-'}</span></span>
  2119. <span style={{ fontSize: 14, color: '#64748b' }}>开始时间:<span style={{ color: '#1e293b', fontWeight: 600 }}>{taskStartTime != null ? dateFormatter(taskStartTime) : '-'}</span></span>
  2120. </div>
  2121. </div>
  2122. {/* 作业属性卡片 */}
  2123. <div style={{
  2124. background: '#ffffff',
  2125. borderRadius: 12,
  2126. padding: '20px',
  2127. boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
  2128. border: '1px solid #eef0f4',
  2129. }}>
  2130. <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '20px 32px' }}>
  2131. <div><span style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>作业名称</span><div style={{ fontSize: 14, color: '#1e293b', fontWeight: 500 }}>{wd.name ?? detailData.workName ?? detailData.name ?? '-'}</div></div>
  2132. <div><span style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>作业分类</span><div style={{ fontSize: 14, color: '#1e293b', fontWeight: 500 }}>{jobCategoryLabel}</div></div>
  2133. <div><span style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>{t('workJobDetail.workflowTemplate') || '流程模板'}</span><div style={{ fontSize: 14, color: '#1e293b', fontWeight: 500 }}>{(wd as any).designName ?? '-'}</div></div>
  2134. <div><span style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>{t('workJobDetail.urgencyLevel') || '紧急程度'}</span><div style={{ fontSize: 14, color: '#1e293b', fontWeight: 500 }}>{(wd as any).urgencyLevel != null ? (getLabelFromList(urgencyLevelDictList, (wd as any).urgencyLevel) || (wd as any).urgencyLevel) : '-'}</div></div>
  2135. </div>
  2136. </div>
  2137. {/* 作业内容卡片 */}
  2138. <div style={{
  2139. background: '#ffffff',
  2140. borderRadius: 12,
  2141. padding: 20,
  2142. boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
  2143. border: '1px solid #eef0f4',
  2144. }}>
  2145. <span style={{ fontSize: 12, color: '#64748b', fontWeight: 500, display: 'block', marginBottom: 10 }}>{t('workJobDetail.jobContent') || '作业内容'}</span>
  2146. <div style={{ fontSize: 14, color: '#334155', lineHeight: 1.7, whiteSpace: 'pre-wrap', background: '#f8fafc', padding: 16, borderRadius: 10, border: '1px solid #eef0f4', minHeight: 60 }}>{(wd.description != null && String(wd.description).trim() !== '') ? String(wd.description).trim() : '-'}</div>
  2147. </div>
  2148. </div>
  2149. );
  2150. }
  2151. const isReview = nodeType === 'review';
  2152. const isIsolation = nodeType === 'isolation';
  2153. const isReleaseIsolation = nodeType === 'releaseIsolation';
  2154. const isReturnLock = nodeType === 'returnLock';
  2155. console.log('TaskManagement: 节点类型判断结果', {
  2156. isReview,
  2157. isIsolation,
  2158. isReleaseIsolation,
  2159. isReturnLock,
  2160. nodeType,
  2161. 'detailData.type': detailData.type,
  2162. 'detailData.nodeType': detailData.nodeType,
  2163. 'typeof detailData.type': typeof detailData.type,
  2164. 'String(detailData.type)': String(detailData.type)
  2165. });
  2166. // 当前节点是否有表单:无 formId 的节点(如完成/结束)不应渲染自定义表单
  2167. const hasFormIdForCurrentNode = detailData?.formId !== undefined && detailData?.formId !== null && (
  2168. typeof detailData.formId === 'number' || (typeof detailData.formId === 'string' && String(detailData.formId).trim() !== '')
  2169. );
  2170. // 隔离/方案节点、解除隔离节点和还锁节点
  2171. if (isIsolation || isReleaseIsolation || isReturnLock) {
  2172. const isolationType = String(detailData?.isolationType ?? '').trim();
  2173. const isLockoutTagout = isolationType === '1'; // 上锁挂牌
  2174. const isBlindPlate = isolationType === '0'; // 盲板
  2175. const isDismantle = isolationType === '2'; // 拆除
  2176. const showLockCabinet = isReturnLock || (isLockoutTagout && (isIsolation || isReleaseIsolation));
  2177. const showDeviceForm = (isIsolation || isReleaseIsolation) && (isBlindPlate || isDismantle);
  2178. if (showDeviceForm) {
  2179. const deviceLabel = isReleaseIsolation
  2180. ? (isBlindPlate ? t('form.releaseBlindPlateDeviceNo') : t('form.restoreDeviceNo'))
  2181. : (isBlindPlate ? t('form.blindPlateDeviceNo') : t('form.dismantleDeviceNo'));
  2182. const formIdVal = detailData?.formId;
  2183. const hasFormIdForBlindDismantle = formIdVal !== undefined && formIdVal !== null && (
  2184. typeof formIdVal === 'number' || (typeof formIdVal === 'string' && String(formIdVal).trim() !== '')
  2185. );
  2186. return (
  2187. <div key="isolation-device-form" style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
  2188. <div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '24px', background: 'transparent' }}>
  2189. <div style={{ width: '100%' }}>
  2190. <div style={{ marginBottom: 26 }}>
  2191. <label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 600, color: '#1f2937', marginBottom: 12 }}>
  2192. <span style={{ width: 28, height: 28, borderRadius: 8, background: '#e6f4ff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
  2193. <BarcodeOutlined style={{ fontSize: 14, color: '#1677ff' }} />
  2194. </span>
  2195. {deviceLabel}
  2196. </label>
  2197. <Input
  2198. value={isolationDeviceNumber}
  2199. onChange={(e) => !isApproved && setIsolationDeviceNumber(e.target.value)}
  2200. placeholder={`请输入${deviceLabel}`}
  2201. maxLength={100}
  2202. size="large"
  2203. disabled={isApproved}
  2204. prefix={<BarcodeOutlined style={{ color: '#91caff', marginRight: 8 }} />}
  2205. style={{ borderRadius: 10, borderColor: '#d9e8ff', fontSize: 15 }}
  2206. />
  2207. </div>
  2208. <div>
  2209. <label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 600, color: '#1f2937', marginBottom: 12 }}>
  2210. <span style={{ width: 28, height: 28, borderRadius: 8, background: '#e6f4ff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
  2211. <UploadOutlined style={{ fontSize: 14, color: '#1677ff' }} />
  2212. </span>
  2213. 附件上传
  2214. {!isApproved && <span style={{ marginLeft: 8, fontSize: 12, fontWeight: 400, color: '#69b1ff', background: '#e6f4ff', padding: '2px 8px', borderRadius: 6 }}>支持拖拽</span>}
  2215. </label>
  2216. {isApproved && isolationFileList.length > 0 ? (
  2217. <div
  2218. style={{
  2219. borderRadius: 12,
  2220. padding: '32px 20px',
  2221. background: 'linear-gradient(180deg, #f5faff 0%, #e8f4ff 50%, #e0efff 100%)',
  2222. border: '2px solid #91caff',
  2223. minHeight: 180,
  2224. display: 'flex',
  2225. flexWrap: 'wrap',
  2226. gap: 16,
  2227. alignItems: 'center',
  2228. justifyContent: 'center',
  2229. alignContent: 'center',
  2230. }}
  2231. >
  2232. <Image.PreviewGroup>
  2233. {isolationFileList.filter((f: any) => {
  2234. const u = f.url || f.response;
  2235. return /\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(String(u || '')) || /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(String(f.name || ''));
  2236. }).map((file: any) => (
  2237. <Image
  2238. key={file.uid}
  2239. width={160}
  2240. height={160}
  2241. src={file.url || file.response}
  2242. style={{ objectFit: 'cover', borderRadius: 10, cursor: 'pointer', flexShrink: 0 }}
  2243. alt={file.name}
  2244. />
  2245. ))}
  2246. </Image.PreviewGroup>
  2247. {isolationFileList.filter((f: any) => {
  2248. const u = f.url || f.response;
  2249. return !/\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(String(u || '')) && !/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(String(f.name || ''));
  2250. }).map((file: any) => (
  2251. <a
  2252. key={file.uid}
  2253. href={file.url || file.response}
  2254. target="_blank"
  2255. rel="noopener noreferrer"
  2256. style={{ display: 'block', padding: '12px 16px', background: '#fff', borderRadius: 10, color: '#1677ff', border: '1px solid #91caff', maxWidth: 240, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
  2257. >
  2258. {file.name}
  2259. </a>
  2260. ))}
  2261. </div>
  2262. ) : isApproved ? (
  2263. <div style={{ padding: 24, textAlign: 'center', color: '#8c8c8c', background: '#fafafa', borderRadius: 12 }}>暂无附件</div>
  2264. ) : (
  2265. <Upload.Dragger
  2266. fileList={isolationFileList}
  2267. onChange={({ fileList }) => setIsolationFileList(fileList.slice(-5))}
  2268. multiple
  2269. showUploadList={{ showRemoveIcon: true }}
  2270. customRequest={async ({ file, onSuccess, onError }) => {
  2271. try {
  2272. const url = await fileApi.upload(file as File);
  2273. onSuccess?.(url);
  2274. } catch (e: any) {
  2275. onError?.(e);
  2276. message.error(e?.message || '文件上传失败');
  2277. }
  2278. }}
  2279. style={{
  2280. borderRadius: 12,
  2281. padding: '32px 20px',
  2282. background: 'linear-gradient(180deg, #f5faff 0%, #e8f4ff 50%, #e0efff 100%)',
  2283. border: '2px dashed #91caff',
  2284. }}
  2285. >
  2286. <p className="ant-upload-drag-icon" style={{ marginBottom: 12 }}>
  2287. <span style={{ width: 64, height: 64, borderRadius: 16, background: 'linear-gradient(135deg, rgba(22,119,255,0.15) 0%, rgba(64,150,255,0.1) 100%)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
  2288. <UploadOutlined style={{ fontSize: 32, color: '#1677ff' }} />
  2289. </span>
  2290. </p>
  2291. <p className="ant-upload-text" style={{ margin: 0, color: '#0958d9', fontSize: 15, fontWeight: 500 }}>点击或拖拽文件到此区域上传</p>
  2292. <p className="ant-upload-hint" style={{ margin: '8px 0 0', color: '#69b1ff', fontSize: 13 }}>最多 5 个文件,支持多选</p>
  2293. </Upload.Dragger>
  2294. )}
  2295. </div>
  2296. {/* 盲板/拆除有 formId 时在设备编号和附件上传下方渲染自定义表单,可滚动查看 */}
  2297. {hasFormIdForBlindDismantle && (
  2298. <div style={{ marginTop: 24, paddingTop: 24, borderTop: '1px solid rgba(22, 119, 255, 0.12)' }}>
  2299. {formLoading ? (
  2300. <div className="py-6 text-center text-gray-500">{t('form.formLoading')}</div>
  2301. ) : formData.rule && formData.rule.length > 0 ? (
  2302. (() => {
  2303. const formConfig = formData.option?.formConfig || defaultFormConfig;
  2304. const layoutColumns = formConfig.layoutColumns || 1;
  2305. const gridStyle = layoutColumns > 1 ? {
  2306. display: 'grid',
  2307. gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
  2308. gap: '12px',
  2309. rowGap: '16px',
  2310. } : undefined;
  2311. return (
  2312. <AntdForm
  2313. form={detailForm}
  2314. layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
  2315. size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
  2316. requiredMark={formConfig.hideRequiredMark ? false : undefined}
  2317. labelCol={
  2318. formConfig.labelPosition === 'top' || !formConfig.labelWidth
  2319. ? undefined
  2320. : {
  2321. flex: `${formConfig.labelWidth}px`,
  2322. style: {
  2323. minWidth: `${formConfig.labelWidth}px`,
  2324. textAlign: formConfig.labelPosition === 'right' ? 'right' : 'left',
  2325. },
  2326. }
  2327. }
  2328. wrapperCol={formConfig.labelPosition === 'top' ? undefined : { flex: 'auto', style: { minWidth: 0 } }}
  2329. >
  2330. <div style={gridStyle} className={layoutColumns === 1 ? 'space-y-4' : 'form-detail-grid'}>
  2331. <style>{`.form-detail-grid .ant-form-item { margin-bottom: 12px; } .form-detail-grid .ant-form-item-control { min-width: 0; }`}</style>
  2332. {(formData.rule || []).map((field: any) => {
  2333. const fieldWithDisabled = isApproved ? { ...field, disabled: true, readOnly: true } : field;
  2334. return renderFieldPreview(fieldWithDisabled);
  2335. })}
  2336. </div>
  2337. </AntdForm>
  2338. );
  2339. })()
  2340. ) : null}
  2341. </div>
  2342. )}
  2343. </div>
  2344. </div>
  2345. </div>
  2346. );
  2347. }
  2348. console.log('TaskManagement: ✅ 进入隔离节点渲染分支', { isIsolation, isReleaseIsolation, isReturnLock, nodeType, isolationType });
  2349. // 隔离/方案(上锁挂牌)节点:三步并列分栏展示
  2350. if (isIsolation && isLockoutTagout) {
  2351. const points: any[] = Array.isArray((detailData as any)?.points) ? (detailData as any).points : [];
  2352. const keysList: any[] = Array.isArray((detailData as any)?.keys) ? (detailData as any).keys : [];
  2353. const jtLockerUsers: any[] = Array.isArray((detailData as any)?.nodeUserList)
  2354. ? ((detailData as any).nodeUserList || []).filter((u: any) => String(u?.type || '').toLowerCase() === 'jtlocker')
  2355. : [];
  2356. const jtCoLockerUsers: any[] = Array.isArray((detailData as any)?.nodeUserList)
  2357. ? ((detailData as any).nodeUserList || []).filter((u: any) => String(u?.type || '').toLowerCase() === 'jtcolocker')
  2358. : [];
  2359. const lockPersons: any[] = jtLockerUsers;
  2360. const getPointName = (p: any) =>
  2361. p?.pointName ??
  2362. p?.name ??
  2363. p?.pointLabel ??
  2364. p?.pointCode ??
  2365. (p?.id != null ? `点位${p.id}` : '-');
  2366. const isLocked = (p: any) => Boolean(p?.lockTime) || p?.locked === true || String(p?.status || '').toLowerCase() === 'locked';
  2367. const lockBadgeStyle = (locked: boolean): React.CSSProperties =>
  2368. locked
  2369. ? { backgroundColor: '#15803d', color: '#ffffff' }
  2370. : { backgroundColor: '#e2e8f0', color: '#475569' };
  2371. const getKeyStatus = (k: any) => {
  2372. const status = String(k?.keyStatus ?? '').trim();
  2373. // 钥匙状态(0-待取出 1-已取出 2-已归还)
  2374. if (status === '0') return 'to_take';
  2375. if (status === '1') return 'taken';
  2376. if (status === '2') return 'returned';
  2377. return 'unknown';
  2378. };
  2379. const getUserAvatarSrc = (u: any) =>
  2380. u?.avatar ??
  2381. u?.avatarUrl ??
  2382. u?.headImg ??
  2383. u?.headImage ??
  2384. u?.headUrl ??
  2385. u?.profilePhoto ??
  2386. u?.photo ??
  2387. undefined;
  2388. const getUserName = (u: any) => u?.nickname ?? u?.nickName ?? u?.name ?? u?.username ?? '-';
  2389. const getLockerStatus = (u: any) => {
  2390. // 上锁人状态:0未上锁、1已上锁、2已解锁
  2391. const s = String(u?.status ?? '').trim();
  2392. if (s === '1') return 'locked';
  2393. if (s === '2') return 'unlocked_done';
  2394. if (s === '0') return 'unlocked';
  2395. return 'unknown';
  2396. };
  2397. return (
  2398. <div
  2399. key="isolation-lockout-tabs"
  2400. style={{
  2401. height: '100%',
  2402. display: 'flex',
  2403. flexDirection: 'column',
  2404. overflow: 'hidden',
  2405. }}
  2406. >
  2407. <div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: 24, background: 'transparent' }}>
  2408. <Card
  2409. style={{
  2410. width: '100%',
  2411. borderRadius: 16,
  2412. boxShadow: '0 8px 28px rgba(15, 23, 42, 0.08), 0 2px 8px rgba(0,0,0,0.04)',
  2413. border: '1px solid #e6e8f0',
  2414. background: '#ffffff',
  2415. }}
  2416. bodyStyle={{ padding: 18 }}
  2417. >
  2418. <div
  2419. style={{
  2420. display: 'grid',
  2421. gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
  2422. gap: 0,
  2423. }}
  2424. >
  2425. {/* 第一步:取挂锁,取钥匙 */}
  2426. <div style={{ padding: '0 14px', borderRight: '1px solid #eef0f6', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
  2427. <div style={{ fontSize: 14, fontWeight: 600, color: '#0f172a', marginBottom: 12, minHeight: 22 }}>
  2428. 第一步:取挂锁,取钥匙
  2429. </div>
  2430. <div style={{ padding: '8px 0', flex: 1 }}>
  2431. <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>是否上锁</div>
  2432. <div style={{ display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10, marginBottom: 14 }}>
  2433. {(lockPersons.length > 0 ? lockPersons : [{ __empty: true }]).map((u: any, idx: number) => {
  2434. if (u.__empty) {
  2435. return (
  2436. <div
  2437. key="empty-locker"
  2438. style={{
  2439. padding: '12px 12px',
  2440. borderRadius: 10,
  2441. border: '1px dashed #cbd5e1',
  2442. background: '#f8fafc',
  2443. color: '#64748b',
  2444. textAlign: 'center',
  2445. fontSize: 13,
  2446. }}
  2447. >
  2448. 暂无上锁人
  2449. </div>
  2450. );
  2451. }
  2452. const name = getUserName(u);
  2453. const avatarSrc = getUserAvatarSrc(u);
  2454. const st = getLockerStatus(u);
  2455. return (
  2456. <div
  2457. key={u.id ?? u.userId ?? u.uid ?? idx}
  2458. style={{
  2459. display: 'flex',
  2460. alignItems: 'center',
  2461. justifyContent: 'space-between',
  2462. gap: 10,
  2463. padding: '10px 10px',
  2464. borderRadius: 10,
  2465. border: '1px solid #e6e8f0',
  2466. background: '#ffffff',
  2467. }}
  2468. >
  2469. <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
  2470. <Avatar size={32} src={avatarSrc} style={{ background: '#e5e7eb', color: '#111827', flexShrink: 0 }}>
  2471. {String(name || '-').slice(0, 1)}
  2472. </Avatar>
  2473. <div style={{ minWidth: 0 }}>
  2474. <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
  2475. {name}
  2476. </div>
  2477. </div>
  2478. </div>
  2479. <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12 }}>
  2480. {st === 'unknown' ? (
  2481. <>
  2482. <WarningOutlined style={{ fontSize: 14, color: '#f97316' }} />
  2483. <span style={{ color: '#f97316' }}>未知</span>
  2484. </>
  2485. ) : st === 'locked' ? (
  2486. <>
  2487. <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} />
  2488. <span style={{ color: '#15803d' }}>已上锁</span>
  2489. </>
  2490. ) : st === 'unlocked_done' ? (
  2491. <>
  2492. <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} />
  2493. <span style={{ color: '#15803d' }}>已解锁</span>
  2494. </>
  2495. ) : (
  2496. <>
  2497. <CloseCircleOutlined style={{ fontSize: 14, color: '#dc2626' }} />
  2498. <span style={{ color: '#dc2626' }}>未上锁</span>
  2499. </>
  2500. )}
  2501. </span>
  2502. </div>
  2503. );
  2504. })}
  2505. </div>
  2506. <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>
  2507. 本次能量隔离任务中需要隔离上锁的点位
  2508. </div>
  2509. <div style={{ width: '100%', maxWidth: 320, display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10 }}>
  2510. {(points.length > 0 ? points : [{ __empty: true }]).map((p: any, idx: number) => {
  2511. if (p.__empty) {
  2512. return (
  2513. <div
  2514. key="empty"
  2515. style={{
  2516. padding: '14px 12px',
  2517. borderRadius: 10,
  2518. border: '1px dashed #cbd5e1',
  2519. background: '#f8fafc',
  2520. color: '#64748b',
  2521. textAlign: 'center',
  2522. fontSize: 13,
  2523. }}
  2524. >
  2525. 暂无点位
  2526. </div>
  2527. );
  2528. }
  2529. const locked = isLocked(p);
  2530. return (
  2531. <div
  2532. key={p.id ?? p.pointId ?? idx}
  2533. style={{
  2534. display: 'flex',
  2535. alignItems: 'center',
  2536. gap: 10,
  2537. padding: '10px 10px',
  2538. borderRadius: 10,
  2539. border: '1px solid #e6e8f0',
  2540. background: '#ffffff',
  2541. }}
  2542. >
  2543. <span
  2544. style={{
  2545. width: 32,
  2546. height: 32,
  2547. borderRadius: 10,
  2548. background: '#f1f5ff',
  2549. display: 'inline-flex',
  2550. alignItems: 'center',
  2551. justifyContent: 'center',
  2552. flexShrink: 0,
  2553. }}
  2554. >
  2555. <EnvironmentOutlined style={{ color: '#1677ff', fontSize: 16 }} />
  2556. </span>
  2557. <div style={{ minWidth: 0, flex: 1 }}>
  2558. <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
  2559. <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
  2560. {getPointName(p)}
  2561. </div>
  2562. <span className="inline-flex px-3 py-1 rounded-full text-xs font-medium" style={lockBadgeStyle(locked)}>
  2563. {locked ? '已上锁' : '未上锁'}
  2564. </span>
  2565. </div>
  2566. </div>
  2567. </div>
  2568. );
  2569. })}
  2570. </div>
  2571. </div>
  2572. </div>
  2573. {/* 第二步:上锁后归还钥匙 */}
  2574. <div style={{ padding: '0 14px', borderRight: '1px solid #eef0f6', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
  2575. <div style={{ fontSize: 14, fontWeight: 600, color: '#0f172a', marginBottom: 12, minHeight: 22 }}>
  2576. 第二步:上锁后归还钥匙
  2577. </div>
  2578. <div style={{ padding: '8px 0', flex: 1 }}>
  2579. <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>
  2580. 是否有待归还的钥匙
  2581. </div>
  2582. <div style={{ width: '100%', maxWidth: 320, display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10 }}>
  2583. {(keysList.length > 0 ? keysList : [{ __empty: true }]).map((k: any, idx: number) => {
  2584. if (k.__empty) {
  2585. return (
  2586. <div
  2587. key="empty-key"
  2588. style={{
  2589. padding: '14px 12px',
  2590. borderRadius: 10,
  2591. border: '1px dashed #cbd5e1',
  2592. background: '#f8fafc',
  2593. color: '#64748b',
  2594. textAlign: 'center',
  2595. fontSize: 13,
  2596. }}
  2597. >
  2598. 暂无钥匙
  2599. </div>
  2600. );
  2601. }
  2602. const status = getKeyStatus(k);
  2603. const isReturned = status === 'returned';
  2604. let statusText = '未知';
  2605. let statusColor = '#6b7280';
  2606. if (status === 'to_take') {
  2607. statusText = '待取出';
  2608. statusColor = '#f97316';
  2609. } else if (status === 'taken') {
  2610. statusText = '已取出';
  2611. statusColor = '#dc2626';
  2612. } else if (isReturned) {
  2613. statusText = '已归还';
  2614. statusColor = '#15803d';
  2615. }
  2616. const returnedAt =
  2617. k?.giveBackTime ??
  2618. k?.returnTime ??
  2619. k?.returnedTime ??
  2620. k?.backTime ??
  2621. k?.keyReturnTime ??
  2622. k?.updateTime ??
  2623. k?.returnedAt ??
  2624. k?.returnAt;
  2625. return (
  2626. <div
  2627. key={k.id ?? k.keyId ?? idx}
  2628. style={{
  2629. display: 'flex',
  2630. alignItems: 'center',
  2631. gap: 10,
  2632. padding: '10px 10px',
  2633. borderRadius: 10,
  2634. border: '1px solid #e6e8f0',
  2635. background: '#ffffff',
  2636. }}
  2637. >
  2638. <span
  2639. style={{
  2640. width: 32,
  2641. height: 32,
  2642. borderRadius: 10,
  2643. background: '#eff6ff',
  2644. display: 'inline-flex',
  2645. alignItems: 'center',
  2646. justifyContent: 'center',
  2647. flexShrink: 0,
  2648. }}
  2649. >
  2650. <KeyOutlined style={{ color: '#2563eb', fontSize: 16 }} />
  2651. </span>
  2652. <div style={{ minWidth: 0, flex: 1 }}>
  2653. <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
  2654. <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
  2655. {k?.keyNfc ?? k?.keyName ?? k?.name ?? k?.code ?? '-'}
  2656. </div>
  2657. <span
  2658. style={{
  2659. display: 'inline-flex',
  2660. alignItems: 'center',
  2661. gap: 4,
  2662. fontSize: 12,
  2663. color: statusColor,
  2664. }}
  2665. >
  2666. {status === 'returned' ? (
  2667. <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} />
  2668. ) : status === 'taken' ? (
  2669. <CloseCircleOutlined style={{ fontSize: 14, color: '#dc2626' }} />
  2670. ) : status === 'to_take' ? (
  2671. <WarningOutlined style={{ fontSize: 14, color: '#f97316' }} />
  2672. ) : (
  2673. <WarningOutlined style={{ fontSize: 14, color: '#9ca3af' }} />
  2674. )}
  2675. <span>{statusText}</span>
  2676. </span>
  2677. </div>
  2678. <div style={{ marginTop: 4, fontSize: 12, color: '#64748b' }}>
  2679. 归还时间:{isReturned && returnedAt != null && String(returnedAt).trim() !== '' ? dateFormatter(returnedAt) : '-'}
  2680. </div>
  2681. </div>
  2682. </div>
  2683. );
  2684. })}
  2685. </div>
  2686. </div>
  2687. </div>
  2688. {/* 第三步:共锁人共锁 */}
  2689. <div style={{ padding: '0 14px', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
  2690. <div style={{ fontSize: 14, fontWeight: 600, color: '#0f172a', marginBottom: 12, minHeight: 22 }}>
  2691. 第三步:共锁人共锁
  2692. </div>
  2693. <div style={{ padding: '8px 0', flex: 1 }}>
  2694. <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>是否共锁</div>
  2695. <div style={{ width: '100%', maxWidth: 320, display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10 }}>
  2696. {(jtCoLockerUsers.length > 0 ? jtCoLockerUsers : [{ __empty: true }]).map((u: any, idx: number) => {
  2697. if (u.__empty) {
  2698. return (
  2699. <div
  2700. key="empty-colocker"
  2701. style={{
  2702. padding: '14px 12px',
  2703. borderRadius: 10,
  2704. border: '1px dashed #cbd5e1',
  2705. background: '#f8fafc',
  2706. color: '#64748b',
  2707. textAlign: 'center',
  2708. fontSize: 13,
  2709. }}
  2710. >
  2711. 暂无共锁人
  2712. </div>
  2713. );
  2714. }
  2715. const name = getUserName(u);
  2716. const avatarSrc = getUserAvatarSrc(u);
  2717. // 共锁人状态: 0未共锁、1已共锁、2已解共锁
  2718. const lockedStatus = String(u?.status ?? '').trim();
  2719. const coLockStatusText = lockedStatus === '1' ? '已共锁' : lockedStatus === '2' ? '已解共锁' : '未共锁';
  2720. const coLockStatusColor = lockedStatus === '1' ? '#15803d' : lockedStatus === '2' ? '#15803d' : '#dc2626';
  2721. return (
  2722. <div
  2723. key={u.id ?? u.userId ?? u.uid ?? idx}
  2724. style={{
  2725. display: 'flex',
  2726. alignItems: 'center',
  2727. justifyContent: 'space-between',
  2728. gap: 10,
  2729. padding: '10px 10px',
  2730. borderRadius: 10,
  2731. border: '1px solid #e6e8f0',
  2732. background: '#ffffff',
  2733. }}
  2734. >
  2735. <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
  2736. <Avatar
  2737. size={32}
  2738. src={avatarSrc}
  2739. style={{ background: '#e5e7eb', color: '#111827', flexShrink: 0 }}
  2740. >
  2741. {String(name || '-').slice(0, 1)}
  2742. </Avatar>
  2743. <div style={{ minWidth: 0 }}>
  2744. <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
  2745. {name}
  2746. </div>
  2747. </div>
  2748. </div>
  2749. <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: coLockStatusColor }}>
  2750. {lockedStatus === '1' ? (
  2751. <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} />
  2752. ) : lockedStatus === '2' ? (
  2753. <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} />
  2754. ) : (
  2755. <CloseCircleOutlined style={{ fontSize: 14, color: '#dc2626' }} />
  2756. )}
  2757. <span>{coLockStatusText}</span>
  2758. </span>
  2759. </div>
  2760. );
  2761. })}
  2762. </div>
  2763. </div>
  2764. </div>
  2765. </div>
  2766. </Card>
  2767. </div>
  2768. </div>
  2769. );
  2770. }
  2771. // 解除隔离(上锁挂牌)节点:三步并列分栏展示
  2772. if (isReleaseIsolation && isLockoutTagout) {
  2773. const releaseKeysList: any[] = Array.isArray((detailData as any)?.keys) ? (detailData as any).keys : [];
  2774. const releaseCoLockers: any[] = Array.isArray((detailData as any)?.nodeUserList)
  2775. ? ((detailData as any).nodeUserList || []).filter((u: any) => String(u?.type || '').toLowerCase() === 'jtcolocker')
  2776. : [];
  2777. const releaseLockPersons: any[] = Array.isArray((detailData as any)?.nodeUserList)
  2778. ? ((detailData as any).nodeUserList || []).filter((u: any) => String(u?.type || '').toLowerCase() === 'jtlocker')
  2779. : [];
  2780. const getReleaseKeyStatus = (k: any) => {
  2781. const s = String(k?.keyStatus ?? '').trim();
  2782. // 钥匙状态(0-待取出 1-已取出 2-已归还)
  2783. if (s === '0') return 'to_take';
  2784. if (s === '1') return 'taken';
  2785. if (s === '2') return 'returned';
  2786. return 'unknown';
  2787. };
  2788. const getReleaseUserAvatarSrc = (u: any) =>
  2789. u?.avatar ?? u?.avatarUrl ?? u?.headImg ?? u?.headImage ?? u?.headUrl ?? u?.profilePhoto ?? u?.photo ?? undefined;
  2790. const getReleaseUserName = (u: any) => u?.nickname ?? u?.nickName ?? u?.name ?? u?.username ?? '-';
  2791. return (
  2792. <div key="release-isolation-columns" style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
  2793. <div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: 24, background: 'transparent' }}>
  2794. <Card
  2795. style={{
  2796. width: '100%',
  2797. borderRadius: 16,
  2798. boxShadow: '0 8px 28px rgba(15, 23, 42, 0.08), 0 2px 8px rgba(0,0,0,0.04)',
  2799. border: '1px solid #e6e8f0',
  2800. background: '#ffffff',
  2801. }}
  2802. bodyStyle={{ padding: 18 }}
  2803. >
  2804. <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 0 }}>
  2805. {/* 第一步:解除共锁 */}
  2806. <div style={{ padding: '0 14px', borderRight: '1px solid #eef0f6', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
  2807. <div style={{ fontSize: 14, fontWeight: 600, color: '#0f172a', marginBottom: 12, minHeight: 22 }}>第一步:解除共锁</div>
  2808. <div style={{ padding: '8px 0', flex: 1 }}>
  2809. <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>是否解除共锁</div>
  2810. <div style={{ width: '100%', maxWidth: 320, display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10 }}>
  2811. {(releaseCoLockers.length > 0 ? releaseCoLockers : [{ __empty: true }]).map((u: any, idx: number) => {
  2812. if (u.__empty) {
  2813. return (
  2814. <div key="empty-release-colocker" style={{ padding: '14px 12px', borderRadius: 10, border: '1px dashed #cbd5e1', background: '#f8fafc', color: '#64748b', textAlign: 'center', fontSize: 13 }}>暂无共锁人</div>
  2815. );
  2816. }
  2817. const name = getReleaseUserName(u);
  2818. const avatarSrc = getReleaseUserAvatarSrc(u);
  2819. // 共锁人状态: 0未共锁、1已共锁、2已解共锁
  2820. const coLockStatus = String(u?.status ?? '').trim();
  2821. const coLockText = coLockStatus === '1' ? '已共锁' : coLockStatus === '2' ? '已解共锁' : '未共锁';
  2822. const coLockColor = coLockStatus === '1' ? '#15803d' : coLockStatus === '2' ? '#15803d' : '#dc2626';
  2823. return (
  2824. <div key={u.id ?? u.userId ?? idx} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, padding: '10px 10px', borderRadius: 10, border: '1px solid #e6e8f0', background: '#ffffff' }}>
  2825. <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
  2826. <Avatar size={32} src={avatarSrc} style={{ background: '#e5e7eb', color: '#111827', flexShrink: 0 }}>{String(name || '-').slice(0, 1)}</Avatar>
  2827. <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{name}</div>
  2828. </div>
  2829. <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: coLockColor }}>
  2830. {coLockStatus === '1' ? <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} /> : coLockStatus === '2' ? <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} /> : <CloseCircleOutlined style={{ fontSize: 14, color: '#dc2626' }} />}
  2831. <span>{coLockText}</span>
  2832. </span>
  2833. </div>
  2834. );
  2835. })}
  2836. </div>
  2837. </div>
  2838. </div>
  2839. {/* 第二步:取钥匙(只显示待取出/已取出,接口若为2已归还则视为已取出) */}
  2840. <div style={{ padding: '0 14px', borderRight: '1px solid #eef0f6', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
  2841. <div style={{ fontSize: 14, fontWeight: 600, color: '#0f172a', marginBottom: 12, minHeight: 22 }}>第二步:取钥匙</div>
  2842. <div style={{ padding: '8px 0', flex: 1 }}>
  2843. <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>钥匙状态</div>
  2844. <div style={{ width: '100%', maxWidth: 320, display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10 }}>
  2845. {(releaseKeysList.length > 0 ? releaseKeysList : [{ __empty: true }]).map((k: any, idx: number) => {
  2846. if (k.__empty) {
  2847. return (
  2848. <div key="empty-release-key" style={{ padding: '14px 12px', borderRadius: 10, border: '1px dashed #cbd5e1', background: '#f8fafc', color: '#64748b', textAlign: 'center', fontSize: 13 }}>暂无钥匙</div>
  2849. );
  2850. }
  2851. const status = getReleaseKeyStatus(k);
  2852. // 第二步只显示两种:0待取出 / 1或2 均显示已取出(已归还说明必然已取出过)
  2853. const step2Taken = status !== 'to_take';
  2854. const step2StatusText = step2Taken ? '已取出' : '待取出';
  2855. return (
  2856. <div key={k.id ?? k.keyId ?? idx} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 10px', borderRadius: 10, border: '1px solid #e6e8f0', background: '#ffffff' }}>
  2857. <span style={{ width: 32, height: 32, borderRadius: 10, background: '#eff6ff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
  2858. <KeyOutlined style={{ color: '#2563eb', fontSize: 16 }} />
  2859. </span>
  2860. <div style={{ minWidth: 0, flex: 1 }}>
  2861. <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
  2862. <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{k?.keyNfc ?? k?.keyName ?? k?.name ?? '-'}</div>
  2863. <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 12, color: step2Taken ? '#15803d' : '#dc2626' }}>
  2864. {step2Taken ? <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} /> : <CloseCircleOutlined style={{ fontSize: 14, color: '#dc2626' }} />}
  2865. <span>{step2StatusText}</span>
  2866. </span>
  2867. </div>
  2868. </div>
  2869. </div>
  2870. );
  2871. })}
  2872. </div>
  2873. </div>
  2874. </div>
  2875. {/* 第三步:还钥匙 */}
  2876. <div style={{ padding: '0 14px', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
  2877. <div style={{ fontSize: 14, fontWeight: 600, color: '#0f172a', marginBottom: 12, minHeight: 22 }}>第三步:还钥匙</div>
  2878. <div style={{ padding: '8px 0', flex: 1 }}>
  2879. <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>解锁人</div>
  2880. <div style={{ width: '100%', maxWidth: 320, display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10, marginBottom: 14 }}>
  2881. {(releaseLockPersons.length > 0 ? releaseLockPersons : [{ __empty: true }]).map((u: any, idx: number) => {
  2882. if (u.__empty) {
  2883. return (
  2884. <div key="empty-release-locker" style={{ padding: '12px 12px', borderRadius: 10, border: '1px dashed #cbd5e1', background: '#f8fafc', color: '#64748b', textAlign: 'center', fontSize: 13 }}>暂无解锁人</div>
  2885. );
  2886. }
  2887. const name = getReleaseUserName(u);
  2888. const avatarSrc = getReleaseUserAvatarSrc(u);
  2889. // 上锁人状态: 0未上锁、1已上锁、2已解锁
  2890. const lockStatus = String(u?.status ?? '').trim();
  2891. const lockText = lockStatus === '1' ? '已上锁' : lockStatus === '2' ? '已解锁' : '未上锁';
  2892. const lockColor = lockStatus === '1' ? '#15803d' : lockStatus === '2' ? '#15803d' : '#dc2626';
  2893. return (
  2894. <div key={u.id ?? u.userId ?? idx} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, padding: '10px 10px', borderRadius: 10, border: '1px solid #e6e8f0', background: '#ffffff' }}>
  2895. <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
  2896. <Avatar size={32} src={avatarSrc} style={{ background: '#e5e7eb', color: '#111827', flexShrink: 0 }}>{String(name || '-').slice(0, 1)}</Avatar>
  2897. <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{name}</div>
  2898. </div>
  2899. <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: lockColor }}>
  2900. {lockStatus === '1' ? <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} /> : lockStatus === '2' ? <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} /> : <CloseCircleOutlined style={{ fontSize: 14, color: '#dc2626' }} />}
  2901. <span>{lockText}</span>
  2902. </span>
  2903. </div>
  2904. );
  2905. })}
  2906. </div>
  2907. <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>钥匙状态</div>
  2908. <div style={{ width: '100%', maxWidth: 320, display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10 }}>
  2909. {(releaseKeysList.length > 0 ? releaseKeysList : [{ __empty: true }]).map((k: any, idx: number) => {
  2910. if (k.__empty) {
  2911. return (
  2912. <div key="empty-release-key2" style={{ padding: '14px 12px', borderRadius: 10, border: '1px dashed #cbd5e1', background: '#f8fafc', color: '#64748b', textAlign: 'center', fontSize: 13 }}>暂无钥匙</div>
  2913. );
  2914. }
  2915. const status = getReleaseKeyStatus(k);
  2916. // 第三步显示三种状态:0待取出、1已取出、2已归还
  2917. const step3StatusText = status === 'returned' ? '已归还' : status === 'taken' ? '已取出' : status === 'to_take' ? '待取出' : '未知';
  2918. const step3Color = status === 'returned' ? '#15803d' : status === 'taken' ? '#f97316' : '#dc2626';
  2919. const returnedAt = k?.giveBackTime ?? k?.returnTime ?? k?.returnedTime ?? k?.backTime ?? k?.keyReturnTime ?? k?.updateTime ?? k?.returnedAt ?? k?.returnAt;
  2920. return (
  2921. <div key={k.id ?? k.keyId ?? idx} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 10px', borderRadius: 10, border: '1px solid #e6e8f0', background: '#ffffff' }}>
  2922. <span style={{ width: 32, height: 32, borderRadius: 10, background: '#eff6ff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
  2923. <KeyOutlined style={{ color: '#2563eb', fontSize: 16 }} />
  2924. </span>
  2925. <div style={{ minWidth: 0, flex: 1 }}>
  2926. <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
  2927. <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{k?.keyNfc ?? k?.keyName ?? k?.name ?? '-'}</div>
  2928. <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 12, color: step3Color }}>
  2929. {status === 'returned' ? <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} /> : status === 'taken' ? <CheckCircleOutlined style={{ fontSize: 14, color: '#f97316' }} /> : <CloseCircleOutlined style={{ fontSize: 14, color: '#dc2626' }} />}
  2930. <span>{step3StatusText}</span>
  2931. </span>
  2932. </div>
  2933. <div style={{ marginTop: 4, fontSize: 12, color: '#64748b' }}>归还时间:{returnedAt != null && String(returnedAt).trim() !== '' ? dateFormatter(returnedAt) : '-'}</div>
  2934. </div>
  2935. </div>
  2936. );
  2937. })}
  2938. </div>
  2939. </div>
  2940. </div>
  2941. </div>
  2942. </Card>
  2943. </div>
  2944. </div>
  2945. );
  2946. }
  2947. const isolationContent = (
  2948. <div
  2949. key="isolation-content"
  2950. style={{
  2951. flex: 1,
  2952. minHeight: 0,
  2953. overflow: 'hidden',
  2954. display: 'flex',
  2955. alignItems: 'center',
  2956. justifyContent: 'center',
  2957. padding: '24px',
  2958. background: 'linear-gradient(165deg, #f0f7ff 0%, #fafbff 45%, #fff 100%)',
  2959. }}
  2960. >
  2961. <Card
  2962. style={{
  2963. width: '100%',
  2964. maxWidth: 500,
  2965. textAlign: 'center',
  2966. borderRadius: 16,
  2967. boxShadow: '0 8px 32px rgba(22, 119, 255, 0.12), 0 2px 8px rgba(0,0,0,0.04)',
  2968. border: '1px solid rgba(22, 119, 255, 0.15)',
  2969. background: 'linear-gradient(180deg, #ffffff 0%, #fafcff 100%)',
  2970. }}
  2971. bodyStyle={{ padding: '48px 40px' }}
  2972. >
  2973. <div style={{ marginBottom: 28 }}>
  2974. <span
  2975. style={{
  2976. width: 88,
  2977. height: 88,
  2978. borderRadius: 20,
  2979. background: 'linear-gradient(135deg, #1677ff 0%, #4096ff 50%, #69b1ff 100%)',
  2980. display: 'inline-flex',
  2981. alignItems: 'center',
  2982. justifyContent: 'center',
  2983. boxShadow: '0 8px 24px rgba(22, 119, 255, 0.35)',
  2984. }}
  2985. >
  2986. <LockOutlined style={{ fontSize: 42, color: '#fff' }} />
  2987. </span>
  2988. </div>
  2989. <div
  2990. style={{
  2991. fontSize: 18,
  2992. fontWeight: 600,
  2993. color: '#1f2937',
  2994. lineHeight: 1.6,
  2995. marginBottom: 8,
  2996. }}
  2997. >
  2998. {t('form.lockCabinetTip')}
  2999. </div>
  3000. <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13, color: '#69b1ff' }}>
  3001. <KeyOutlined />
  3002. <span>完成取锁、取钥匙后,任务将自动推进</span>
  3003. </div>
  3004. </Card>
  3005. </div>
  3006. );
  3007. return isolationContent;
  3008. }
  3009. // 审核类型节点 (review) - 表单直接放在外层,取消内置白框避免挤压
  3010. if (isReview) {
  3011. return (
  3012. <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
  3013. <div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '24px', background: 'transparent' }}>
  3014. <div style={{ width: '100%' }}>
  3015. {/* 自定义表单:仅当前节点有 formId 时渲染,避免完成/结束节点误展示上一节点表单 */}
  3016. {formLoading ? (
  3017. <div className="py-8 text-center text-gray-500">{t('form.formLoading')}</div>
  3018. ) : hasFormIdForCurrentNode && formData.rule && formData.rule.length > 0 ? (
  3019. <div className="space-y-4">
  3020. {(() => {
  3021. const formConfig = formData.option?.formConfig || defaultFormConfig;
  3022. const layoutColumns = formConfig.layoutColumns || 1;
  3023. const gridStyle = layoutColumns > 1 ? {
  3024. display: 'grid',
  3025. gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
  3026. gap: '12px',
  3027. rowGap: '16px',
  3028. } : undefined;
  3029. return (
  3030. <AntdForm
  3031. form={detailForm}
  3032. layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
  3033. size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
  3034. requiredMark={formConfig.hideRequiredMark ? false : undefined}
  3035. labelCol={
  3036. formConfig.labelPosition === 'top' || !formConfig.labelWidth
  3037. ? undefined
  3038. : {
  3039. flex: `${formConfig.labelWidth}px`,
  3040. style: {
  3041. minWidth: `${formConfig.labelWidth}px`,
  3042. textAlign: formConfig.labelPosition === 'right' ? 'right' : 'left',
  3043. },
  3044. }
  3045. }
  3046. wrapperCol={formConfig.labelPosition === 'top' ? undefined : { flex: 'auto', style: { minWidth: 0 } }}
  3047. >
  3048. <div style={gridStyle} className={layoutColumns === 1 ? 'space-y-4' : 'form-detail-grid'}>
  3049. <style>{`.form-detail-grid .ant-form-item { margin-bottom: 12px; } .form-detail-grid .ant-form-item-control { min-width: 0; }`}</style>
  3050. {(formData.rule || []).map((field: any) => {
  3051. const fieldWithDisabled = isApproved ? { ...field, disabled: true, readOnly: true } : field;
  3052. return renderFieldPreview(fieldWithDisabled);
  3053. })}
  3054. </div>
  3055. </AntdForm>
  3056. );
  3057. })()}
  3058. </div>
  3059. ) : (
  3060. <div style={{ textAlign: 'center', padding: '32px 24px' }}>
  3061. <span style={{ width: 72, height: 72, borderRadius: 18, background: '#e5e7eb', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', marginBottom: 20 }}>
  3062. <CheckCircleOutlined style={{ fontSize: 36, color: '#6b7280' }} />
  3063. </span>
  3064. <div style={{ fontSize: 18, fontWeight: 600, color: '#1f2937', lineHeight: 1.6 }}>
  3065. 无需操作
  3066. </div>
  3067. </div>
  3068. )}
  3069. {/* 审核意见 */}
  3070. <div className="flex items-start" style={{ gap: 16, marginTop: 24, paddingTop: 24, borderTop: '1px solid #e2e8f0' }}>
  3071. <label className="text-sm font-medium text-gray-700 whitespace-nowrap pt-2" style={{ width: defaultFormConfig.labelWidth ? `${defaultFormConfig.labelWidth - 20}px` : '80px', textAlign: defaultFormConfig.labelPosition === 'left' ? 'left' : defaultFormConfig.labelPosition === 'right' ? 'right' : 'left', flexShrink: 0 }}>
  3072. {t('form.reviewComment')}
  3073. </label>
  3074. <div className="flex-1">
  3075. <Input.TextArea
  3076. rows={4}
  3077. value={approvalComment}
  3078. onChange={(e) => setApprovalComment(e.target.value)}
  3079. placeholder={t('form.reviewCommentPlaceholder')}
  3080. maxLength={500}
  3081. showCount
  3082. disabled={isApproved}
  3083. readOnly={isApproved}
  3084. style={{ borderRadius: 10, borderColor: '#d9e8ff' }}
  3085. />
  3086. </div>
  3087. </div>
  3088. </div>
  3089. </div>
  3090. </div>
  3091. );
  3092. }
  3093. // 其他节点类型(录入信息等,需要根据 formId 获取表单)- 样式与创建作业弹框一致:无浅蓝背景、无底部操作按钮
  3094. return (
  3095. <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
  3096. <div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '24px', background: 'transparent' }}>
  3097. <div style={{ width: '100%' }}>
  3098. {/* 自定义表单:仅当前节点有 formId 时渲染,避免完成/结束节点误展示上一节点表单 */}
  3099. {formLoading ? (
  3100. <div className="py-8 text-center text-gray-500">{t('form.formLoading')}</div>
  3101. ) : hasFormIdForCurrentNode && formData.rule && formData.rule.length > 0 ? (
  3102. <div className="space-y-4">
  3103. {(() => {
  3104. const formConfig = formData.option?.formConfig || defaultFormConfig;
  3105. const layoutColumns = formConfig.layoutColumns || 1;
  3106. const gridStyle = layoutColumns > 1 ? {
  3107. display: 'grid',
  3108. gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
  3109. gap: '12px',
  3110. rowGap: '16px',
  3111. } : undefined;
  3112. return (
  3113. <AntdForm
  3114. form={detailForm}
  3115. layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
  3116. size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
  3117. requiredMark={formConfig.hideRequiredMark ? false : undefined}
  3118. labelCol={
  3119. formConfig.labelPosition === 'top' || !formConfig.labelWidth
  3120. ? undefined
  3121. : {
  3122. flex: `${formConfig.labelWidth}px`,
  3123. style: {
  3124. minWidth: `${formConfig.labelWidth}px`,
  3125. textAlign: formConfig.labelPosition === 'right' ? 'right' : 'left',
  3126. },
  3127. }
  3128. }
  3129. wrapperCol={formConfig.labelPosition === 'top' ? undefined : { flex: 'auto', style: { minWidth: 0 } }}
  3130. >
  3131. <div
  3132. style={gridStyle}
  3133. className={layoutColumns === 1 ? 'space-y-4' : 'form-detail-grid'}
  3134. >
  3135. <style>{`
  3136. .form-detail-grid .ant-form-item { margin-bottom: 12px; }
  3137. .form-detail-grid .ant-form-item-control { min-width: 0; }
  3138. `}</style>
  3139. {(formData.rule || []).map((field: any) => {
  3140. // 如果已通过,禁用所有字段
  3141. const fieldWithDisabled = isApproved ? { ...field, disabled: true, readOnly: true } : field;
  3142. return renderFieldPreview(fieldWithDisabled);
  3143. })}
  3144. </div>
  3145. </AntdForm>
  3146. );
  3147. })()}
  3148. </div>
  3149. ) : (
  3150. <div
  3151. style={{
  3152. display: 'flex',
  3153. alignItems: 'center',
  3154. justifyContent: 'center',
  3155. padding: '24px 0',
  3156. }}
  3157. >
  3158. <div style={{ width: '100%', textAlign: 'center' }}>
  3159. <div style={{ marginBottom: 24 }}>
  3160. <span
  3161. style={{
  3162. width: 88,
  3163. height: 88,
  3164. borderRadius: 20,
  3165. background: '#e5e7eb',
  3166. display: 'inline-flex',
  3167. alignItems: 'center',
  3168. justifyContent: 'center',
  3169. }}
  3170. >
  3171. <CheckCircleOutlined style={{ fontSize: 42, color: '#6b7280' }} />
  3172. </span>
  3173. </div>
  3174. <div style={{ fontSize: 18, fontWeight: 600, color: '#1f2937', lineHeight: 1.6 }}>
  3175. 无需操作
  3176. </div>
  3177. </div>
  3178. </div>
  3179. )}
  3180. </div>
  3181. </div>
  3182. </div>
  3183. );
  3184. })()}
  3185. </div>
  3186. <div
  3187. style={{
  3188. padding: '20px 24px 24px',
  3189. borderTop: '1px solid #eef0f4',
  3190. display: 'flex',
  3191. justifyContent: 'center',
  3192. alignItems: 'center',
  3193. background: '#f8fafc',
  3194. }}
  3195. >
  3196. <Button
  3197. size="large"
  3198. type="default"
  3199. style={{
  3200. minWidth: 180,
  3201. height: 44,
  3202. borderRadius: 10,
  3203. fontSize: 15,
  3204. fontWeight: 500,
  3205. borderColor: '#e2e8f0',
  3206. color: '#334155',
  3207. boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
  3208. background: '#ffffff',
  3209. }}
  3210. onClick={() => {
  3211. console.log('TaskManagement: 弹框关闭(底部关闭按钮)');
  3212. setDetailVisible(false);
  3213. setDetailData(null);
  3214. setFormData({ rule: [], option: {} });
  3215. setFormLoading(false);
  3216. setOriginalFields([]);
  3217. setOriginalConf('');
  3218. detailForm.resetFields();
  3219. setApprovalComment('');
  3220. setIsolationDeviceNumber('');
  3221. setIsolationFileList([]);
  3222. }}
  3223. >
  3224. 关 闭
  3225. </Button>
  3226. </div>
  3227. </div>
  3228. );
  3229. }
  3230. return <div className="py-8 text-center text-gray-500">{t('form.noData')}</div>;
  3231. })()}
  3232. </Modal>
  3233. </div>
  3234. );
  3235. }