MyTask.tsx 140 KB

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