MyTask.tsx 143 KB

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