TaskManagement.tsx 139 KB

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