MyTask.tsx 68 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584
  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 } from 'antd';
  5. import { UploadOutlined, LockOutlined, KeyOutlined } from '@ant-design/icons';
  6. import type { ColumnsType } from 'antd/es/table';
  7. import { toast } from 'sonner';
  8. import { myTaskApi, MyTaskVO, MyTaskPageParam, PageResponse, MyTaskNodeDetailVO, UpdateNodeApprovalParam } from '../api/mytask';
  9. import { dateFormatter } from '../utils/formatTime';
  10. import { DICT_TYPE, getDictLabel } from '../utils/dict';
  11. import { setConfAndFields2, FormCreateData } from '../utils/formCreate';
  12. export default function MyTask() {
  13. const navigate = useNavigate();
  14. const [loading, setLoading] = useState(true);
  15. const [list, setList] = useState<MyTaskVO[]>([]);
  16. const [total, setTotal] = useState(0);
  17. const [queryParams, setQueryParams] = useState<MyTaskPageParam>({
  18. pageNo: 1,
  19. pageSize: 10,
  20. key: '',
  21. });
  22. const [searchKey, setSearchKey] = useState('');
  23. const [approvalStatusDictList, setApprovalStatusDictList] = useState<any[]>([]);
  24. // 节点详情弹框相关状态
  25. const [detailVisible, setDetailVisible] = useState(false);
  26. const [detailLoading, setDetailLoading] = useState(false);
  27. const [detailData, setDetailData] = useState<MyTaskNodeDetailVO | null>(null);
  28. const [formData, setFormData] = useState<FormCreateData>({
  29. rule: [],
  30. option: {}
  31. });
  32. const [formLoading, setFormLoading] = useState(false); // 表单加载状态
  33. const [originalFields, setOriginalFields] = useState<string[]>([]); // 保存原始的 fields 数组(JSON 字符串数组)
  34. const [originalConf, setOriginalConf] = useState<string>(''); // 保存原始的 conf(JSON 字符串)
  35. const [detailForm] = AntdForm.useForm();
  36. const [approvalComment, setApprovalComment] = useState(''); // 审核意见
  37. const [approvalLoading, setApprovalLoading] = useState(false); // 审核操作loading状态
  38. const [submitLoading, setSubmitLoading] = useState(false); // 提交操作loading状态
  39. // 组件挂载时打印调试信息
  40. useEffect(() => {
  41. console.log('MyTask 组件已加载');
  42. }, []);
  43. // 监听弹框状态变化
  44. useEffect(() => {
  45. console.log('MyTask: detailVisible 状态变化', detailVisible, 'detailData:', detailData);
  46. }, [detailVisible, detailData]);
  47. // 获取审批状态字典
  48. const getApprovalStatusDictList = async () => {
  49. try {
  50. const { dictDataApi } = await import('../api/DictData');
  51. const response = await dictDataApi.getDictDataPage({
  52. pageNo: 1,
  53. pageSize: -1,
  54. dictType: 'approval_status',
  55. });
  56. const data = (response as any)?.data || response;
  57. const dictList = data?.list || [];
  58. setApprovalStatusDictList(dictList);
  59. console.log('MyTask: 获取审批状态字典成功', dictList);
  60. } catch (error: any) {
  61. console.error('获取审批状态字典失败:', error);
  62. }
  63. };
  64. useEffect(() => {
  65. getApprovalStatusDictList();
  66. }, []);
  67. /** 查询列表 */
  68. const getList = async (params?: MyTaskPageParam) => {
  69. const searchParams = params || queryParams;
  70. setLoading(true);
  71. try {
  72. console.log('MyTask: 开始获取我的任务列表', searchParams);
  73. const response = await myTaskApi.getMyWorkPage(searchParams);
  74. console.log('MyTask: 接口响应', response);
  75. const data = (response as any)?.data || response;
  76. const pageData = (data as PageResponse<MyTaskVO>);
  77. console.log('MyTask: 解析后的数据', pageData);
  78. setList(pageData.list || []);
  79. setTotal(pageData.total || 0);
  80. } catch (error: any) {
  81. console.error('MyTask: 获取我的任务列表失败', error);
  82. toast.error(error.message || '获取我的任务列表失败');
  83. setList([]);
  84. setTotal(0);
  85. } finally {
  86. setLoading(false);
  87. }
  88. };
  89. useEffect(() => {
  90. console.log('MyTask: useEffect 触发,queryParams:', queryParams);
  91. getList();
  92. // eslint-disable-next-line react-hooks/exhaustive-deps
  93. }, [queryParams.pageNo, queryParams.pageSize, queryParams.key]);
  94. // 搜索
  95. const handleSearch = () => {
  96. setQueryParams({
  97. ...queryParams,
  98. pageNo: 1,
  99. key: searchKey.trim() || undefined,
  100. });
  101. };
  102. // 重置
  103. const handleReset = () => {
  104. setSearchKey('');
  105. setQueryParams({
  106. ...queryParams,
  107. pageNo: 1,
  108. key: undefined,
  109. });
  110. };
  111. // 默认表单配置
  112. const defaultFormConfig = {
  113. name: '',
  114. labelPosition: 'right',
  115. formSize: 'middle',
  116. labelSuffix: '',
  117. labelWidth: 100,
  118. hideRequiredMark: false,
  119. showValidationError: true,
  120. inlineValidation: false,
  121. showSubmitButton: false,
  122. showResetButton: false,
  123. };
  124. // 渲染字段预览(支持嵌套结构)
  125. const renderFieldPreview = (field: any, parentSpanStyle?: React.CSSProperties): React.ReactNode => {
  126. const formConfig = formData.option?.formConfig || defaultFormConfig;
  127. const layoutColumns = formConfig.layoutColumns || 1;
  128. const spanStyle = parentSpanStyle || (layoutColumns > 1 ? { gridColumn: `span ${Math.min(layoutColumns, field.span || 1)}` } : undefined);
  129. // 处理容器类型(card 和 grid)
  130. if (field.type === 'card') {
  131. const children = field.children || [];
  132. // 优先使用 label(字段名称),如果没有则使用 cardTitle,最后使用默认值
  133. const cardTitle = field.label || field.cardTitle || '卡片容器';
  134. return (
  135. <div key={field.id} style={spanStyle} className="mb-4">
  136. <Card title={cardTitle} className="w-full">
  137. <div className="space-y-4">
  138. {children.map((child: any) => renderFieldPreview(child))}
  139. </div>
  140. </Card>
  141. </div>
  142. );
  143. }
  144. if (field.type === 'grid') {
  145. const gridColumns = field.gridColumns || 2;
  146. const children = field.children || [];
  147. return (
  148. <div key={field.id} style={spanStyle} className="mb-4">
  149. <div
  150. style={{
  151. display: 'grid',
  152. gridTemplateColumns: `repeat(${gridColumns}, minmax(0, 1fr))`,
  153. gap: '16px',
  154. }}
  155. >
  156. {children.map((child: any) => {
  157. const childSpanStyle = gridColumns > 1
  158. ? { gridColumn: `span ${Math.min(gridColumns, child.span || 1)}` }
  159. : undefined;
  160. return (
  161. <div key={child.id} style={childSpanStyle}>
  162. {renderFieldPreview(child)}
  163. </div>
  164. );
  165. })}
  166. </div>
  167. </div>
  168. );
  169. }
  170. // 处理 alert 类型
  171. if (field.type === 'alert') {
  172. const themeClass =
  173. field.alertTheme === 'dark'
  174. ? 'bg-gray-800 text-white border-gray-700'
  175. : '';
  176. return (
  177. <div key={field.id} className="mb-4" style={spanStyle}>
  178. <Alert
  179. message={field.alertTitle || field.label}
  180. description={field.alertDescription}
  181. type={field.alertType || 'info'}
  182. showIcon={field.alertShowIcon !== false}
  183. closable={field.alertClosable}
  184. closeText={field.alertCloseText}
  185. className={`${field.alertCentered ? 'text-center' : ''} ${themeClass}`}
  186. banner
  187. style={field.style}
  188. />
  189. {field.hint && <div className="text-xs text-gray-400 mt-1">{field.hint}</div>}
  190. </div>
  191. );
  192. }
  193. // 处理普通字段
  194. switch (field.type) {
  195. case 'textarea':
  196. return (
  197. <div key={field.id} style={spanStyle}>
  198. <AntdForm.Item
  199. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  200. name={field.name || field.field}
  201. required={field.required && !formConfig.hideRequiredMark}
  202. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  203. help={field.hint}
  204. >
  205. <Input.TextArea
  206. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  207. rows={4}
  208. allowClear={field.showClear}
  209. maxLength={field.maxLength}
  210. readOnly={field.readOnly}
  211. disabled={field.disabled}
  212. size={field.size || 'middle'}
  213. />
  214. </AntdForm.Item>
  215. </div>
  216. );
  217. case 'password':
  218. return (
  219. <div key={field.id} style={spanStyle}>
  220. <AntdForm.Item
  221. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  222. name={field.name || field.field}
  223. required={field.required && !formConfig.hideRequiredMark}
  224. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  225. help={field.hint}
  226. >
  227. <Input.Password
  228. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  229. allowClear={field.showClear}
  230. maxLength={field.maxLength}
  231. readOnly={field.readOnly}
  232. disabled={field.disabled}
  233. size={field.size || 'middle'}
  234. />
  235. </AntdForm.Item>
  236. </div>
  237. );
  238. case 'number':
  239. return (
  240. <div key={field.id} style={spanStyle}>
  241. <AntdForm.Item
  242. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  243. name={field.name || field.field}
  244. required={field.required && !formConfig.hideRequiredMark}
  245. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  246. help={field.hint}
  247. >
  248. <InputNumber
  249. style={{ width: '100%' }}
  250. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  251. max={field.maxLength}
  252. readOnly={field.readOnly}
  253. disabled={field.disabled}
  254. size={field.size || 'middle'}
  255. />
  256. </AntdForm.Item>
  257. </div>
  258. );
  259. case 'select':
  260. return (
  261. <div key={field.id} style={spanStyle}>
  262. <AntdForm.Item
  263. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  264. name={field.name || field.field}
  265. required={field.required && !formConfig.hideRequiredMark}
  266. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  267. help={field.hint}
  268. >
  269. <Select
  270. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择'}
  271. allowClear={field.showClear}
  272. disabled={field.disabled}
  273. size={field.size || 'middle'}
  274. >
  275. {(field.options || []).map((opt: any, idx: number) => (
  276. <Select.Option key={idx} value={opt.value}>{opt.label}</Select.Option>
  277. ))}
  278. </Select>
  279. </AntdForm.Item>
  280. </div>
  281. );
  282. case 'date':
  283. return (
  284. <div key={field.id} style={spanStyle}>
  285. <AntdForm.Item
  286. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  287. name={field.name || field.field}
  288. required={field.required && !formConfig.hideRequiredMark}
  289. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期' }] : []}
  290. help={field.hint}
  291. >
  292. <DatePicker
  293. style={{ width: '100%' }}
  294. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期'}
  295. allowClear={field.showClear}
  296. disabled={field.disabled}
  297. size={field.size || 'middle'}
  298. />
  299. </AntdForm.Item>
  300. </div>
  301. );
  302. case 'daterange':
  303. return (
  304. <div key={field.id} style={spanStyle}>
  305. <AntdForm.Item
  306. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  307. name={field.name || field.field}
  308. required={field.required && !formConfig.hideRequiredMark}
  309. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期范围' }] : []}
  310. help={field.hint}
  311. >
  312. <DatePicker.RangePicker
  313. style={{ width: '100%' }}
  314. placeholder={Array.isArray(field.placeholder) ? field.placeholder as [string, string] : ['开始日期', '结束日期']}
  315. allowClear={field.showClear}
  316. disabled={field.disabled}
  317. size={field.size || 'middle'}
  318. />
  319. </AntdForm.Item>
  320. </div>
  321. );
  322. case 'datetime':
  323. return (
  324. <div key={field.id} style={spanStyle}>
  325. <AntdForm.Item
  326. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  327. name={field.name || field.field}
  328. required={field.required && !formConfig.hideRequiredMark}
  329. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期时间' }] : []}
  330. help={field.hint}
  331. >
  332. <DatePicker
  333. style={{ width: '100%' }}
  334. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期时间'}
  335. allowClear={field.showClear}
  336. showTime={{ format: 'HH:mm:ss' }}
  337. format="YYYY-MM-DD HH:mm:ss"
  338. disabled={field.disabled}
  339. size={field.size || 'middle'}
  340. />
  341. </AntdForm.Item>
  342. </div>
  343. );
  344. case 'switch':
  345. return (
  346. <div key={field.id} style={spanStyle}>
  347. <AntdForm.Item
  348. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  349. name={field.name || field.field}
  350. required={field.required && !formConfig.hideRequiredMark}
  351. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  352. help={field.hint}
  353. valuePropName="checked"
  354. >
  355. <Switch disabled={field.disabled} />
  356. </AntdForm.Item>
  357. </div>
  358. );
  359. case 'radio':
  360. return (
  361. <div key={field.id} style={spanStyle}>
  362. <AntdForm.Item
  363. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  364. name={field.name || field.field}
  365. required={field.required && !formConfig.hideRequiredMark}
  366. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  367. help={field.hint}
  368. >
  369. <Radio.Group disabled={field.disabled}>
  370. {(field.options || []).map((opt: any, idx: number) => (
  371. <Radio key={idx} value={opt.value}>{opt.label}</Radio>
  372. ))}
  373. </Radio.Group>
  374. </AntdForm.Item>
  375. </div>
  376. );
  377. case 'checkbox':
  378. return (
  379. <div key={field.id} style={spanStyle}>
  380. <AntdForm.Item
  381. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  382. name={field.name || field.field}
  383. required={field.required && !formConfig.hideRequiredMark}
  384. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  385. help={field.hint}
  386. >
  387. <Checkbox.Group disabled={field.disabled}>
  388. {(field.options || []).map((opt: any, idx: number) => (
  389. <Checkbox key={idx} value={opt.value}>{opt.label}</Checkbox>
  390. ))}
  391. </Checkbox.Group>
  392. </AntdForm.Item>
  393. </div>
  394. );
  395. case 'cascader':
  396. return (
  397. <div key={field.id} style={spanStyle}>
  398. <AntdForm.Item
  399. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  400. name={field.name || field.field}
  401. required={field.required && !formConfig.hideRequiredMark}
  402. rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
  403. help={field.hint}
  404. >
  405. <Cascader
  406. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择'}
  407. options={field.cascaderOptions || []}
  408. className="w-full"
  409. allowClear={field.showClear}
  410. disabled={field.disabled}
  411. size={field.size || 'middle'}
  412. />
  413. </AntdForm.Item>
  414. </div>
  415. );
  416. case 'upload':
  417. return (
  418. <div key={field.id} style={spanStyle}>
  419. <AntdForm.Item
  420. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  421. name={field.name || field.field}
  422. required={field.required && !formConfig.hideRequiredMark}
  423. rules={field.required ? [{ required: true, message: field.requiredMessage || '请上传' }] : []}
  424. help={field.hint}
  425. >
  426. <Upload
  427. disabled={field.disabled}
  428. maxCount={field.uploadType === 'single-image' ? 1 : field.maxCount || (field.uploadType === 'multiple-image' ? 9 : undefined)}
  429. accept={field.accept || (field.uploadType === 'single-image' || field.uploadType === 'multiple-image' ? 'image/*' : undefined)}
  430. listType={(field.uploadType === 'file' ? 'text' : 'picture-card') as 'text' | 'picture-card' | 'picture'}
  431. multiple={field.uploadType !== 'single-image'}
  432. >
  433. {field.uploadType === 'file' ? (
  434. <Button icon={<UploadOutlined />}>上传文件</Button>
  435. ) : (
  436. <div>
  437. <UploadOutlined />
  438. <div style={{ marginTop: 8 }}>上传</div>
  439. </div>
  440. )}
  441. </Upload>
  442. </AntdForm.Item>
  443. </div>
  444. );
  445. default:
  446. return (
  447. <div key={field.id} style={spanStyle}>
  448. <AntdForm.Item
  449. label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
  450. name={field.name || field.field}
  451. required={field.required && !formConfig.hideRequiredMark}
  452. rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
  453. help={field.hint}
  454. >
  455. <Input
  456. type={field.inputType || 'text'}
  457. placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
  458. allowClear={field.showClear}
  459. maxLength={field.maxLength}
  460. readOnly={field.readOnly}
  461. disabled={field.disabled}
  462. size={field.size || 'middle'}
  463. />
  464. </AntdForm.Item>
  465. </div>
  466. );
  467. }
  468. };
  469. // 查看节点详情(节点详情弹框)
  470. const handleViewDetail = async (record: MyTaskVO) => {
  471. console.log('MyTask: handleViewDetail 被调用', record);
  472. if (!record.nodeId) {
  473. console.warn('MyTask: 节点ID不存在', record);
  474. message.warning('节点ID不存在');
  475. return;
  476. }
  477. setDetailLoading(true);
  478. setDetailVisible(true); // 先打开弹框显示加载状态
  479. console.log('MyTask: 弹框状态已设置为 true (加载中)');
  480. try {
  481. console.log('MyTask: 开始获取节点详情', { nodeId: record.nodeId });
  482. const response = await myTaskApi.getMyWorkNodeDetail(record.nodeId);
  483. console.log('MyTask: 节点详情响应', response);
  484. let data: any = response;
  485. // 直接从第一层 data.formId 获取 formId
  486. let extractedFormId: number | undefined = undefined;
  487. if (data?.formId !== undefined && data?.formId !== null) {
  488. const formIdValue = data.formId;
  489. if (typeof formIdValue === 'string') {
  490. const parsed = parseInt(formIdValue, 10);
  491. if (!isNaN(parsed)) {
  492. extractedFormId = parsed;
  493. console.log('MyTask: ✅ 从 data.formId (字符串) 提取到 formId', extractedFormId);
  494. }
  495. } else if (typeof formIdValue === 'number') {
  496. extractedFormId = formIdValue;
  497. console.log('MyTask: ✅ 从 data.formId (数字) 提取到 formId', extractedFormId);
  498. }
  499. }
  500. console.log('MyTask: formId 提取结果', {
  501. extractedFormId,
  502. '原始 data.formId': data?.formId,
  503. 'data.formId 类型': typeof data?.formId
  504. });
  505. if (extractedFormId === undefined) {
  506. console.warn('MyTask: ⚠️ 未能提取到 formId', {
  507. 'data.formId': data?.formId,
  508. 'data': data
  509. });
  510. }
  511. // 合并列表数据中的作业信息(作业名称、编号、负责人、时间等)
  512. // 直接使用第一层 data,不解析 data.data
  513. const detailDataWithWorkInfo: MyTaskNodeDetailVO = {
  514. ...(data || {}),
  515. workName: record.name || data?.workName || data?.name,
  516. orderNo: record.orderNo || data?.orderNo,
  517. workerUserName: record.workerUserName || data?.workerUserName,
  518. workTime: record.workTime || data?.workTime,
  519. // 确保 type 字段存在,使用第一层 data.type
  520. type: data?.type || data?.nodeType || '',
  521. nodeType: data?.nodeType || data?.type || '', // 同时设置 nodeType 作为兼容
  522. // 使用提取到的 formId
  523. formId: extractedFormId,
  524. };
  525. console.log('MyTask: 合并后的详情数据 - type 和 formId 字段', {
  526. 'data.type': data?.type,
  527. 'data.nodeType': data?.nodeType,
  528. 'data.formId': data?.formId,
  529. 'detailDataWithWorkInfo.type': detailDataWithWorkInfo.type,
  530. 'detailDataWithWorkInfo.nodeType': detailDataWithWorkInfo.nodeType,
  531. 'detailDataWithWorkInfo.formId': detailDataWithWorkInfo.formId,
  532. });
  533. console.log('MyTask: 合并后的详情数据', detailDataWithWorkInfo);
  534. console.log('MyTask: 节点类型', detailDataWithWorkInfo.type);
  535. // 先设置数据,确保弹框有内容显示
  536. setDetailData(detailDataWithWorkInfo);
  537. console.log('MyTask: detailData 已设置', detailDataWithWorkInfo);
  538. // 确保弹框是打开状态
  539. setDetailVisible(true);
  540. setDetailLoading(false);
  541. console.log('MyTask: 弹框状态设置为 true,loading 设置为 false');
  542. // 根据节点类型决定是否需要获取表单
  543. // isolation、releaseIsolation 和 returnLock 不需要表单,其他类型(review 和其他)需要根据 formId 获取表单
  544. const nodeType = detailDataWithWorkInfo.type || detailDataWithWorkInfo.nodeType || '';
  545. const isIsolation = nodeType === 'isolation' || nodeType === '隔离' || nodeType === '隔离/方案';
  546. const isReleaseIsolation = nodeType === 'releaseIsolation' || nodeType === '解除隔离';
  547. const isReturnLock = nodeType === 'returnLock';
  548. // 检查任务状态是否为"已通过"
  549. const isApproved = detailDataWithWorkInfo.approvalStatus === 'approved';
  550. // 获取 formId(可能为 0,所以需要明确检查 undefined 和 null)
  551. const formId = detailDataWithWorkInfo.formId;
  552. // formId 可能是数字(包括 0)或字符串,只要不是 undefined、null 或空字符串,就认为有 formId
  553. const hasFormId = formId !== undefined && formId !== null && formId !== '' && (typeof formId === 'number' || (typeof formId === 'string' && formId.trim() !== ''));
  554. // 检查是否有 formData(已提交的表单数据)
  555. const hasFormData = detailDataWithWorkInfo.formData && (
  556. (typeof detailDataWithWorkInfo.formData === 'string' && detailDataWithWorkInfo.formData.trim() !== '') ||
  557. (typeof detailDataWithWorkInfo.formData === 'object' && Object.keys(detailDataWithWorkInfo.formData).length > 0)
  558. );
  559. console.log('MyTask: 表单获取判断', {
  560. nodeType,
  561. isIsolation,
  562. isReleaseIsolation,
  563. isReturnLock,
  564. isApproved,
  565. hasFormData,
  566. formId,
  567. formIdType: typeof formId,
  568. hasFormId,
  569. '从formData获取': isApproved && hasFormData,
  570. '从formId获取': !isIsolation && !isReleaseIsolation && !isReturnLock && hasFormId && !(isApproved && hasFormData),
  571. 'detailDataWithWorkInfo': detailDataWithWorkInfo,
  572. });
  573. // 如果任务状态为"已通过"且有 formData,则从 formData 中解析表单结构
  574. if (isApproved && hasFormData) {
  575. console.log('MyTask: ✅ 任务已通过,从 formData 中解析表单结构');
  576. setFormLoading(true);
  577. try {
  578. // 解析 formData(可能是 JSON 字符串或对象)
  579. let parsedFormData: any;
  580. if (typeof detailDataWithWorkInfo.formData === 'string') {
  581. parsedFormData = JSON.parse(detailDataWithWorkInfo.formData);
  582. } else {
  583. parsedFormData = detailDataWithWorkInfo.formData;
  584. }
  585. console.log('MyTask: 解析后的 formData', parsedFormData);
  586. // 从 formData 中提取 conf 和 fields
  587. const conf = parsedFormData.conf;
  588. const fields = parsedFormData.fields;
  589. if (conf && fields) {
  590. // 保存原始的 conf 和 fields(JSON 字符串格式)
  591. const confString = typeof conf === 'string' ? conf : JSON.stringify(conf);
  592. const fieldsArray = Array.isArray(fields) ? fields : [];
  593. const fieldsStringArray = fieldsArray.map((field: any) => {
  594. return typeof field === 'string' ? field : JSON.stringify(field);
  595. });
  596. setOriginalConf(confString);
  597. setOriginalFields(fieldsStringArray);
  598. console.log('MyTask: 从 formData 保存原始表单数据', {
  599. conf: confString,
  600. fieldsCount: fieldsStringArray.length,
  601. fields: fieldsStringArray
  602. });
  603. // 解析 fields 中的每个字段,提取 value 值用于回显
  604. const formValues: any = {};
  605. fieldsStringArray.forEach((fieldString: string) => {
  606. try {
  607. const fieldObj = typeof fieldString === 'string' ? JSON.parse(fieldString) : fieldString;
  608. const fieldName = fieldObj.name || fieldObj.field;
  609. if (fieldName && fieldObj.value !== undefined && fieldObj.value !== null) {
  610. formValues[fieldName] = fieldObj.value;
  611. }
  612. } catch (e) {
  613. console.error('MyTask: 解析字段 JSON 失败', e, fieldString);
  614. }
  615. });
  616. console.log('MyTask: 从 formData 提取的表单值', formValues);
  617. // 设置表单配置和字段
  618. setConfAndFields2(setFormData, conf, fields);
  619. console.log('MyTask: 表单配置已设置(从 formData)');
  620. // 回填表单值
  621. setTimeout(() => {
  622. detailForm.setFieldsValue(formValues);
  623. console.log('MyTask: 表单数据已回填(从 formData)', formValues);
  624. }, 100);
  625. } else {
  626. console.warn('MyTask: formData 中缺少 conf 或 fields', { conf: !!conf, fields: !!fields });
  627. message.warning('表单数据不完整');
  628. }
  629. } catch (e) {
  630. console.error('MyTask: 解析 formData 失败', e);
  631. message.error('解析表单数据失败: ' + (e instanceof Error ? e.message : String(e)));
  632. } finally {
  633. setFormLoading(false);
  634. }
  635. }
  636. // 如果不是隔离类型,且有表单ID,且不是从 formData 获取,则从接口获取表单配置
  637. else if (!isIsolation && !isReleaseIsolation && !isReturnLock && hasFormId) {
  638. console.log('MyTask: ✅ 满足条件,开始获取表单', { formId, nodeType });
  639. // 重置表单数据
  640. setFormData({ rule: [], option: {} });
  641. setOriginalFields([]);
  642. setOriginalConf('');
  643. setFormLoading(true);
  644. // 立即调用接口,不使用 setTimeout,确保接口被调用
  645. (async () => {
  646. try {
  647. // 确保 formId 是数字类型
  648. const numericFormId = typeof formId === 'string' ? parseInt(formId, 10) : formId;
  649. if (isNaN(numericFormId)) {
  650. console.error('MyTask: formId 不是有效数字', formId);
  651. message.error('表单ID无效');
  652. setFormLoading(false);
  653. return;
  654. }
  655. console.log('MyTask: 🚀 开始调用表单接口', { formId: numericFormId, nodeType, url: `/bpm/form/get?id=${numericFormId}` });
  656. const FormApi = await import('../api/bpm/form');
  657. const formDetailResponse = await FormApi.getForm(numericFormId);
  658. console.log('MyTask: ✅ 表单接口调用成功,原始响应', formDetailResponse);
  659. // 处理响应数据(可能包含 code 和 data 包装)
  660. let formDetail: any = formDetailResponse;
  661. if (formDetailResponse && typeof formDetailResponse === 'object' && 'data' in formDetailResponse) {
  662. // 如果响应有 data 字段,使用 data
  663. formDetail = (formDetailResponse as any).data || formDetailResponse;
  664. }
  665. console.log('MyTask: 处理后的表单详情', formDetail);
  666. // 获取 conf 和 fields
  667. const conf = formDetail?.conf || formDetail?.formConfig;
  668. const fields = formDetail?.fields || formDetail?.formFields;
  669. console.log('MyTask: 表单配置和字段', {
  670. hasConf: !!conf,
  671. confType: typeof conf,
  672. hasFields: !!fields,
  673. fieldsType: Array.isArray(fields) ? 'array' : typeof fields,
  674. fieldsLength: Array.isArray(fields) ? fields.length : 0
  675. });
  676. // 解析表单配置和字段
  677. if (conf && fields) {
  678. // 保存原始的 conf 和 fields(JSON 字符串格式)
  679. const confString = typeof conf === 'string' ? conf : JSON.stringify(conf);
  680. const fieldsArray = Array.isArray(fields) ? fields : [];
  681. const fieldsStringArray = fieldsArray.map((field: any) => {
  682. return typeof field === 'string' ? field : JSON.stringify(field);
  683. });
  684. setOriginalConf(confString);
  685. setOriginalFields(fieldsStringArray);
  686. console.log('MyTask: 保存原始表单数据', {
  687. conf: confString,
  688. fieldsCount: fieldsStringArray.length,
  689. fields: fieldsStringArray
  690. });
  691. // setConfAndFields2 会自动处理 JSON 字符串解析
  692. setConfAndFields2(setFormData, conf, fields);
  693. console.log('MyTask: 表单配置已设置', {
  694. conf: typeof conf === 'string' ? 'JSON字符串' : '对象',
  695. fieldsCount: Array.isArray(fields) ? fields.length : 0
  696. });
  697. // 如果有表单数据值,回填到表单(在表单配置设置后)
  698. if (detailDataWithWorkInfo.formData) {
  699. try {
  700. const formValues = typeof detailDataWithWorkInfo.formData === 'string'
  701. ? JSON.parse(detailDataWithWorkInfo.formData)
  702. : detailDataWithWorkInfo.formData;
  703. // 延迟一下,确保表单字段已经渲染
  704. setTimeout(() => {
  705. detailForm.setFieldsValue(formValues);
  706. console.log('MyTask: 表单数据已回填', formValues);
  707. }, 100);
  708. } catch (e) {
  709. console.error('MyTask: 解析表单数据失败', e);
  710. }
  711. }
  712. } else {
  713. console.warn('MyTask: 表单详情缺少配置或字段', { conf: !!conf, fields: !!fields, formDetail });
  714. message.warning('表单配置不完整');
  715. }
  716. } catch (e) {
  717. console.error('MyTask: 获取表单详情失败', e);
  718. message.error('获取表单详情失败: ' + (e instanceof Error ? e.message : String(e)));
  719. // 即使获取表单失败,也继续显示弹框
  720. } finally {
  721. setFormLoading(false);
  722. }
  723. })();
  724. } else {
  725. setFormLoading(false);
  726. if (isIsolation || isReleaseIsolation || isReturnLock) {
  727. console.log('MyTask: ⚠️ 隔离类型节点,跳过表单配置获取', { nodeType, isIsolation, isReleaseIsolation, isReturnLock });
  728. } else if (!hasFormId) {
  729. console.warn('MyTask: ⚠️ 没有表单ID,跳过表单配置获取', {
  730. formId,
  731. formIdType: typeof formId,
  732. hasFormId,
  733. nodeType,
  734. 'detailDataWithWorkInfo': detailDataWithWorkInfo
  735. });
  736. message.warning(`节点类型 ${nodeType} 缺少表单ID,无法加载表单`);
  737. } else {
  738. console.warn('MyTask: ⚠️ 未知原因未获取表单', {
  739. nodeType,
  740. isIsolation,
  741. isReleaseIsolation,
  742. isReturnLock,
  743. hasFormId,
  744. formId
  745. });
  746. }
  747. }
  748. // 注意:表单数据回填已移到表单配置加载完成后,确保表单字段已渲染
  749. // 回显审核意见(如果有 approvalOpinion 字段,且不是 "pending")
  750. if (detailDataWithWorkInfo.approvalOpinion && detailDataWithWorkInfo.approvalOpinion !== 'pending') {
  751. setApprovalComment(detailDataWithWorkInfo.approvalOpinion);
  752. console.log('MyTask: 回显审核意见', detailDataWithWorkInfo.approvalOpinion);
  753. } else {
  754. setApprovalComment(''); // 重置审核意见(包括 pending 的情况)
  755. }
  756. setDetailLoading(false);
  757. console.log('MyTask: 弹框已打开', { detailVisible: true, detailData: detailDataWithWorkInfo });
  758. } catch (error: any) {
  759. console.error('MyTask: 获取节点详情失败', error);
  760. toast.error(error.message || '获取节点详情失败');
  761. // 即使接口失败,也显示弹框(显示错误信息)
  762. if (!detailData) {
  763. setDetailData({
  764. id: record.nodeId,
  765. nodeId: record.nodeId,
  766. workId: record.workId,
  767. workName: record.name,
  768. orderNo: record.orderNo,
  769. workerUserName: record.workerUserName,
  770. workTime: record.workTime,
  771. type: '',
  772. } as MyTaskNodeDetailVO);
  773. }
  774. setDetailVisible(true);
  775. } finally {
  776. setDetailLoading(false);
  777. }
  778. };
  779. // 获取任务状态显示文本(使用 approval_status 字典)
  780. const getTaskStatusText = (status: string | number | undefined): string => {
  781. if (!status) return '未知';
  782. const statusStr = String(status).toLowerCase();
  783. const statusItem = approvalStatusDictList.find(item => String(item.value).toLowerCase() === statusStr);
  784. if (statusItem) {
  785. return statusItem.label || statusItem.name || '未知';
  786. }
  787. // 如果没有找到字典值,使用默认映射
  788. const statusMap: Record<string, string> = {
  789. 'pending': '待审核',
  790. 'approved': '已通过',
  791. 'rejected': '已驳回',
  792. 'unaudited': '未审核',
  793. };
  794. return statusMap[statusStr] || '未知';
  795. };
  796. // 获取任务状态样式(审批状态)
  797. const getTaskStatusClassName = (status: string | number | undefined): string => {
  798. if (!status) return 'bg-gray-100 text-gray-600';
  799. const statusStr = String(status).toLowerCase();
  800. const statusMap: Record<string, string> = {
  801. 'pending': 'bg-yellow-100 text-yellow-700',
  802. 'approved': 'bg-green-100 text-green-700',
  803. 'rejected': 'bg-red-100 text-red-700',
  804. 'unaudited': 'bg-gray-100 text-gray-600',
  805. };
  806. return statusMap[statusStr] || 'bg-gray-100 text-gray-600';
  807. };
  808. // 表格列配置
  809. const columns: ColumnsType<MyTaskVO> = [
  810. {
  811. title: '作业编号',
  812. dataIndex: 'orderNo',
  813. width: 180,
  814. align: 'center',
  815. render: (text: string) => text || '-',
  816. },
  817. {
  818. title: '作业名称',
  819. dataIndex: 'name',
  820. width: 220,
  821. align: 'center',
  822. ellipsis: true,
  823. },
  824. {
  825. title: '负责人',
  826. dataIndex: 'workerUserName',
  827. width: 150,
  828. align: 'center',
  829. render: (text: string, record: MyTaskVO) => {
  830. // 优先使用 workerUserName,如果没有则尝试从其他字段获取
  831. return text || record.responsibleName || record.responsible || record.initiatorName || record.initiator || '-';
  832. },
  833. },
  834. {
  835. title: '当前任务',
  836. dataIndex: 'currentNodeName',
  837. width: 180,
  838. align: 'center',
  839. render: (text: string, record: MyTaskVO) => {
  840. // 优先使用 currentNodeName,如果没有则使用 currentNode
  841. return text || record.currentNode || '-';
  842. },
  843. },
  844. {
  845. title: '任务开始时间',
  846. dataIndex: 'workTime',
  847. width: 180,
  848. align: 'center',
  849. render: (text: string | number | Date | undefined, record: MyTaskVO) => {
  850. // 优先使用 workTime,如果没有则使用其他时间字段
  851. const time = text || record.taskStartTime || record.initiationTime || record.initiateTime;
  852. return time ? dateFormatter(time) : '-';
  853. },
  854. },
  855. {
  856. title: '任务状态',
  857. dataIndex: 'approvalStatus',
  858. width: 120,
  859. align: 'center',
  860. render: (status: string | number | undefined, record: MyTaskVO) => {
  861. // 优先使用 approvalStatus,如果没有则使用其他状态字段
  862. const taskStatus = status || record.taskStatus || record.status;
  863. return (
  864. <span
  865. className={`inline-flex px-3 py-1 rounded-lg text-xs ${getTaskStatusClassName(taskStatus)}`}
  866. >
  867. {getTaskStatusText(taskStatus)}
  868. </span>
  869. );
  870. },
  871. },
  872. {
  873. title: '操作',
  874. width: 120,
  875. align: 'center',
  876. fixed: 'right',
  877. render: (_: any, record: MyTaskVO) => (
  878. <Space size="small">
  879. <Button
  880. type="link"
  881. size="small"
  882. icon={<Eye className="w-4 h-4" />}
  883. onClick={(e) => {
  884. e.stopPropagation();
  885. console.log('MyTask: 查看详情按钮被点击', record);
  886. handleViewDetail(record);
  887. }}
  888. >
  889. 查看详情
  890. </Button>
  891. </Space>
  892. ),
  893. },
  894. ];
  895. // 计算分页数据
  896. const totalPages = Math.ceil(total / queryParams.pageSize) || 1;
  897. return (
  898. <div className="space-y-6">
  899. {/* 操作栏和表格容器 */}
  900. <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
  901. {/* 查询与操作栏 */}
  902. <div className="p-4 lg:p-5 border-b border-gray-200/50">
  903. <div className="flex flex-wrap items-center gap-3">
  904. <Input
  905. value={searchKey}
  906. onChange={(e) => setSearchKey(e.target.value)}
  907. placeholder="请输入作业编号或者作业名称进行查询"
  908. allowClear
  909. className="flex-1"
  910. style={{ minWidth: 200 }}
  911. onPressEnter={handleSearch}
  912. />
  913. <Space size="small">
  914. <Button type="primary" icon={<Search className="w-4 h-4" />} onClick={handleSearch}>
  915. 搜索
  916. </Button>
  917. <Button icon={<RotateCcw className="w-4 h-4" />} onClick={handleReset}>
  918. 重置
  919. </Button>
  920. </Space>
  921. </div>
  922. </div>
  923. {/* 表格容器 */}
  924. <div className="overflow-hidden min-w-0">
  925. <AntdTable
  926. loading={loading}
  927. columns={columns}
  928. dataSource={list}
  929. rowKey={(record) => record.id || Math.random()}
  930. pagination={false}
  931. scroll={{ x: 'max-content' }}
  932. locale={{
  933. emptyText: '暂无数据',
  934. }}
  935. />
  936. </div>
  937. </div>
  938. {/* 分页 */}
  939. {total > 0 && (
  940. <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
  941. <div className="flex items-center justify-between">
  942. <div className="text-sm text-gray-600">
  943. 共 <span className="text-blue-600 font-medium">{total}</span> 条记录
  944. </div>
  945. <div className="flex gap-2">
  946. <Button
  947. onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo - 1 })}
  948. disabled={queryParams.pageNo <= 1}
  949. >
  950. 上一页
  951. </Button>
  952. <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
  953. {queryParams.pageNo} / {totalPages}
  954. </span>
  955. <Button
  956. onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo + 1 })}
  957. disabled={queryParams.pageNo >= totalPages}
  958. >
  959. 下一页
  960. </Button>
  961. </div>
  962. </div>
  963. </div>
  964. )}
  965. {/* 详情弹框 */}
  966. <Modal
  967. title={null}
  968. open={detailVisible}
  969. onCancel={() => {
  970. console.log('MyTask: 弹框关闭');
  971. setDetailVisible(false);
  972. setDetailData(null);
  973. setFormData({ rule: [], option: {} });
  974. setFormLoading(false);
  975. setOriginalFields([]);
  976. setOriginalConf('');
  977. detailForm.resetFields();
  978. setApprovalComment('');
  979. }}
  980. footer={null}
  981. width={800}
  982. destroyOnClose
  983. confirmLoading={detailLoading}
  984. maskClosable={false}
  985. zIndex={1000}
  986. styles={{
  987. body: {
  988. minHeight: '500px',
  989. maxHeight: '600px',
  990. height: '600px',
  991. padding: 0,
  992. display: 'flex',
  993. flexDirection: 'column',
  994. overflow: 'hidden'
  995. }
  996. }}
  997. >
  998. {(() => {
  999. console.log('MyTask: Modal 内容渲染', { detailLoading, detailData, detailVisible });
  1000. if (detailLoading) {
  1001. return <div className="py-8 text-center">加载中...</div>;
  1002. }
  1003. if (detailData) {
  1004. return (
  1005. <div style={{
  1006. display: 'flex',
  1007. flexDirection: 'column',
  1008. height: '100%',
  1009. overflow: 'hidden'
  1010. }}>
  1011. {/* 标题区域 */}
  1012. <div className="mb-4" style={{ padding: '20px 24px 0' }}>
  1013. <h2 className="text-xl font-semibold text-gray-900 mb-2">
  1014. {detailData.workName || detailData.name || '作业详情'}
  1015. </h2>
  1016. <div className="text-sm flex gap-4" style={{ color: '#898f9a' }}>
  1017. <span>作业编号:{detailData.orderNo || '-'}</span>
  1018. <span>负责人:{detailData.workerUserName || '-'}</span>
  1019. <span>时间:{detailData.workTime ? dateFormatter(detailData.workTime) : '-'}</span>
  1020. </div>
  1021. </div>
  1022. {/* 内容区域 - 可滚动 */}
  1023. <div className="mt-6" style={{
  1024. padding: '0 24px',
  1025. flex: 1,
  1026. overflowY: 'auto',
  1027. minHeight: 0
  1028. }}>
  1029. {(() => {
  1030. // 检查节点状态是否为已通过(approved),如果是则禁用所有输入和按钮
  1031. const isApproved = detailData?.approvalStatus === 'approved';
  1032. // 从 detailData.type 或 detailData.nodeType 获取节点类型
  1033. const nodeType = String(detailData?.type || detailData?.nodeType || '').trim();
  1034. console.log('MyTask: 渲染内容区域 - 节点类型', nodeType, 'detailData:', detailData);
  1035. const isReview = nodeType === 'review';
  1036. const isIsolation = nodeType === 'isolation';
  1037. const isReleaseIsolation = nodeType === 'releaseIsolation';
  1038. const isReturnLock = nodeType === 'returnLock';
  1039. console.log('MyTask: 节点类型判断结果', {
  1040. isReview,
  1041. isIsolation,
  1042. isReleaseIsolation,
  1043. isReturnLock,
  1044. nodeType,
  1045. 'detailData.type': detailData.type,
  1046. 'detailData.nodeType': detailData.nodeType,
  1047. 'typeof detailData.type': typeof detailData.type,
  1048. 'String(detailData.type)': String(detailData.type)
  1049. });
  1050. // 隔离/方案节点、解除隔离节点和还锁节点
  1051. if (isIsolation || isReleaseIsolation || isReturnLock) {
  1052. console.log('MyTask: ✅ 进入隔离节点渲染分支', { isIsolation, isReleaseIsolation, isReturnLock, nodeType });
  1053. const isolationContent = (
  1054. <div
  1055. key="isolation-content"
  1056. style={{
  1057. display: 'flex',
  1058. justifyContent: 'center',
  1059. alignItems: 'center',
  1060. minHeight: '400px',
  1061. padding: '40px 20px'
  1062. }}
  1063. >
  1064. <Card
  1065. style={{
  1066. width: '100%',
  1067. maxWidth: '500px',
  1068. textAlign: 'center',
  1069. borderRadius: '12px',
  1070. boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
  1071. border: '1px solid #e8e8e8'
  1072. }}
  1073. bodyStyle={{
  1074. padding: '40px 30px'
  1075. }}
  1076. >
  1077. <div style={{ marginBottom: '24px' }}>
  1078. <LockOutlined
  1079. style={{
  1080. fontSize: '64px',
  1081. color: '#1890ff',
  1082. display: 'block'
  1083. }}
  1084. />
  1085. </div>
  1086. <div
  1087. style={{
  1088. fontSize: '20px',
  1089. fontWeight: 500,
  1090. color: '#333',
  1091. lineHeight: '1.6'
  1092. }}
  1093. >
  1094. 请前往锁控柜进行取锁,取钥匙操作
  1095. </div>
  1096. </Card>
  1097. </div>
  1098. );
  1099. console.log('MyTask: 隔离节点内容已创建', isolationContent);
  1100. return isolationContent;
  1101. }
  1102. // 审核类型节点 (review)
  1103. if (isReview) {
  1104. return (
  1105. <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
  1106. <div className="space-y-6" style={{ padding: '0 24px', flex: 1, overflowY: 'auto', minHeight: 0 }}>
  1107. {/* 自定义表单 */}
  1108. {formLoading ? (
  1109. <div className="py-8 text-center text-gray-500">表单加载中...</div>
  1110. ) : formData.rule && formData.rule.length > 0 ? (
  1111. <div>
  1112. {(() => {
  1113. const formConfig = formData.option?.formConfig || defaultFormConfig;
  1114. const layoutColumns = formConfig.layoutColumns || 1;
  1115. const gridStyle = layoutColumns > 1 ? {
  1116. display: 'grid',
  1117. gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
  1118. gap: '12px',
  1119. rowGap: '16px',
  1120. } : undefined;
  1121. return (
  1122. <AntdForm
  1123. form={detailForm}
  1124. layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
  1125. size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
  1126. requiredMark={formConfig.hideRequiredMark ? false : undefined}
  1127. labelCol={formConfig.labelWidth ? {
  1128. style: {
  1129. width: `${formConfig.labelWidth}px`,
  1130. textAlign: formConfig.labelPosition === 'left' ? 'left' : formConfig.labelPosition === 'right' ? 'right' : 'left'
  1131. }
  1132. } : undefined}
  1133. >
  1134. <div
  1135. style={gridStyle}
  1136. className={layoutColumns === 1 ? 'space-y-4' : 'form-detail-grid'}
  1137. >
  1138. <style>{`
  1139. .form-detail-grid .ant-form-item {
  1140. margin-bottom: 12px;
  1141. }
  1142. `}</style>
  1143. {(formData.rule || []).map((field: any) => {
  1144. // 如果已通过,禁用所有字段
  1145. const fieldWithDisabled = isApproved ? { ...field, disabled: true, readOnly: true } : field;
  1146. return renderFieldPreview(fieldWithDisabled);
  1147. })}
  1148. </div>
  1149. </AntdForm>
  1150. );
  1151. })()}
  1152. </div>
  1153. ) : (
  1154. <div className="py-4 text-center text-gray-400 text-sm">暂无表单内容</div>
  1155. )}
  1156. {/* 审核意见 - label 和 textarea 同一行 */}
  1157. <div className="flex items-start" style={{ gap: '16px' }}>
  1158. <label
  1159. className="text-sm font-medium text-gray-700 whitespace-nowrap pt-2"
  1160. style={{
  1161. width: defaultFormConfig.labelWidth ? `${defaultFormConfig.labelWidth - 20}px` : '80px',
  1162. textAlign: defaultFormConfig.labelPosition === 'left' ? 'left' : defaultFormConfig.labelPosition === 'right' ? 'right' : 'left',
  1163. flexShrink: 0
  1164. }}
  1165. >
  1166. 审核意见
  1167. </label>
  1168. <div className="flex-1">
  1169. <Input.TextArea
  1170. rows={4}
  1171. value={approvalComment}
  1172. onChange={(e) => setApprovalComment(e.target.value)}
  1173. placeholder="请输入审核意见"
  1174. maxLength={500}
  1175. showCount
  1176. disabled={isApproved}
  1177. readOnly={isApproved}
  1178. />
  1179. </div>
  1180. </div>
  1181. </div>
  1182. {/* 底部按钮 */}
  1183. <div
  1184. className="flex justify-end gap-3"
  1185. style={{
  1186. padding: '16px 24px',
  1187. borderTop: '1px solid #f0f0f0',
  1188. flexShrink: 0
  1189. }}
  1190. >
  1191. <Button
  1192. danger
  1193. loading={approvalLoading}
  1194. disabled={isApproved}
  1195. onClick={async () => {
  1196. const nodeId = detailData.nodeId || detailData.id;
  1197. if (!nodeId) {
  1198. message.error('节点ID不存在');
  1199. return;
  1200. }
  1201. // 如果有自定义表单,先校验表单
  1202. if (formData.rule && formData.rule.length > 0) {
  1203. try {
  1204. // 校验表单所有字段
  1205. await detailForm.validateFields();
  1206. console.log('MyTask: 表单校验通过');
  1207. } catch (error: any) {
  1208. // 校验失败,显示错误信息
  1209. console.error('MyTask: 表单校验失败', error);
  1210. if (error.errorFields && error.errorFields.length > 0) {
  1211. // 获取第一个错误字段的提示信息
  1212. const firstError = error.errorFields[0];
  1213. const errorMessage = firstError.errors?.[0] || '请完善表单内容';
  1214. message.error(errorMessage);
  1215. } else {
  1216. message.error('请完善表单内容');
  1217. }
  1218. return; // 阻止提交
  1219. }
  1220. }
  1221. setApprovalLoading(true);
  1222. try {
  1223. const params: UpdateNodeApprovalParam = {
  1224. nodeId: nodeId,
  1225. approvalStatus: 'rejected',
  1226. approvalOpinion: approvalComment || undefined,
  1227. };
  1228. console.log('MyTask: 调用审核不通过接口', params);
  1229. await myTaskApi.updateNodeApproval(params);
  1230. message.success('审核不通过操作成功');
  1231. // 关闭弹框
  1232. setDetailVisible(false);
  1233. setDetailData(null);
  1234. setFormData({ rule: [], option: {} });
  1235. setFormLoading(false);
  1236. setOriginalFields([]);
  1237. setOriginalConf('');
  1238. detailForm.resetFields();
  1239. setApprovalComment('');
  1240. // 刷新列表
  1241. getList();
  1242. } catch (error: any) {
  1243. console.error('MyTask: 审核不通过失败', error);
  1244. message.error(error?.message || '审核不通过操作失败');
  1245. } finally {
  1246. setApprovalLoading(false);
  1247. }
  1248. }}
  1249. >
  1250. 审核不通过
  1251. </Button>
  1252. <Button
  1253. type="primary"
  1254. loading={approvalLoading}
  1255. disabled={isApproved}
  1256. onClick={async () => {
  1257. const nodeId = detailData.nodeId || detailData.id;
  1258. if (!nodeId) {
  1259. message.error('节点ID不存在');
  1260. return;
  1261. }
  1262. // 如果有自定义表单,先校验表单
  1263. if (formData.rule && formData.rule.length > 0) {
  1264. try {
  1265. // 校验表单所有字段
  1266. await detailForm.validateFields();
  1267. console.log('MyTask: 表单校验通过');
  1268. } catch (error: any) {
  1269. // 校验失败,显示错误信息
  1270. console.error('MyTask: 表单校验失败', error);
  1271. if (error.errorFields && error.errorFields.length > 0) {
  1272. // 获取第一个错误字段的提示信息
  1273. const firstError = error.errorFields[0];
  1274. const errorMessage = firstError.errors?.[0] || '请完善表单内容';
  1275. message.error(errorMessage);
  1276. } else {
  1277. message.error('请完善表单内容');
  1278. }
  1279. return; // 阻止提交
  1280. }
  1281. }
  1282. setApprovalLoading(true);
  1283. try {
  1284. const params: UpdateNodeApprovalParam = {
  1285. nodeId: nodeId,
  1286. approvalStatus: 'approved',
  1287. approvalOpinion: approvalComment || undefined,
  1288. };
  1289. console.log('MyTask: 调用审核通过接口', params);
  1290. await myTaskApi.updateNodeApproval(params);
  1291. message.success('审核通过操作成功');
  1292. // 关闭弹框
  1293. setDetailVisible(false);
  1294. setDetailData(null);
  1295. setFormData({ rule: [], option: {} });
  1296. setFormLoading(false);
  1297. setOriginalFields([]);
  1298. setOriginalConf('');
  1299. detailForm.resetFields();
  1300. setApprovalComment('');
  1301. // 刷新列表
  1302. getList();
  1303. } catch (error: any) {
  1304. console.error('MyTask: 审核通过失败', error);
  1305. message.error(error?.message || '审核通过操作失败');
  1306. } finally {
  1307. setApprovalLoading(false);
  1308. }
  1309. }}
  1310. >
  1311. 审核通过
  1312. </Button>
  1313. </div>
  1314. </div>
  1315. );
  1316. }
  1317. // 其他节点类型(需要根据 formId 获取表单)
  1318. return (
  1319. <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
  1320. <div className="space-y-6" style={{ padding: '0 24px', flex: 1, overflowY: 'auto', minHeight: 0 }}>
  1321. {/* 自定义表单 */}
  1322. {formLoading ? (
  1323. <div className="py-8 text-center text-gray-500">表单加载中...</div>
  1324. ) : formData.rule && formData.rule.length > 0 ? (
  1325. <div>
  1326. {(() => {
  1327. const formConfig = formData.option?.formConfig || defaultFormConfig;
  1328. const layoutColumns = formConfig.layoutColumns || 1;
  1329. const gridStyle = layoutColumns > 1 ? {
  1330. display: 'grid',
  1331. gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
  1332. gap: '12px',
  1333. rowGap: '16px',
  1334. } : undefined;
  1335. return (
  1336. <AntdForm
  1337. form={detailForm}
  1338. layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
  1339. size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
  1340. requiredMark={formConfig.hideRequiredMark ? false : undefined}
  1341. labelCol={formConfig.labelWidth ? {
  1342. style: {
  1343. width: `${formConfig.labelWidth}px`,
  1344. textAlign: formConfig.labelPosition === 'left' ? 'left' : formConfig.labelPosition === 'right' ? 'right' : 'left'
  1345. }
  1346. } : undefined}
  1347. >
  1348. <div
  1349. style={gridStyle}
  1350. className={layoutColumns === 1 ? 'space-y-4' : 'form-detail-grid'}
  1351. >
  1352. <style>{`
  1353. .form-detail-grid .ant-form-item {
  1354. margin-bottom: 12px;
  1355. }
  1356. `}</style>
  1357. {(formData.rule || []).map((field: any) => {
  1358. // 如果已通过,禁用所有字段
  1359. const fieldWithDisabled = isApproved ? { ...field, disabled: true, readOnly: true } : field;
  1360. return renderFieldPreview(fieldWithDisabled);
  1361. })}
  1362. </div>
  1363. </AntdForm>
  1364. );
  1365. })()}
  1366. </div>
  1367. ) : (
  1368. <div className="py-4 text-center text-gray-400 text-sm">暂无表单内容</div>
  1369. )}
  1370. </div>
  1371. {/* 底部按钮 */}
  1372. <div
  1373. className="flex justify-end gap-3"
  1374. style={{
  1375. padding: '16px 24px',
  1376. borderTop: '1px solid #f0f0f0',
  1377. flexShrink: 0
  1378. }}
  1379. >
  1380. <Button
  1381. onClick={() => {
  1382. setDetailVisible(false);
  1383. setDetailData(null);
  1384. setFormData({ rule: [], option: {} });
  1385. setOriginalFields([]);
  1386. setOriginalConf('');
  1387. detailForm.resetFields();
  1388. }}
  1389. >
  1390. 取消
  1391. </Button>
  1392. <Button
  1393. type="primary"
  1394. loading={submitLoading}
  1395. disabled={isApproved}
  1396. onClick={async () => {
  1397. try {
  1398. // 验证表单
  1399. const values = await detailForm.validateFields();
  1400. // 获取节点ID
  1401. const nodeId = detailData.nodeId || detailData.id;
  1402. if (!nodeId) {
  1403. message.error('节点ID不存在');
  1404. return;
  1405. }
  1406. setSubmitLoading(true);
  1407. // 将填写值添加到原始 fields 数组的每个 JSON 字符串中
  1408. const fieldsWithValues = originalFields.map((fieldString: string) => {
  1409. try {
  1410. // 解析字段 JSON 字符串
  1411. const fieldObj = JSON.parse(fieldString);
  1412. // 获取字段名:优先使用 name(表单实际使用的字段名),其次使用 field
  1413. const fieldName = fieldObj.name || fieldObj.field;
  1414. // 获取填写值
  1415. const fieldValue = values[fieldName];
  1416. // 添加或更新 value 字段
  1417. fieldObj.value = fieldValue !== undefined && fieldValue !== null ? fieldValue : '';
  1418. // 转回 JSON 字符串
  1419. return JSON.stringify(fieldObj);
  1420. } catch (e) {
  1421. console.error('MyTask: 解析字段 JSON 失败', e, fieldString);
  1422. return fieldString; // 如果解析失败,返回原始字符串
  1423. }
  1424. });
  1425. // 构建完整的表单数据对象
  1426. const submitData = {
  1427. id: detailData.formId || detailData.id,
  1428. name: detailData.nodeName || '未知',
  1429. conf: originalConf,
  1430. fields: fieldsWithValues
  1431. };
  1432. // 将表单数据转换为 JSON 字符串
  1433. const formDataString = JSON.stringify(submitData);
  1434. // 调用接口
  1435. const params: UpdateNodeApprovalParam = {
  1436. nodeId: nodeId,
  1437. approvalStatus: 'approved',
  1438. approvalOpinion: undefined, // 非审核节点,不需要审批意见
  1439. formData: formDataString
  1440. };
  1441. console.log('MyTask: 调用提交接口', params);
  1442. await myTaskApi.updateNodeApproval(params);
  1443. message.success('提交成功');
  1444. // 关闭弹框
  1445. setDetailVisible(false);
  1446. setDetailData(null);
  1447. setFormData({ rule: [], option: {} });
  1448. setFormLoading(false);
  1449. setOriginalFields([]);
  1450. setOriginalConf('');
  1451. detailForm.resetFields();
  1452. // 刷新列表
  1453. getList();
  1454. } catch (error: any) {
  1455. console.error('MyTask: 提交失败', error);
  1456. if (error?.errorFields) {
  1457. // 表单验证失败
  1458. message.error('请填写完整的表单内容');
  1459. } else {
  1460. message.error(error?.message || '提交失败');
  1461. }
  1462. } finally {
  1463. setSubmitLoading(false);
  1464. }
  1465. }}
  1466. >
  1467. 提交
  1468. </Button>
  1469. </div>
  1470. </div>
  1471. );
  1472. })()}
  1473. </div>
  1474. </div>
  1475. );
  1476. }
  1477. return <div className="py-8 text-center text-gray-500">暂无数据</div>;
  1478. })()}
  1479. </Modal>
  1480. </div>
  1481. );
  1482. }