| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375 |
- import React, { useState, useEffect } from 'react';
- import { useNavigate } from 'react-router-dom';
- import { Eye, Search, RotateCcw } from 'lucide-react';
- import { Button, Space, Table as AntdTable, Input, message, Modal, Form as AntdForm, Card, Alert, Select, DatePicker, InputNumber, Switch, Radio, Checkbox, Cascader, Upload, Image, Avatar } from 'antd';
- import { UploadOutlined, LockOutlined, KeyOutlined, WarningOutlined, CheckCircleOutlined, BarcodeOutlined, SendOutlined, EnvironmentOutlined, CloseCircleOutlined } from '@ant-design/icons';
- import type { ColumnsType } from 'antd/es/table';
- import { toast } from 'sonner';
- import dayjs, { Dayjs } from 'dayjs';
- import { taskManagementApi, MyTaskVO, MyTaskPageParam, PageResponse, MyTaskNodeDetailVO, UpdateNodeApprovalParam } from '../api/mytask';
- import { fileApi } from '../api/file';
- import { dateFormatter } from '../utils/formatTime';
- import { DICT_TYPE, getDictLabel } from '../utils/dict';
- import { setConfAndFields2, FormCreateData } from '../utils/formCreate';
- import urgecy1Icon from '../assets/urgecy1.png';
- import urgecy2Icon from '../assets/urgecy2.png';
- import urgecy3Icon from '../assets/urgecy3.png';
- import { useTranslation } from 'react-i18next';
- import FormUploadField from './FormUploadField';
- // 辅助函数:安全地将值转换为 dayjs 对象
- const safeToDayjs = (value: any): dayjs.Dayjs | null => {
- if (!value && value !== 0) {
- return null;
- }
-
- // 如果是数字(时间戳)
- if (typeof value === 'number') {
- // dayjs 默认将数字当作毫秒级时间戳处理
- // 但如果数字小于 10000000000(10位),可能是秒级时间戳,需要转换为毫秒
- let timestamp = value;
- const originalTimestamp = timestamp;
- if (timestamp > 0 && timestamp < 10000000000) {
- // 秒级时间戳,转换为毫秒
- timestamp = timestamp * 1000;
- console.log(`时间戳转换: 秒级 -> 毫秒级`, { original: originalTimestamp, converted: timestamp });
- }
- const dayjsValue = dayjs(timestamp);
- if (dayjsValue.isValid()) {
- console.log(`时间戳解析成功:`, {
- original: value,
- timestamp,
- date: dayjsValue.format('YYYY-MM-DD HH:mm:ss'),
- year: dayjsValue.year()
- });
- return dayjsValue;
- } else {
- console.warn(`时间戳解析失败:`, { original: value, timestamp });
- return null;
- }
- }
-
- // 如果是字符串
- if (typeof value === 'string' && value.trim() !== '') {
- // 尝试判断是否为时间戳字符串
- const numValue = Number(value);
- if (!isNaN(numValue) && numValue > 0 && numValue.toString() === value.trim()) {
- // 是纯数字字符串,可能是时间戳
- // 判断是秒级还是毫秒级时间戳
- let timestamp = numValue;
- const originalTimestamp = timestamp;
- if (timestamp < 10000000000) {
- // 秒级时间戳(10位数字),转换为毫秒
- timestamp = timestamp * 1000;
- console.log(`时间戳转换: 秒级 -> 毫秒级`, { original: originalTimestamp, converted: timestamp });
- } else {
- console.log(`时间戳转换: 毫秒级时间戳`, { timestamp });
- }
- // 否则是毫秒级时间戳(13位或更多),直接使用
- const dayjsValue = dayjs(timestamp);
- if (dayjsValue.isValid()) {
- console.log(`时间戳解析成功:`, {
- original: value,
- timestamp,
- date: dayjsValue.format('YYYY-MM-DD HH:mm:ss'),
- year: dayjsValue.year()
- });
- return dayjsValue;
- } else {
- console.warn(`时间戳解析失败:`, { original: value, timestamp });
- return null;
- }
- } else {
- // 普通日期字符串
- const dayjsValue = dayjs(value);
- return dayjsValue.isValid() ? dayjsValue : null;
- }
- }
-
- // 如果已经是 dayjs 对象,检查是否有 isValid 方法
- if (value && typeof value === 'object' && typeof value.isValid === 'function') {
- try {
- return value.isValid() ? value : null;
- } catch (e) {
- // isValid 调用失败,尝试重新创建
- }
- }
-
- // 如果是对象,检查是否有 isValid 方法(可能是序列化后的 dayjs 对象)
- if (value && typeof value === 'object') {
- // 尝试重新创建 dayjs 对象
- try {
- // 如果有 $d 属性,可能是 dayjs 对象
- if (value.$d) {
- const dayjsValue = dayjs(value.$d);
- return dayjsValue.isValid() ? dayjsValue : null;
- }
- // 如果有时间戳相关的属性
- if (value.valueOf && typeof value.valueOf === 'function') {
- const timestamp = value.valueOf();
- if (typeof timestamp === 'number') {
- const dayjsValue = dayjs(timestamp);
- return dayjsValue.isValid() ? dayjsValue : null;
- }
- }
- } catch (e) {
- // 转换失败,返回 null
- return null;
- }
- }
-
- return null;
- };
- // 辅助函数:验证 dayjs 对象是否有效
- const isValidDayjs = (value: any): boolean => {
- if (!value) return false;
- if (typeof value !== 'object') return false;
- if (typeof value.isValid !== 'function') return false;
- try {
- return value.isValid();
- } catch (e) {
- return false;
- }
- };
- // 辅助函数:将表单值中的日期字符串转换为 dayjs 对象
- const convertDateValues = (formValues: any, fields: any[]): any => {
- const fieldTypeMap: { [key: string]: string } = {};
-
- // 建立字段名到类型的映射
- fields.forEach((field: any) => {
- try {
- const fieldObj = typeof field === 'string' ? JSON.parse(field) : field;
- const fieldName = fieldObj.name || fieldObj.field;
- if (fieldName) {
- fieldTypeMap[fieldName] = fieldObj.type;
- }
- } catch (e) {
- // 解析失败,跳过
- console.warn('解析字段配置失败:', e);
- }
- });
-
- // 转换日期值
- const convertedValues: any = {};
- Object.keys(formValues).forEach((fieldName) => {
- const fieldType = fieldTypeMap[fieldName];
- const value = formValues[fieldName];
-
- if (fieldType === 'date' || fieldType === 'datetime' || fieldType === 'timepicker') {
- // 日期、日期时间或时间选择器类型
- convertedValues[fieldName] = safeToDayjs(value) || undefined;
- } else if (fieldType === 'daterange') {
- // 日期范围类型
- if (Array.isArray(value) && value.length === 2) {
- const [start, end] = value;
- const startDayjs = safeToDayjs(start);
- const endDayjs = safeToDayjs(end);
-
- // 调试日志
- if (startDayjs && endDayjs) {
- console.log(`日期范围字段 ${fieldName} 转换成功:`, {
- original: { start, end },
- converted: {
- start: startDayjs.format('YYYY-MM-DD HH:mm:ss'),
- end: endDayjs.format('YYYY-MM-DD HH:mm:ss')
- }
- });
- convertedValues[fieldName] = [startDayjs, endDayjs];
- } else {
- console.warn(`日期范围字段 ${fieldName} 转换失败:`, { start, end, startDayjs, endDayjs });
- convertedValues[fieldName] = undefined;
- }
- } else {
- convertedValues[fieldName] = undefined;
- }
- } else {
- // 其他类型直接使用原值
- convertedValues[fieldName] = value;
- }
- });
-
- return convertedValues;
- };
- export default function TaskManagement() {
- const { t } = useTranslation();
- const navigate = useNavigate();
-
- const [loading, setLoading] = useState(true);
- const [list, setList] = useState<MyTaskVO[]>([]);
- const [total, setTotal] = useState(0);
- // 从 sessionStorage 读取 status 参数
- const getInitialStatus = () => {
- const status = sessionStorage.getItem('taskManagementStatus');
- return status || undefined;
- };
- const [queryParams, setQueryParams] = useState<MyTaskPageParam>({
- pageNo: 1,
- pageSize: 10,
- key: undefined,
- status: getInitialStatus(),
- });
- const [searchOrderNo, setSearchOrderNo] = useState('');
- const [searchName, setSearchName] = useState('');
- const [approvalStatusDictList, setApprovalStatusDictList] = useState<any[]>([]);
- const [urgencyLevelDictList, setUrgencyLevelDictList] = useState<any[]>([]);
- const [jobCategoryDictList, setJobCategoryDictList] = useState<any[]>([]); // 作业分类 work_type
- // 节点详情弹框相关状态
- const [detailVisible, setDetailVisible] = useState(false);
- const [detailLoading, setDetailLoading] = useState(false);
- const [detailData, setDetailData] = useState<MyTaskNodeDetailVO | null>(null);
- const [formData, setFormData] = useState<FormCreateData>({
- rule: [],
- option: {}
- });
- const [formLoading, setFormLoading] = useState(false); // 表单加载状态
- const [originalFields, setOriginalFields] = useState<string[]>([]); // 保存原始的 fields 数组(JSON 字符串数组)
- const [originalConf, setOriginalConf] = useState<string>(''); // 保存原始的 conf(JSON 字符串)
- const [detailForm] = AntdForm.useForm();
- const [approvalComment, setApprovalComment] = useState(''); // 审核意见
- const [approvalLoading, setApprovalLoading] = useState(false); // 审核操作loading状态
- const [submitLoading, setSubmitLoading] = useState(false); // 提交操作loading状态
- // 隔离方式字典(isolation_method:0=盲板,1=上锁挂牌,2=拆除)
- const [isolationMethodDictList, setIsolationMethodDictList] = useState<any[]>([]);
- // 盲板/拆除类型时的设备编号与附件(仅隔离/方案、解除隔离且隔离方式为盲板或拆除时使用)
- const [isolationDeviceNumber, setIsolationDeviceNumber] = useState('');
- const [isolationFileList, setIsolationFileList] = useState<any[]>([]);
- const [isolationSubmitLoading, setIsolationSubmitLoading] = useState(false);
-
- // 组件挂载时打印调试信息,并从 sessionStorage 读取 status
- useEffect(() => {
- console.log('TaskManagement 组件已加载');
- const status = sessionStorage.getItem('taskManagementStatus');
- if (status) {
- console.log('从 sessionStorage 读取到 status:', status);
- setQueryParams(prev => ({
- ...prev,
- status: status,
- pageNo: 1, // 重置到第一页
- }));
- // 读取后清除 sessionStorage,避免下次进入时自动应用
- sessionStorage.removeItem('taskManagementStatus');
- }
- }, []);
-
- // 监听弹框状态变化
- useEffect(() => {
- console.log('TaskManagement: detailVisible 状态变化', detailVisible, 'detailData:', detailData);
- }, [detailVisible, detailData]);
-
- // 获取审批状态字典
- const getApprovalStatusDictList = async () => {
- try {
- const { dictDataApi } = await import('../api/DictData');
- const response = await dictDataApi.getDictDataPage({
- pageNo: 1,
- pageSize: -1,
- dictType: 'approval_status',
- });
- const data = (response as any)?.data || response;
- const dictList = data?.list || [];
- setApprovalStatusDictList(dictList);
- console.log('TaskManagement: 获取审批状态字典成功', dictList);
- } catch (error: any) {
- console.error('获取审批状态字典失败:', error);
- }
- };
- // 获取紧急程度字典
- const getUrgencyLevelDictList = async () => {
- try {
- const { dictDataApi } = await import('../api/DictData');
- const response = await dictDataApi.getDictDataPage({
- pageNo: 1,
- pageSize: -1,
- dictType: 'urgency_level',
- });
- const data = (response as any)?.data || response;
- const dictList = data?.list || [];
- setUrgencyLevelDictList(dictList);
- console.log('TaskManagement: 获取紧急程度字典成功', dictList);
- } catch (error: any) {
- console.error('获取紧急程度字典失败:', error);
- }
- };
- // 获取隔离方式字典(isolation_method)
- const getIsolationMethodDictList = async () => {
- try {
- const { dictDataApi } = await import('../api/DictData');
- const response = await dictDataApi.getDictDataPage({
- pageNo: 1,
- pageSize: -1,
- dictType: 'isolation_method',
- });
- const data = (response as any)?.data || response;
- const dictList = data?.list || [];
- setIsolationMethodDictList(dictList);
- } catch (error: any) {
- console.error('获取隔离方式字典失败:', error);
- }
- };
- // 获取作业分类字典(work_type)
- const getJobCategoryDictList = async () => {
- try {
- const { dictDataApi } = await import('../api/DictData');
- const response = await dictDataApi.getDictDataPage({
- pageNo: 1,
- pageSize: -1,
- dictType: 'work_type',
- });
- const data = (response as any)?.data || response;
- setJobCategoryDictList(data?.list || []);
- } catch (error: any) {
- console.error('获取作业分类字典失败:', error);
- setJobCategoryDictList([]);
- }
- };
- useEffect(() => {
- getApprovalStatusDictList();
- getUrgencyLevelDictList();
- getIsolationMethodDictList();
- getJobCategoryDictList();
- }, []);
-
- /** 查询列表 */
- const getList = async (params?: MyTaskPageParam) => {
- const searchParams = params || queryParams;
- setLoading(true);
- try {
- console.log('TaskManagement: 开始获取任务管理列表', searchParams);
- const response = await taskManagementApi.getAdminWorkPage(searchParams);
- console.log('TaskManagement: 接口响应', response);
- const data = (response as any)?.data || response;
- const pageData = (data as PageResponse<MyTaskVO>);
- console.log('TaskManagement: 解析后的数据', pageData);
- setList(pageData.list || []);
- setTotal(pageData.total || 0);
- } catch (error: any) {
- console.error('TaskManagement: 获取任务管理列表失败', error);
- toast.error(error.message || t('form.fetchTaskListFailed'));
- setList([]);
- setTotal(0);
- } finally {
- setLoading(false);
- }
- };
- useEffect(() => {
- console.log('TaskManagement: useEffect 触发,queryParams:', queryParams);
- getList();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [queryParams.pageNo, queryParams.pageSize, queryParams.key, queryParams.status]);
- // 搜索:两个条件都通过 key 传参,值为具体输入(多个用空格拼接)
- const handleSearch = () => {
- const parts = [searchOrderNo.trim(), searchName.trim()].filter(Boolean);
- setQueryParams({
- ...queryParams,
- pageNo: 1,
- key: parts.length ? parts.join(' ') : undefined,
- });
- };
- // 重置
- const handleReset = () => {
- setSearchOrderNo('');
- setSearchName('');
- setQueryParams({
- ...queryParams,
- pageNo: 1,
- key: undefined,
- status: undefined, // 重置时也清除 status
- });
- };
- // 默认表单配置
- const defaultFormConfig = {
- name: '',
- labelPosition: 'right',
- formSize: 'middle',
- labelSuffix: '',
- labelWidth: 100,
- hideRequiredMark: false,
- showValidationError: true,
- inlineValidation: false,
- showSubmitButton: false,
- showResetButton: false,
- };
- // 渲染字段预览(支持嵌套结构)
- const renderFieldPreview = (field: any, parentSpanStyle?: React.CSSProperties): React.ReactNode => {
- const formConfig = formData.option?.formConfig || defaultFormConfig;
- const layoutColumns = formConfig.layoutColumns || 1;
- const spanStyle = parentSpanStyle || (layoutColumns > 1 ? { gridColumn: `span ${Math.min(layoutColumns, field.span || 1)}` } : undefined);
- // 处理容器类型(card 和 grid)
- if (field.type === 'card') {
- const children = field.children || [];
- // 优先使用 label(字段名称),如果没有则使用 cardTitle,最后使用默认值
- const cardTitle = field.label || field.cardTitle || '卡片容器';
- return (
- <div key={field.id} style={spanStyle} className="mb-4">
- <Card title={cardTitle} className="w-full">
- <div className="space-y-4">
- {children.map((child: any) => renderFieldPreview(child))}
- </div>
- </Card>
- </div>
- );
- }
- if (field.type === 'grid') {
- const gridColumns = field.gridColumns || 2;
- const children = field.children || [];
- return (
- <div key={field.id} style={spanStyle} className="mb-4">
- <div
- style={{
- display: 'grid',
- gridTemplateColumns: `repeat(${gridColumns}, minmax(0, 1fr))`,
- gap: '16px',
- }}
- >
- {children.map((child: any) => {
- const childSpanStyle = gridColumns > 1
- ? { gridColumn: `span ${Math.min(gridColumns, child.span || 1)}` }
- : undefined;
- return (
- <div key={child.id} style={childSpanStyle}>
- {renderFieldPreview(child)}
- </div>
- );
- })}
- </div>
- </div>
- );
- }
- // 处理 alert 类型
- if (field.type === 'alert') {
- const themeClass =
- field.alertTheme === 'dark'
- ? 'bg-gray-800 text-white border-gray-700'
- : '';
- return (
- <div key={field.id} className="mb-4" style={spanStyle}>
- <Alert
- message={field.alertTitle || field.label}
- description={field.alertDescription}
- type={field.alertType || 'info'}
- showIcon={field.alertShowIcon !== false}
- closable={field.alertClosable}
- closeText={field.alertCloseText}
- className={`${field.alertCentered ? 'text-center' : ''} ${themeClass}`}
- banner
- style={field.style}
- />
- {field.hint && <div className="text-xs text-gray-400 mt-1">{field.hint}</div>}
- </div>
- );
- }
- // 与表单设计器一致:每字段 labelCol/wrapperCol
- const getItemLayout = (f: any) => {
- const isTop = formConfig.labelPosition === 'top';
- const w = f.labelWidth ?? formConfig.labelWidth ?? 100;
- const effective = (typeof w === 'number' && w > 0) ? w : 100;
- return {
- labelCol: isTop ? undefined : { flex: `${effective}px`, style: { minWidth: `${effective}px`, textAlign: formConfig.labelPosition === 'right' ? 'right' : 'left' } },
- wrapperCol: isTop ? undefined : { flex: 'auto', style: { minWidth: 0 } },
- };
- };
- const itemLayout = getItemLayout(field);
- // 处理普通字段
- switch (field.type) {
- case 'textarea':
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- >
- <Input.TextArea
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
- rows={4}
- allowClear={field.showClear}
- maxLength={field.maxLength}
- readOnly={field.readOnly}
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- case 'password':
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- >
- <Input.Password
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
- allowClear={field.showClear}
- maxLength={field.maxLength}
- readOnly={field.readOnly}
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- case 'number':
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- >
- <InputNumber
- style={{ width: '100%' }}
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
- max={field.maxLength}
- readOnly={field.readOnly}
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- case 'select':
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- >
- <Select
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择'}
- allowClear={field.showClear}
- disabled={field.disabled}
- size={field.size || 'middle'}
- >
- {(field.options || []).map((opt: any, idx: number) => (
- <Select.Option key={idx} value={opt.value}>{opt.label}</Select.Option>
- ))}
- </Select>
- </AntdForm.Item>
- </div>
- );
- case 'date':
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- getValueFromEvent={(value) => {
- // 选择日期时,将 dayjs 对象转换为毫秒级时间戳
- if (!value) return undefined;
- if (value && typeof value === 'object' && 'valueOf' in value && typeof value.valueOf === 'function' && 'isValid' in value && typeof value.isValid === 'function') {
- // 确保是有效的 dayjs 对象
- if (value.isValid()) {
- return value.valueOf(); // dayjs 的 valueOf() 返回毫秒级时间戳
- }
- return undefined;
- }
- return value;
- }}
- normalize={(value) => {
- // 回显时,将毫秒级时间戳转换为 dayjs 对象
- if (!value) return undefined;
- if (typeof value === 'number') {
- const dayjsValue = dayjs(value);
- return dayjsValue.isValid() ? dayjsValue : undefined;
- }
- if (typeof value === 'string') {
- const numValue = Number(value);
- if (!isNaN(numValue) && numValue > 0) {
- const dayjsValue = dayjs(numValue);
- return dayjsValue.isValid() ? dayjsValue : undefined;
- }
- const dayjsValue = dayjs(value);
- return dayjsValue.isValid() ? dayjsValue : undefined;
- }
- if (value && typeof value === 'object' && 'isValid' in value && typeof value.isValid === 'function') {
- return value.isValid() ? value : undefined;
- }
- return undefined;
- }}
- >
- <DatePicker
- style={{ width: '100%' }}
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期'}
- allowClear={field.showClear}
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- case 'daterange':
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期范围' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- getValueFromEvent={(value) => {
- // 选择日期范围时,将 dayjs 对象数组转换为毫秒级时间戳数组
- if (!value || !Array.isArray(value) || value.length !== 2) return undefined;
- const [start, end] = value;
- if (start && typeof start === 'object' && 'valueOf' in start && typeof start.valueOf === 'function' && 'isValid' in start && typeof start.isValid === 'function' &&
- end && typeof end === 'object' && 'valueOf' in end && typeof end.valueOf === 'function' && 'isValid' in end && typeof end.isValid === 'function') {
- // 确保都是有效的 dayjs 对象
- if (start.isValid() && end.isValid()) {
- return [start.valueOf(), end.valueOf()]; // 返回毫秒级时间戳数组
- }
- return undefined;
- }
- return value;
- }}
- normalize={(value) => {
- // 回显时,将毫秒级时间戳数组转换为 dayjs 对象数组
- if (!value) return undefined;
- if (Array.isArray(value) && value.length === 2) {
- const [start, end] = value;
- let startDayjs: any = null;
- let endDayjs: any = null;
-
- if (typeof start === 'number') {
- startDayjs = dayjs(start);
- startDayjs = startDayjs.isValid() ? startDayjs : null;
- } else if (typeof start === 'string') {
- const numValue = Number(start);
- if (!isNaN(numValue) && numValue > 0) {
- startDayjs = dayjs(numValue);
- startDayjs = startDayjs.isValid() ? startDayjs : null;
- } else {
- startDayjs = dayjs(start);
- startDayjs = startDayjs.isValid() ? startDayjs : null;
- }
- } else if (start && typeof start === 'object' && 'isValid' in start && typeof start.isValid === 'function') {
- startDayjs = start.isValid() ? start : null;
- }
-
- if (typeof end === 'number') {
- endDayjs = dayjs(end);
- endDayjs = endDayjs.isValid() ? endDayjs : null;
- } else if (typeof end === 'string') {
- const numValue = Number(end);
- if (!isNaN(numValue) && numValue > 0) {
- endDayjs = dayjs(numValue);
- endDayjs = endDayjs.isValid() ? endDayjs : null;
- } else {
- endDayjs = dayjs(end);
- endDayjs = endDayjs.isValid() ? endDayjs : null;
- }
- } else if (end && typeof end === 'object' && 'isValid' in end && typeof end.isValid === 'function') {
- endDayjs = end.isValid() ? end : null;
- }
-
- if (startDayjs && endDayjs) {
- return [startDayjs, endDayjs];
- }
- }
- return undefined;
- }}
- >
- <DatePicker.RangePicker
- style={{ width: '100%' }}
- placeholder={Array.isArray(field.placeholder) ? field.placeholder as [string, string] : ['开始日期', '结束日期']}
- allowClear={field.showClear}
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- case 'datetime':
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期时间' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- getValueFromEvent={(value) => {
- // 选择日期时间时,将 dayjs 对象转换为毫秒级时间戳
- if (!value) return undefined;
- if (value && typeof value === 'object' && 'valueOf' in value) {
- return value.valueOf(); // dayjs 的 valueOf() 返回毫秒级时间戳
- }
- return value;
- }}
- normalize={(value) => {
- // 回显时,将毫秒级时间戳转换为 dayjs 对象
- if (!value) return undefined;
- if (typeof value === 'number') {
- const dayjsValue = dayjs(value);
- return dayjsValue.isValid() ? dayjsValue : undefined;
- }
- if (typeof value === 'string') {
- const numValue = Number(value);
- if (!isNaN(numValue) && numValue > 0) {
- const dayjsValue = dayjs(numValue);
- return dayjsValue.isValid() ? dayjsValue : undefined;
- }
- const dayjsValue = dayjs(value);
- return dayjsValue.isValid() ? dayjsValue : undefined;
- }
- if (value && typeof value === 'object' && 'isValid' in value && typeof value.isValid === 'function') {
- return value.isValid() ? value : undefined;
- }
- return undefined;
- }}
- >
- <DatePicker
- style={{ width: '100%' }}
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期时间'}
- allowClear={field.showClear}
- showTime={{ format: 'HH:mm:ss' }}
- format="YYYY-MM-DD HH:mm:ss"
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- case 'switch':
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
- help={field.hint}
- valuePropName="checked"
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- >
- <Switch disabled={field.disabled} />
- </AntdForm.Item>
- </div>
- );
- case 'radio':
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- >
- <Radio.Group disabled={field.disabled}>
- {(field.options || []).map((opt: any, idx: number) => (
- <Radio key={idx} value={opt.value}>{opt.label}</Radio>
- ))}
- </Radio.Group>
- </AntdForm.Item>
- </div>
- );
- case 'checkbox':
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- >
- <Checkbox.Group disabled={field.disabled}>
- {(field.options || []).map((opt: any, idx: number) => (
- <Checkbox key={idx} value={opt.value}>{opt.label}</Checkbox>
- ))}
- </Checkbox.Group>
- </AntdForm.Item>
- </div>
- );
- case 'cascader':
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- >
- <Cascader
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择'}
- options={field.cascaderOptions || []}
- className="w-full"
- allowClear={field.showClear}
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- case 'upload':
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请上传' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- >
- <FormUploadField
- uploadType={field.uploadType}
- maxCount={field.maxCount}
- accept={field.accept}
- disabled={field.disabled}
- />
- </AntdForm.Item>
- </div>
- );
- case 'time':
- case 'timepicker':
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择时间' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- >
- <DatePicker
- style={{ width: '100%' }}
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择时间'}
- allowClear={field.showClear}
- picker="time"
- format="HH:mm:ss"
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- default:
- // 检查 inputType 是否为时间类型
- const inputType = field.inputType || field.type;
- if (inputType === 'date' || inputType === 'datetime' || inputType === 'time' || inputType === 'timepicker') {
- // 如果是时间类型,使用 DatePicker
- if (inputType === 'datetime') {
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期时间' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- >
- <DatePicker
- style={{ width: '100%' }}
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期时间'}
- allowClear={field.showClear}
- showTime={{ format: 'HH:mm:ss' }}
- format="YYYY-MM-DD HH:mm:ss"
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- } else if (inputType === 'time' || inputType === 'timepicker') {
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择时间' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- >
- <DatePicker
- style={{ width: '100%' }}
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择时间'}
- allowClear={field.showClear}
- picker="time"
- format="HH:mm:ss"
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- } else {
- // date 类型
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请选择日期' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- >
- <DatePicker
- style={{ width: '100%' }}
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请选择日期'}
- allowClear={field.showClear}
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- }
- }
-
- // 其他类型使用 Input
- return (
- <div key={field.id} style={{ ...spanStyle, backgroundColor: '#f5f5f5', padding: '12px', borderRadius: '12px', marginBottom: '12px', border: '1px solid #e0e0e0' }}>
- <AntdForm.Item
- label={(field.label || field.title || '') + (formConfig.labelSuffix || '')}
- name={field.name || field.field}
- required={field.required && !formConfig.hideRequiredMark}
- rules={field.required ? [{ required: true, message: field.requiredMessage || '请输入' }] : []}
- help={field.hint}
- labelCol={itemLayout.labelCol}
- wrapperCol={itemLayout.wrapperCol}
- >
- <Input
- type={field.inputType || 'text'}
- placeholder={typeof field.placeholder === 'string' ? field.placeholder : '请输入'}
- allowClear={field.showClear}
- maxLength={field.maxLength}
- readOnly={field.readOnly}
- disabled={field.disabled}
- size={field.size || 'middle'}
- />
- </AntdForm.Item>
- </div>
- );
- }
- };
- // 查看节点详情(节点详情弹框)
- const handleViewDetail = async (record: MyTaskVO) => {
- console.log('TaskManagement: handleViewDetail 被调用', record);
-
- if (!record.nodeId) {
- console.warn('TaskManagement: 节点ID不存在', record);
- message.warning('节点ID不存在');
- return;
- }
-
- setDetailLoading(true);
- setDetailVisible(true); // 先打开弹框显示加载状态
- console.log('TaskManagement: 弹框状态已设置为 true (加载中)');
-
- try {
- console.log('TaskManagement: 开始获取节点详情', { nodeId: record.nodeId });
- const response = await taskManagementApi.getAdminWorkNodeDetail(record.nodeId);
- console.log('TaskManagement: 节点详情响应', response);
-
- let data: any = response;
-
- // 直接从第一层 data.formId 获取 formId
- let extractedFormId: number | undefined = undefined;
- if (data?.formId !== undefined && data?.formId !== null) {
- const formIdValue = data.formId;
- if (typeof formIdValue === 'string') {
- const parsed = parseInt(formIdValue, 10);
- if (!isNaN(parsed)) {
- extractedFormId = parsed;
- console.log('TaskManagement: ✅ 从 data.formId (字符串) 提取到 formId', extractedFormId);
- }
- } else if (typeof formIdValue === 'number') {
- extractedFormId = formIdValue;
- console.log('TaskManagement: ✅ 从 data.formId (数字) 提取到 formId', extractedFormId);
- }
- }
-
- console.log('TaskManagement: formId 提取结果', {
- extractedFormId,
- '原始 data.formId': data?.formId,
- 'data.formId 类型': typeof data?.formId
- });
-
- if (extractedFormId === undefined) {
- console.warn('TaskManagement: ⚠️ 未能提取到 formId', {
- 'data.formId': data?.formId,
- 'data': data
- });
- }
-
- // 合并列表数据中的作业信息(作业名称、编号、负责人、时间等)
- // 直接使用第一层 data,不解析 data.data
- const detailDataWithWorkInfo: MyTaskNodeDetailVO = {
- ...(data || {}),
- workName: record.name || data?.workName || data?.name,
- orderNo: record.orderNo || data?.orderNo,
- workerUserName: record.workerUserName || data?.workerUserName,
- initiatorName: data?.initiatorName || data?.initiator || record.initiatorName || record.initiator || '',
- workTime: record.workTime || data?.workTime,
- // 确保 type 字段存在,使用第一层 data.type
- type: data?.type || data?.nodeType || '',
- nodeType: data?.nodeType || data?.type || '', // 同时设置 nodeType 作为兼容
- // 使用提取到的 formId
- formId: extractedFormId,
- };
-
- console.log('TaskManagement: 合并后的详情数据 - type 和 formId 字段', {
- 'data.type': data?.type,
- 'data.nodeType': data?.nodeType,
- 'data.formId': data?.formId,
- 'detailDataWithWorkInfo.type': detailDataWithWorkInfo.type,
- 'detailDataWithWorkInfo.nodeType': detailDataWithWorkInfo.nodeType,
- 'detailDataWithWorkInfo.formId': detailDataWithWorkInfo.formId,
- });
-
- console.log('TaskManagement: 合并后的详情数据', detailDataWithWorkInfo);
- console.log('TaskManagement: 节点类型', detailDataWithWorkInfo.type);
-
- // 先设置数据,确保弹框有内容显示
- setDetailData(detailDataWithWorkInfo);
- console.log('TaskManagement: detailData 已设置', detailDataWithWorkInfo);
-
- // 确保弹框是打开状态
- setDetailVisible(true);
- setDetailLoading(false);
- console.log('TaskManagement: 弹框状态设置为 true,loading 设置为 false');
-
- // 根据节点类型决定是否需要获取表单
- // isolation、releaseIsolation 和 returnLock 不需要表单,其他类型(review 和其他)需要根据 formId 获取表单
- const nodeType = detailDataWithWorkInfo.type || detailDataWithWorkInfo.nodeType || '';
- const isIsolation = nodeType === 'isolation' || nodeType === '隔离' || nodeType === '隔离/方案';
- const isReleaseIsolation = nodeType === 'releaseIsolation' || nodeType === '解除隔离';
- const isReturnLock = nodeType === 'returnLock';
- const isolationType = String(detailDataWithWorkInfo?.isolationType ?? '').trim();
- const isBlindOrDismantle = isolationType === '0' || isolationType === '2'; // 0=盲板 2=拆除
-
- // 检查任务状态是否为"已通过"
- const isApproved = detailDataWithWorkInfo.approvalStatus === 'approved';
-
- // 获取 formId(可能为 0,所以需要明确检查 undefined 和 null)
- const formId = detailDataWithWorkInfo.formId;
- // formId 可能是数字(包括 0)或字符串,只要不是 undefined、null 或空字符串,就认为有 formId
- const hasFormId = formId !== undefined && formId !== null && (
- typeof formId === 'number' ||
- (typeof formId === 'string' && formId !== '' && formId.trim() !== '')
- );
-
- // 检查是否有 formData(已提交的表单数据)
- const hasFormData = detailDataWithWorkInfo.formData && (
- (typeof detailDataWithWorkInfo.formData === 'string' && detailDataWithWorkInfo.formData.trim() !== '') ||
- (typeof detailDataWithWorkInfo.formData === 'object' && Object.keys(detailDataWithWorkInfo.formData).length > 0)
- );
-
- console.log('TaskManagement: 表单获取判断', {
- nodeType,
- isIsolation,
- isReleaseIsolation,
- isReturnLock,
- isApproved,
- hasFormData,
- formId,
- formIdType: typeof formId,
- hasFormId,
- '从formData获取': isApproved && hasFormData,
- '从formId获取': !isIsolation && !isReleaseIsolation && !isReturnLock && hasFormId && !(isApproved && hasFormData),
- 'detailDataWithWorkInfo': detailDataWithWorkInfo,
- });
-
- // 当前节点无表单(无 formId 且不会从 formData 解析出表单)时清空表单 state,避免完成/结束节点误展示上一节点的自定义表单
- if (!hasFormId && !(isApproved && hasFormData)) {
- setFormData({ rule: [], option: {} });
- setOriginalFields([]);
- setOriginalConf('');
- }
-
- // 打开盲板/拆除详情时先清空设备编号和附件,后续若有 formData 再回填
- if (isIsolation || isReleaseIsolation) {
- const isolationType = String(detailDataWithWorkInfo.isolationType ?? '').trim();
- if (isolationType === '0' || isolationType === '2') {
- setIsolationDeviceNumber('');
- setIsolationFileList([]);
- }
- }
-
- // 如果任务状态为"已通过"且有 formData,则从 formData 中解析表单结构
- if (isApproved && hasFormData) {
- console.log('TaskManagement: ✅ 任务已通过,从 formData 中解析表单结构');
- setFormLoading(true);
-
- try {
- // 解析 formData(可能是 JSON 字符串或对象)
- let parsedFormData: any;
- if (typeof detailDataWithWorkInfo.formData === 'string') {
- parsedFormData = JSON.parse(detailDataWithWorkInfo.formData);
- } else {
- parsedFormData = detailDataWithWorkInfo.formData;
- }
-
- console.log('TaskManagement: 解析后的 formData', parsedFormData);
-
- // 从 formData 中提取 conf 和 fields
- const conf = parsedFormData.conf;
- const fields = parsedFormData.fields;
-
- if (conf && fields) {
- // 保存原始的 conf 和 fields(JSON 字符串格式)
- const confString = typeof conf === 'string' ? conf : JSON.stringify(conf);
- const fieldsArray = Array.isArray(fields) ? fields : [];
- const fieldsStringArray = fieldsArray.map((field: any) => {
- return typeof field === 'string' ? field : JSON.stringify(field);
- });
-
- setOriginalConf(confString);
- setOriginalFields(fieldsStringArray);
- console.log('TaskManagement: 从 formData 保存原始表单数据', {
- conf: confString,
- fieldsCount: fieldsStringArray.length,
- fields: fieldsStringArray
- });
-
- // 解析 fields 中的每个字段,提取 value 值用于回显
- const formValues: any = {};
- const fieldTypeMap: { [key: string]: string } = {}; // 存储字段名到类型的映射
-
- // 先解析所有字段,建立字段类型映射
- fieldsStringArray.forEach((fieldString: string) => {
- try {
- const fieldObj = typeof fieldString === 'string' ? JSON.parse(fieldString) : fieldString;
- const fieldName = fieldObj.name || fieldObj.field;
- if (fieldName) {
- fieldTypeMap[fieldName] = fieldObj.type;
- }
- } catch (e) {
- console.error('TaskManagement: 解析字段 JSON 失败', e, fieldString);
- }
- });
-
- // 提取值并转换日期类型
- fieldsStringArray.forEach((fieldString: string) => {
- try {
- const fieldObj = typeof fieldString === 'string' ? JSON.parse(fieldString) : fieldString;
- const fieldName = fieldObj.name || fieldObj.field;
- if (fieldName && fieldObj.value !== undefined && fieldObj.value !== null) {
- const fieldType = fieldTypeMap[fieldName];
- let value = fieldObj.value;
-
- // 如果是日期类型,转换为 dayjs 对象
- if (fieldType === 'date' || fieldType === 'datetime') {
- if (typeof value === 'string' && value.trim() !== '') {
- const dayjsValue = dayjs(value);
- formValues[fieldName] = dayjsValue.isValid() ? dayjsValue : undefined;
- } else {
- formValues[fieldName] = undefined;
- }
- } else if (fieldType === 'daterange') {
- // 日期范围类型,转换为 dayjs 数组
- if (Array.isArray(value) && value.length === 2) {
- const [start, end] = value;
- const startDayjs = typeof start === 'string' && start ? dayjs(start) : null;
- const endDayjs = typeof end === 'string' && end ? dayjs(end) : null;
- if (startDayjs && startDayjs.isValid() && endDayjs && endDayjs.isValid()) {
- formValues[fieldName] = [startDayjs, endDayjs];
- } else {
- formValues[fieldName] = undefined;
- }
- } else {
- formValues[fieldName] = undefined;
- }
- } else {
- // 其他类型直接使用原值
- formValues[fieldName] = value;
- }
- }
- } catch (e) {
- console.error('TaskManagement: 解析字段 JSON 失败', e, fieldString);
- }
- });
-
- console.log('TaskManagement: 从 formData 提取的表单值', formValues);
-
- // 设置表单配置和字段
- setConfAndFields2(setFormData, conf, fields);
- console.log('TaskManagement: 表单配置已设置(从 formData)');
-
- // 转换日期值为 dayjs 对象
- const convertedFormValues = convertDateValues(formValues, fieldsStringArray);
-
- // 回填表单值
- setTimeout(() => {
- detailForm.setFieldsValue(convertedFormValues);
- console.log('TaskManagement: 表单数据已回填(从 formData)', convertedFormValues);
- }, 100);
- // 有 conf/fields 时也回显 deviceNumber、attachments(解除隔离/隔离方案完成查看详情);attachments 可能是 JSON 字符串
- const normAttachments = (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 [];
- };
- const listInConf = normAttachments(parsedFormData.attachments).length ? normAttachments(parsedFormData.attachments) : normAttachments((detailDataWithWorkInfo as any).attachments);
- const deviceNumInConf = parsedFormData.deviceNumber ?? (detailDataWithWorkInfo as any).deviceNumber;
- const hasDeviceInConf = deviceNumInConf !== undefined && deviceNumInConf !== null;
- if (hasDeviceInConf || listInConf.length > 0) {
- setIsolationDeviceNumber(deviceNumInConf ?? '');
- setIsolationFileList(
- listInConf.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,
- }))
- );
- console.log('TaskManagement: 盲板/拆除 deviceNumber/attachments 已回显', { deviceNumber: parsedFormData.deviceNumber, attachmentsCount: listInConf.length });
- }
- } else if (
- parsedFormData.deviceNumber !== undefined ||
- (() => { const a = parsedFormData.attachments; if (a == null) return false; if (Array.isArray(a)) return a.length > 0; if (typeof a === 'string' && a.trim()) { try { const p = JSON.parse(a); return Array.isArray(p) && p.length > 0; } catch { return false; } } return false; })()
- ) {
- // 盲板/拆除 仅 deviceNumber/attachments 无 conf/fields 时的回显
- const listElse = (() => {
- const a = parsedFormData.attachments;
- if (a == null) return [];
- if (Array.isArray(a)) return a;
- if (typeof a === 'string' && a.trim()) { try { const p = JSON.parse(a); return Array.isArray(p) ? p : []; } catch { return []; } }
- return [];
- })();
- setIsolationDeviceNumber(parsedFormData.deviceNumber ?? '');
- setIsolationFileList(
- listElse.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,
- }))
- );
- console.log('TaskManagement: 盲板/拆除 formData 已回显', { deviceNumber: parsedFormData.deviceNumber, attachmentsCount: listElse.length });
- } else {
- // 完成/结束等节点可能无表单内容,不提示“表单数据不完整”,有则显示、无则不显示即可
- console.warn('TaskManagement: formData 中缺少 conf 或 fields', { conf: !!conf, fields: !!fields });
- }
- } catch (e) {
- console.error('TaskManagement: 解析 formData 失败', e);
- message.error('解析表单数据失败: ' + (e instanceof Error ? e.message : String(e)));
- } finally {
- setFormLoading(false);
- }
- }
- // 有 formId 时从接口获取表单:① 审核等非隔离节点 ② 盲板/拆除节点(隔离或解除隔离且隔离方式为盲板或拆除)
- else if (hasFormId && (
- (!isIsolation && !isReleaseIsolation && !isReturnLock) ||
- ((isIsolation || isReleaseIsolation) && isBlindOrDismantle)
- )) {
- console.log('TaskManagement: ✅ 满足条件,开始获取表单', { formId, nodeType, isBlindOrDismantle });
- // 重置表单数据
- setFormData({ rule: [], option: {} });
- setOriginalFields([]);
- setOriginalConf('');
- setFormLoading(true);
-
- // 立即调用接口,不使用 setTimeout,确保接口被调用
- (async () => {
- try {
- // 确保 formId 是数字类型
- const numericFormId = typeof formId === 'string' ? parseInt(formId, 10) : formId;
- if (isNaN(numericFormId)) {
- console.error('TaskManagement: formId 不是有效数字', formId);
- message.error('表单ID无效');
- setFormLoading(false);
- return;
- }
-
- console.log('TaskManagement: 🚀 开始调用表单接口', { formId: numericFormId, nodeType, url: `/bpm/form/get?id=${numericFormId}` });
- const FormApi = await import('../api/bpm/form');
- const formDetailResponse = await FormApi.getForm(numericFormId);
- console.log('TaskManagement: ✅ 表单接口调用成功,原始响应', formDetailResponse);
-
- // 处理响应数据(可能包含 code 和 data 包装)
- let formDetail: any = formDetailResponse;
- if (formDetailResponse && typeof formDetailResponse === 'object' && 'data' in formDetailResponse) {
- // 如果响应有 data 字段,使用 data
- formDetail = (formDetailResponse as any).data || formDetailResponse;
- }
-
- console.log('TaskManagement: 处理后的表单详情', formDetail);
-
- // 获取 conf 和 fields
- const conf = formDetail?.conf || formDetail?.formConfig;
- const fields = formDetail?.fields || formDetail?.formFields;
-
- console.log('TaskManagement: 表单配置和字段', {
- hasConf: !!conf,
- confType: typeof conf,
- hasFields: !!fields,
- fieldsType: Array.isArray(fields) ? 'array' : typeof fields,
- fieldsLength: Array.isArray(fields) ? fields.length : 0
- });
-
- // 解析表单配置和字段
- if (conf && fields) {
- // 保存原始的 conf 和 fields(JSON 字符串格式)
- const confString = typeof conf === 'string' ? conf : JSON.stringify(conf);
- const fieldsArray = Array.isArray(fields) ? fields : [];
- const fieldsStringArray = fieldsArray.map((field: any) => {
- return typeof field === 'string' ? field : JSON.stringify(field);
- });
-
- setOriginalConf(confString);
- setOriginalFields(fieldsStringArray);
- console.log('TaskManagement: 保存原始表单数据', {
- conf: confString,
- fieldsCount: fieldsStringArray.length,
- fields: fieldsStringArray
- });
-
- // setConfAndFields2 会自动处理 JSON 字符串解析
- setConfAndFields2(setFormData, conf, fields);
- console.log('TaskManagement: 表单配置已设置', {
- conf: typeof conf === 'string' ? 'JSON字符串' : '对象',
- fieldsCount: Array.isArray(fields) ? fields.length : 0
- });
-
- // 仅任务已完成(approved)时回显表单数据;进行中一律不回显并清空表单
- const shouldEchoFormData = detailDataWithWorkInfo.formData && isApproved;
- if (shouldEchoFormData) {
- try {
- let formValues = typeof detailDataWithWorkInfo.formData === 'string'
- ? JSON.parse(detailDataWithWorkInfo.formData)
- : detailDataWithWorkInfo.formData;
-
- // 转换日期值为 dayjs 对象
- const fieldsArray = Array.isArray(fields) ? fields : [];
- formValues = convertDateValues(formValues, fieldsArray);
-
- // 清理无效的日期值,确保所有日期字段都是有效的 dayjs 对象或 undefined
- const cleanedFormValues: any = {};
- Object.keys(formValues).forEach(key => {
- const value = formValues[key];
- const field = fieldsArray.find((f: any) => {
- const fieldObj = typeof f === 'string' ? JSON.parse(f) : f;
- return (fieldObj.name || fieldObj.field) === key;
- });
-
- if (field) {
- const fieldObj = typeof field === 'string' ? JSON.parse(field) : field;
- const fieldType = fieldObj.type;
-
- if (fieldType === 'date' || fieldType === 'datetime' || fieldType === 'timepicker') {
- // 日期、日期时间或时间选择器类型:确保是有效的 dayjs 对象或 undefined
- if (isValidDayjs(value)) {
- // 已经是有效的 dayjs 对象
- cleanedFormValues[key] = value;
- } else if (value !== null && value !== undefined) {
- // 尝试转换
- const dayjsValue = safeToDayjs(value);
- cleanedFormValues[key] = dayjsValue || undefined;
- } else {
- cleanedFormValues[key] = undefined;
- }
- } else if (fieldType === 'daterange') {
- // 日期范围类型:确保数组中的每个元素都是有效的 dayjs 对象
- if (Array.isArray(value) && value.length === 2) {
- const [start, end] = value;
- // 验证开始时间
- let startDayjs: dayjs.Dayjs | null = null;
- if (isValidDayjs(start)) {
- startDayjs = start;
- } else {
- startDayjs = safeToDayjs(start);
- }
- // 验证结束时间
- let endDayjs: dayjs.Dayjs | null = null;
- if (isValidDayjs(end)) {
- endDayjs = end;
- } else {
- endDayjs = safeToDayjs(end);
- }
- cleanedFormValues[key] = (startDayjs && endDayjs) ? [startDayjs, endDayjs] : undefined;
- } else {
- cleanedFormValues[key] = undefined;
- }
- } else {
- // 其他类型直接使用
- cleanedFormValues[key] = value;
- }
- } else {
- // 没有找到字段定义,直接使用原值
- cleanedFormValues[key] = value;
- }
- });
-
- // 延迟一下,确保表单字段已经渲染
- setTimeout(() => {
- try {
- detailForm.setFieldsValue(cleanedFormValues);
- console.log('TaskManagement: 表单数据已回填', cleanedFormValues);
- } catch (e) {
- console.error('TaskManagement: 设置表单值失败', e);
- // 如果设置失败,尝试只设置非日期字段
- const safeValues: any = {};
- Object.keys(cleanedFormValues).forEach(key => {
- const value = cleanedFormValues[key];
- const field = fieldsArray.find((f: any) => {
- const fieldObj = typeof f === 'string' ? JSON.parse(f) : f;
- return (fieldObj.name || fieldObj.field) === key;
- });
- if (field) {
- const fieldObj = typeof field === 'string' ? JSON.parse(field) : field;
- const fieldType = fieldObj.type;
- if (fieldType !== 'date' && fieldType !== 'datetime' && fieldType !== 'daterange' && fieldType !== 'timepicker') {
- safeValues[key] = value;
- }
- } else {
- safeValues[key] = value;
- }
- });
- detailForm.setFieldsValue(safeValues);
- }
- }, 100);
- } catch (e) {
- console.error('TaskManagement: 解析表单数据失败', e);
- }
- } else if (!isApproved) {
- // 任务进行中:清空自定义表单,避免带出历史或隔离方案等填写内容
- setTimeout(() => { try { detailForm.resetFields(); } catch (_) { /* ignore */ } }, 100);
- }
- // 隔离/解除隔离完成查看详情:从 formData 或节点详情顶层回显 deviceNumber、attachments(部分解除隔离节点仅顶层有数据)
- if (isApproved && (isIsolation || isReleaseIsolation)) {
- try {
- 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 [];
- };
- let deviceNum: any;
- let listF: any[];
- if (detailDataWithWorkInfo.formData) {
- const parsed = typeof detailDataWithWorkInfo.formData === 'string'
- ? JSON.parse(detailDataWithWorkInfo.formData)
- : detailDataWithWorkInfo.formData;
- listF = normAtt(parsed.attachments).length ? normAtt(parsed.attachments) : normAtt((detailDataWithWorkInfo as any).attachments);
- deviceNum = parsed.deviceNumber ?? (detailDataWithWorkInfo as any).deviceNumber;
- } else {
- deviceNum = (detailDataWithWorkInfo as any).deviceNumber;
- listF = normAtt((detailDataWithWorkInfo as any).attachments);
- }
- const hasDev = deviceNum !== undefined && deviceNum !== null;
- if (hasDev || listF.length > 0) {
- setIsolationDeviceNumber(deviceNum ?? '');
- 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,
- }))
- );
- console.log('TaskManagement: formId 分支 deviceNumber/attachments 已回显', { deviceNumber: deviceNum, attachmentsCount: listF.length });
- }
- } catch (_) { /* ignore */ }
- }
- } else {
- console.warn('TaskManagement: 表单详情缺少配置或字段', { conf: !!conf, fields: !!fields, formDetail });
- message.warning('表单配置不完整');
- }
- } catch (e) {
- console.error('TaskManagement: 获取表单详情失败', e);
- message.error('获取表单详情失败: ' + (e instanceof Error ? e.message : String(e)));
- // 即使获取表单失败,也继续显示弹框
- } finally {
- setFormLoading(false);
- }
- })();
- } else {
- setFormLoading(false);
- // 隔离/解除隔离已完成但未走 formData 或 formId 分支时,从节点详情顶层回显 deviceNumber、attachments
- if (isApproved && (isIsolation || isReleaseIsolation)) {
- try {
- 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 [];
- };
- const deviceNum = (detailDataWithWorkInfo as any).deviceNumber;
- const listF = normAtt((detailDataWithWorkInfo as any).attachments);
- if ((deviceNum !== undefined && deviceNum !== null) || listF.length > 0) {
- setIsolationDeviceNumber(deviceNum ?? '');
- 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,
- }))
- );
- console.log('TaskManagement: 隔离/解除隔离(无formId分支) deviceNumber/attachments 已从顶层回显', { deviceNumber: deviceNum, attachmentsCount: listF.length });
- }
- } catch (_) { /* ignore */ }
- }
- if (isIsolation || isReleaseIsolation || isReturnLock) {
- console.log('TaskManagement: ⚠️ 隔离类型节点,跳过表单配置获取', { nodeType, isIsolation, isReleaseIsolation, isReturnLock });
- } else if (!hasFormId) {
- console.warn('TaskManagement: ⚠️ 没有表单ID,跳过表单配置获取', {
- formId,
- formIdType: typeof formId,
- hasFormId,
- nodeType,
- 'detailDataWithWorkInfo': detailDataWithWorkInfo
- });
- // 不显示提示,静默处理
- } else {
- console.warn('TaskManagement: ⚠️ 未知原因未获取表单', {
- nodeType,
- isIsolation,
- isReleaseIsolation,
- isReturnLock,
- hasFormId,
- formId
- });
- }
- }
-
- // 注意:表单数据回填已移到表单配置加载完成后,确保表单字段已渲染
-
- // 回显审核意见(如果有 approvalOpinion 字段,且不是 "pending")
- if (detailDataWithWorkInfo.approvalOpinion && detailDataWithWorkInfo.approvalOpinion !== 'pending') {
- setApprovalComment(detailDataWithWorkInfo.approvalOpinion);
- console.log('TaskManagement: 回显审核意见', detailDataWithWorkInfo.approvalOpinion);
- } else {
- setApprovalComment(''); // 重置审核意见(包括 pending 的情况)
- }
-
- setDetailLoading(false);
- console.log('TaskManagement: 弹框已打开', { detailVisible: true, detailData: detailDataWithWorkInfo });
- } catch (error: any) {
- console.error('TaskManagement: 获取节点详情失败', error);
- toast.error(error.message || '获取节点详情失败');
- // 即使接口失败,也显示弹框(显示错误信息)
- if (!detailData) {
- setDetailData({
- id: record.nodeId,
- nodeId: record.nodeId,
- workId: record.workId,
- workName: record.name,
- orderNo: record.orderNo,
- workerUserName: record.workerUserName,
- workTime: record.workTime,
- type: '',
- } as MyTaskNodeDetailVO);
- }
- setDetailVisible(true);
- } finally {
- setDetailLoading(false);
- }
- };
- // 获取任务状态显示文本(使用 approval_status 字典)
- const getTaskStatusText = (status: string | number | undefined): string => {
- if (!status) return '未知';
- const statusStr = String(status).toLowerCase();
- const statusItem = approvalStatusDictList.find(item => String(item.value).toLowerCase() === statusStr);
- if (statusItem) {
- return statusItem.label || statusItem.name || '未知';
- }
- // 如果没有找到字典值,使用默认映射
- const statusMap: Record<string, string> = {
- 'pending': '待审核',
- 'approved': '已通过',
- 'rejected': '已驳回',
- 'unaudited': '未审核',
- };
- return statusMap[statusStr] || '未知';
- };
- // 获取任务状态样式(审批状态)
- const getTaskStatusStyle = (status: string | number | undefined): React.CSSProperties => {
- if (!status) {
- return {
- backgroundColor: '#e5e5e5',
- color: '#333333',
- };
- }
- const statusStr = String(status).toLowerCase();
-
- // 先获取状态文本,根据文本判断颜色
- const statusText = getTaskStatusText(status);
- const statusTextLower = statusText.toLowerCase();
-
- // 根据状态文本判断颜色
- // 待执行、待发布:灰色 #e5e5e5
- if (statusTextLower.includes('待执行') || statusTextLower.includes('待发布') ||
- statusTextLower.includes('未开始') || statusTextLower.includes('未审核') ||
- statusTextLower.includes('待开始') || statusTextLower.includes('unaudited')) {
- return {
- backgroundColor: '#e5e5e5',
- color: '#333333',
- };
- }
- // 进行中:蓝色 #1677ff
- if (statusTextLower.includes('进行中') || statusTextLower.includes('待审核') ||
- statusTextLower.includes('pending') || statusTextLower.includes('执行中')) {
- return {
- backgroundColor: '#1677ff',
- color: '#ffffff',
- };
- }
- // 已完成:深绿色 #15803d
- if (statusTextLower.includes('已完成') || statusTextLower.includes('已通过') ||
- statusTextLower.includes('approved') || statusTextLower.includes('完成') ||
- statusTextLower.includes('执行完成')) {
- return {
- backgroundColor: '#15803d',
- color: '#ffffff',
- };
- }
-
- // 如果没有匹配到,默认灰色
- return {
- backgroundColor: '#e5e5e5',
- color: '#333333',
- };
- };
- // 作业状态文案(workDetail.status:作业整体状态,与任务状态 approvalStatus/nodeProgress 不同)
- const getWorkStatusText = (status: string | number | undefined): string => {
- if (status == null || String(status).trim() === '') return t('workJobDetail.statusUnknown');
- const s = String(status).toLowerCase();
- // 后端可能直接返回「执行完成」「已完成」等中文文案,这里统一映射为「已完成」
- if (s.includes('执行完成') || s.includes('已完成') || s === 'completed') {
- return '已完成';
- }
- const map: Record<string, string> = {
- pending: t('workJobDetail.statusPending'),
- running: t('workJobDetail.statusRunning'),
- completed: t('workJobDetail.statusCompleted'),
- rejected: t('workJobDetail.statusRejected'),
- skipped: t('workJobDetail.statusSkipped'),
- cancelled: t('workJobDetail.statusCancelled'),
- };
- return map[s] ?? String(status) ?? t('workJobDetail.statusUnknown');
- };
- const getWorkStatusStyle = (status: string | number | undefined): React.CSSProperties => {
- const s = String(status || '').toLowerCase();
- if (['running', '2'].includes(s)) return { backgroundColor: '#1677ff', color: '#fff' };
- if (['completed', 'approved'].includes(s)) return { backgroundColor: '#15803d', color: '#fff' };
- if (['rejected', 'cancelled'].includes(s)) return { backgroundColor: '#ff4d4f', color: '#fff' };
- if (['pending', '1'].includes(s)) return { backgroundColor: '#e5e5e5', color: '#333' };
- return { backgroundColor: '#e2e8f0', color: '#64748b' };
- };
- // 获取紧急程度样式
- // 获取紧急程度图标和样式(根据字典 value 判断:0=一般,1=紧急,2=非常紧急)
- const getUrgencyLevelIconAndStyle = (urgencyValue: string | number | undefined): { icon: React.ReactNode; style: React.CSSProperties } => {
- if (urgencyValue === null || urgencyValue === undefined || urgencyValue === '') {
- return {
- icon: null,
- style: {
- backgroundColor: '#e5e5e5',
- color: '#333333',
- },
- };
- }
-
- const value = Number(urgencyValue);
-
- // 0 = 一般:使用 urgecy1.png 图标 + 黑色文字
- if (value === 0) {
- return {
- icon: (
- <img
- src={urgecy1Icon}
- alt="一般"
- className="w-5 h-5 flex-shrink-0 mr-1.5"
- style={{ objectFit: 'contain' }}
- />
- ),
- style: {
- backgroundColor: 'transparent',
- color: '#000000',
- },
- };
- }
-
- // 1 = 紧急:使用 urgecy2.png 图标 + 橙色加粗文字
- if (value === 1) {
- return {
- icon: (
- <img
- src={urgecy2Icon}
- alt="紧急"
- className="w-5 h-5 flex-shrink-0 mr-1.5"
- style={{ objectFit: 'contain' }}
- />
- ),
- style: {
- backgroundColor: 'transparent',
- color: '#fa8c16',
- fontWeight: 'bold',
- },
- };
- }
-
- // 2 = 非常紧急:使用 urgecy3.png 图标 + 红色加粗文字
- if (value === 2) {
- return {
- icon: (
- <img
- src={urgecy3Icon}
- alt="非常紧急"
- className="w-5 h-5 flex-shrink-0 mr-1.5"
- style={{ objectFit: 'contain' }}
- />
- ),
- style: {
- backgroundColor: 'transparent',
- color: '#ff4d4f',
- fontWeight: 'bold',
- },
- };
- }
-
- // 如果没有匹配到,默认灰色
- return {
- icon: null,
- style: {
- backgroundColor: '#e5e5e5',
- color: '#333333',
- },
- };
- };
- // 获取紧急程度样式(保留用于向后兼容)
- const getUrgencyLevelStyle = (urgencyValue: string | number | undefined): React.CSSProperties => {
- return getUrgencyLevelIconAndStyle(urgencyValue).style;
- };
- // 表格列配置
- const columns: ColumnsType<MyTaskVO> = [
- {
- title: t('form.taskId'),
- dataIndex: 'orderNo',
- width: 180,
- align: 'center',
- render: (text: string) => text || '-',
- },
- {
- title: t('form.taskName'),
- dataIndex: 'name',
- width: 220,
- align: 'center',
- ellipsis: true,
- render: (text: string) => text || '-',
- },
- {
- title: t('form.currentTask'),
- dataIndex: 'currentNodeName',
- width: 180,
- align: 'center',
- ellipsis: true,
- render: (text: string, record: MyTaskVO) => {
- const displayText = text || record.currentNode || '-';
- if (!displayText || displayText === '-') return displayText;
- return (
- <span
- className="cursor-pointer hover:underline"
- style={{ color: '#1677ff' }}
- onMouseEnter={(e) => {
- e.currentTarget.style.textDecoration = 'underline';
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.textDecoration = 'none';
- }}
- onClick={(e) => {
- e.stopPropagation();
- e.preventDefault();
- handleViewDetail(record);
- }}
- title={displayText}
- >
- {displayText}
- </span>
- );
- },
- },
- {
- title: t('form.taskStatus'),
- dataIndex: 'approvalStatus',
- width: 120,
- align: 'center',
- render: (status: string | number | undefined, record: MyTaskVO) => {
- // 优先使用 approvalStatus,如果没有则使用其他状态字段
- const taskStatus = status || record.taskStatus || record.status;
- // 列表里“已完成/已通过”使用浅绿,弹框/详情仍保持深绿
- const baseStyle = getTaskStatusStyle(taskStatus);
- const statusTextLower = String(getTaskStatusText(taskStatus) || '').toLowerCase();
- const listStyle =
- statusTextLower.includes('已完成') ||
- statusTextLower.includes('已通过') ||
- statusTextLower.includes('approved') ||
- statusTextLower.includes('完成') ||
- statusTextLower.includes('执行完成')
- ? { ...baseStyle, backgroundColor: '#0acb57', color: '#ffffff' }
- : baseStyle;
- return (
- <span
- className="inline-flex px-3 py-1 rounded-lg text-xs"
- style={listStyle}
- >
- {getTaskStatusText(taskStatus)}
- </span>
- );
- },
- },
- {
- title: t('form.responsiblePerson'),
- dataIndex: 'workerUserName',
- width: 150,
- align: 'center',
- render: (text: string, record: MyTaskVO) => {
- // 优先使用 workerUserName,如果没有则尝试从其他字段获取
- return text || record.responsibleName || record.responsible || record.initiatorName || record.initiator || '-';
- },
- },
- {
- title: t('form.taskStartTime'),
- dataIndex: 'workTime',
- width: 180,
- align: 'center',
- render: (text: string | number | Date | undefined, record: MyTaskVO) => {
- // 优先使用 workTime,如果没有则使用其他时间字段
- const time = text || record.taskStartTime || record.initiationTime || record.initiateTime;
- return time ? dateFormatter(time) : '-';
- },
- },
- {
- title: t('form.urgencyLevel'),
- dataIndex: 'urgencyLevel',
- width: 120,
- align: 'center',
- render: (urgencyLevel: string | number | undefined, record: MyTaskVO) => {
- const urgencyValue = urgencyLevel || record.urgencyLevel;
- const urgencyItem = urgencyLevelDictList.find(item => String(item.value) === String(urgencyValue));
- const urgencyText = urgencyItem ? (urgencyItem.label || '') : (urgencyValue ? String(urgencyValue) : '-');
-
- if (!urgencyValue || urgencyValue === null || urgencyValue === undefined || urgencyValue === '') {
- return <span>-</span>;
- }
-
- const { icon, style } = getUrgencyLevelIconAndStyle(urgencyValue);
-
- return (
- <span
- className="inline-flex items-center justify-center gap-1.5"
- >
- {icon}
- <span style={style}>{urgencyText}</span>
- </span>
- );
- },
- },
- {
- title: t('common.operation'),
- width: 120,
- align: 'center',
- fixed: 'right',
- render: (_: any, record: MyTaskVO) => {
- // 获取任务状态文本
- const statusText = getTaskStatusText(record.approvalStatus);
- const statusTextLower = statusText.toLowerCase();
-
- // 判断是否为进行中状态
- const isInProgress = statusTextLower.includes('进行中') ||
- statusTextLower.includes('待审核') ||
- statusTextLower.includes('pending') ||
- statusTextLower.includes('执行中');
-
- // 判断是否为已完成状态
- const isCompleted = statusTextLower.includes('已完成') ||
- statusTextLower.includes('已通过') ||
- statusTextLower.includes('approved') ||
- statusTextLower.includes('完成') ||
- statusTextLower.includes('执行完成');
-
- // 根据状态显示不同的文字
- const buttonText = isInProgress ? t('form.handleNow') : isCompleted ? t('form.viewDetail') : t('form.viewDetail');
-
- return (
- <Space size="small">
- <Button
- type="link"
- size="small"
- icon={<Eye className="w-4 h-4" style={{ color: '#000000' }} />}
- onClick={(e) => {
- e.stopPropagation();
- e.preventDefault();
- handleViewDetail(record);
- }}
- style={{ color: '#000000' }}
- className="transition-colors hover:text-[#1677ff] hover:underline"
- onMouseEnter={(e) => {
- e.currentTarget.style.color = '#1677ff';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = '#000000';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
- }}
- >
- {buttonText}
- </Button>
- </Space>
- );
- },
- },
- ];
- // 计算分页数据
- const totalPages = Math.ceil(total / queryParams.pageSize) || 1;
- return (
- <div className="space-y-6">
- {/* 操作栏和表格容器 */}
- <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm overflow-hidden">
- {/* 查询与操作栏 */}
- <div className="p-4 lg:p-5 border-b border-gray-200/50">
- <div className="flex flex-wrap items-center gap-2 lg:gap-3 min-w-0">
- {/* 搜索条件:放宽到 280px,可缩小但不挤压不可见 */}
- <div className="flex items-center gap-2 lg:gap-3 w-[280px] min-w-[180px] flex-shrink">
- <span className="text-sm font-medium text-gray-700 whitespace-nowrap flex-shrink-0">{t('form.workOrderNo')}:</span>
- <Input
- value={searchOrderNo}
- onChange={(e) => setSearchOrderNo(e.target.value)}
- placeholder={t('form.workOrderNoPlaceholder')}
- allowClear
- className="min-w-0 w-full"
- onPressEnter={handleSearch}
- />
- </div>
- <div className="flex items-center gap-2 lg:gap-3 w-[280px] min-w-[180px] flex-shrink">
- <span className="text-sm font-medium text-gray-700 whitespace-nowrap flex-shrink-0">{t('form.workName')}:</span>
- <Input
- value={searchName}
- onChange={(e) => setSearchName(e.target.value)}
- placeholder={t('form.workNamePlaceholder')}
- allowClear
- className="min-w-0 w-full"
- onPressEnter={handleSearch}
- />
- </div>
- <Space size="small">
- <Button type="primary" icon={<Search className="w-4 h-4" />} onClick={handleSearch}>
- {t('common.search')}
- </Button>
- <Button icon={<RotateCcw className="w-4 h-4" />} onClick={handleReset}>
- {t('common.reset')}
- </Button>
- </Space>
- </div>
- </div>
- {/* 表格容器:不设 overflow,避免出现第二层纵向滚动条,仅用 Table 的 scroll.x 做横向滚动 */}
- <div className="min-w-0">
- <AntdTable
- loading={loading}
- columns={columns}
- dataSource={list}
- rowKey={(record) => record.id || Math.random()}
- pagination={false}
- scroll={{ x: 1200 }}
- locale={{
- emptyText: t('form.noData'),
- }}
- />
- </div>
- </div>
- {/* 分页 */}
- {total > 0 && (
- <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
- <div className="flex items-center justify-between">
- <div className="text-sm text-gray-600">
- {t('form.totalRecords')} <span className="text-blue-600 font-medium">{total}</span> {t('form.recordsUnit')}
- </div>
- <div className="flex gap-2">
- <Button
- onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo - 1 })}
- disabled={queryParams.pageNo <= 1}
- >
- {t('common.prevPage')}
- </Button>
- <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
- {queryParams.pageNo} / {totalPages}
- </span>
- <Button
- onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo + 1 })}
- disabled={queryParams.pageNo >= totalPages}
- >
- {t('common.nextPage')}
- </Button>
- </div>
- </div>
- </div>
- )}
- {/* 详情弹框 */}
- <Modal
- title={null}
- open={detailVisible}
- onCancel={() => {
- console.log('TaskManagement: 弹框关闭');
- setDetailVisible(false);
- setDetailData(null);
- setFormData({ rule: [], option: {} });
- setFormLoading(false);
- setOriginalFields([]);
- setOriginalConf('');
- detailForm.resetFields();
- setApprovalComment('');
- setIsolationDeviceNumber('');
- setIsolationFileList([]);
- }}
- footer={null}
- width={1000}
- destroyOnClose
- confirmLoading={detailLoading}
- maskClosable={false}
- zIndex={1000}
- styles={{
- body: {
- minHeight: '600px',
- maxHeight: '700px',
- height: '700px',
- padding: 0,
- display: 'flex',
- flexDirection: 'column',
- overflow: 'hidden'
- }
- }}
- >
- {(() => {
- console.log('TaskManagement: Modal 内容渲染', { detailLoading, detailData, detailVisible });
- if (detailLoading) {
- return <div className="py-8 text-center">{t('form.loading')}</div>;
- }
- if (detailData) {
- return (
- <div style={{
- display: 'flex',
- flexDirection: 'column',
- height: '100%',
- overflow: 'hidden'
- }}>
- {/* 标题区域:左上角显示作业状态(有 workDetail 时),否则显示任务状态 */}
- <div style={{
- padding: '24px 28px 20px',
- background: 'linear-gradient(180deg, #fafbfd 0%, #ffffff 100%)',
- borderBottom: '1px solid #eef0f4',
- }}>
- <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
- <div style={{
- width: '4px',
- height: '22px',
- backgroundColor: '#2563eb',
- borderRadius: '2px',
- flexShrink: 0,
- }} />
- <h2 style={{ color: '#1e293b', fontSize: 20, fontWeight: 600, margin: 0, letterSpacing: '-0.02em' }}>
- {detailData.workName || detailData.name || t('form.workDetail')}
- </h2>
- {(() => {
- const workStatus = (detailData as any).workDetail?.status;
- if (workStatus != null && String(workStatus).trim() !== '') {
- const baseStyle = getWorkStatusStyle(workStatus);
- const workText = getWorkStatusText(workStatus);
- const isCompleted = String(workText || '').toLowerCase().includes('已完成');
- const headerStyle = isCompleted ? { ...baseStyle, backgroundColor: '#0acb57', color: '#ffffff' } : baseStyle;
- return (
- <span className="inline-flex px-3 py-1 rounded-full text-xs font-medium" style={headerStyle}>
- {workText}
- </span>
- );
- }
- return (
- <span
- className="inline-flex px-3 py-1 rounded-full text-xs font-medium"
- style={(() => {
- const s = detailData.approvalStatus ?? detailData.taskStatus ?? (detailData as any).nodeProgress;
- const baseStyle = getTaskStatusStyle(s);
- const textLower = String(getTaskStatusText(s) || '').toLowerCase();
- const isCompleted =
- textLower.includes('已完成') ||
- textLower.includes('已通过') ||
- textLower.includes('approved') ||
- textLower.includes('完成') ||
- textLower.includes('执行完成');
- return isCompleted ? { ...baseStyle, backgroundColor: '#0acb57', color: '#ffffff' } : baseStyle;
- })()}
- >
- {getTaskStatusText(detailData.approvalStatus ?? detailData.taskStatus ?? (detailData as any).nodeProgress)}
- </span>
- );
- })()}
- </div>
- <div className="text-sm flex flex-wrap gap-y-2" style={{ color: '#64748b', marginTop: 14 }}>
- <span>{t('form.workOrderNo')}:{detailData.orderNo || '-'}</span>
- <span style={{ paddingLeft: 16, marginLeft: 12, borderLeft: '1px solid #e2e8f0' }}>
- {t('form.workResponsible')}:{detailData.initiatorName || '-'}</span>
- <span style={{ paddingLeft: 16, marginLeft: 12, borderLeft: '1px solid #e2e8f0' }}>
- {t('form.taskResponsible')}:{detailData.workerUserName || '-'}</span>
- <span style={{ paddingLeft: 16, marginLeft: 12, borderLeft: '1px solid #e2e8f0' }}>
- {t('form.initiationTime')}:{(detailData as any).workDetail?.initiationTime != null ? dateFormatter((detailData as any).workDetail.initiationTime) : (detailData.initiationTime != null ? dateFormatter(detailData.initiationTime) : (detailData.workTime ? dateFormatter(detailData.workTime) : '-'))}
- </span>
- </div>
- </div>
- {/* 内容区域 - 可滚动 */}
- <div
- style={{
- padding: '20px 24px 24px',
- flex: 1,
- overflowY: 'auto',
- minHeight: 0,
- background: '#f8fafc',
- }}
- >
- {(() => {
- // 检查节点状态是否为已通过(approved),如果是则禁用所有输入和按钮
- const isApproved = detailData?.approvalStatus === 'approved';
-
- // 从 detailData.type 或 detailData.nodeType 获取节点类型
- const nodeType = String(detailData?.type || detailData?.nodeType || '').trim();
- console.log('TaskManagement: 渲染内容区域 - 节点类型', nodeType, 'detailData:', detailData);
- // 创建作业类型节点:展示作业详情(作业类型、作业名称、作业发起人、作业分类、作业内容等)
- if (nodeType === 'createJob') {
- const wd = (detailData as any).workDetail || {};
- const getLabelFromList = (list: any[], val: any) => list?.find((d: any) => String(d.value) === String(val))?.label ?? val;
- const taskStartTime = (detailData as any).startTime ?? (detailData as any).createTime ?? detailData.workTime;
- const taskStatus = detailData.approvalStatus ?? (detailData as any).taskStatus ?? (detailData as any).nodeProgress;
- const jobCategoryVal = (wd as any).workCategory ?? (wd as any).jobCategory ?? wd.type;
- const jobCategoryLabel = jobCategoryVal != null ? (getLabelFromList(jobCategoryDictList, jobCategoryVal) || jobCategoryVal) : '-';
- return (
- <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
- {/* 任务概览卡片 */}
- <div style={{
- background: '#ffffff',
- borderRadius: 12,
- padding: '18px 20px',
- boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
- border: '1px solid #eef0f4',
- }}>
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: '16px 28px', alignItems: 'center' }}>
- <span style={{ display: 'inline-flex', alignItems: 'center', fontSize: 14, color: '#64748b' }}>
- 任务名称:<span style={{ color: '#1e293b', fontWeight: 600 }}>{detailData?.nodeName ?? '-'}</span>
- <span
- className="inline-flex px-3 py-1 rounded-full text-xs font-medium"
- style={(() => {
- const baseStyle = getTaskStatusStyle(taskStatus);
- const textLower = String(getTaskStatusText(taskStatus) || '').toLowerCase();
- const isCompleted = textLower.includes('已完成') || textLower.includes('已通过') || textLower.includes('approved') || textLower.includes('完成') || textLower.includes('执行完成');
- const badgeStyle = isCompleted ? { ...baseStyle, backgroundColor: '#0acb57', color: '#ffffff', marginLeft: 10 } : { ...baseStyle, marginLeft: 10 };
- return badgeStyle;
- })()}
- >
- {getTaskStatusText(taskStatus)}
- </span>
- </span>
- <span style={{ fontSize: 14, color: '#64748b' }}>负责人:<span style={{ color: '#1e293b', fontWeight: 600 }}>{detailData?.workerUserName ?? '-'}</span></span>
- <span style={{ fontSize: 14, color: '#64748b' }}>开始时间:<span style={{ color: '#1e293b', fontWeight: 600 }}>{taskStartTime != null ? dateFormatter(taskStartTime) : '-'}</span></span>
- </div>
- </div>
- {/* 作业属性卡片 */}
- <div style={{
- background: '#ffffff',
- borderRadius: 12,
- padding: '20px',
- boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
- border: '1px solid #eef0f4',
- }}>
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '20px 32px' }}>
- <div><span style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>作业名称</span><div style={{ fontSize: 14, color: '#1e293b', fontWeight: 500 }}>{wd.name ?? detailData.workName ?? detailData.name ?? '-'}</div></div>
- <div><span style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>作业分类</span><div style={{ fontSize: 14, color: '#1e293b', fontWeight: 500 }}>{jobCategoryLabel}</div></div>
- <div><span style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>{t('workJobDetail.workflowTemplate') || '流程模板'}</span><div style={{ fontSize: 14, color: '#1e293b', fontWeight: 500 }}>{(wd as any).designName ?? '-'}</div></div>
- <div><span style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>{t('workJobDetail.urgencyLevel') || '紧急程度'}</span><div style={{ fontSize: 14, color: '#1e293b', fontWeight: 500 }}>{(wd as any).urgencyLevel != null ? (getLabelFromList(urgencyLevelDictList, (wd as any).urgencyLevel) || (wd as any).urgencyLevel) : '-'}</div></div>
- </div>
- </div>
- {/* 作业内容卡片 */}
- <div style={{
- background: '#ffffff',
- borderRadius: 12,
- padding: 20,
- boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
- border: '1px solid #eef0f4',
- }}>
- <span style={{ fontSize: 12, color: '#64748b', fontWeight: 500, display: 'block', marginBottom: 10 }}>{t('workJobDetail.jobContent') || '作业内容'}</span>
- <div style={{ fontSize: 14, color: '#334155', lineHeight: 1.7, whiteSpace: 'pre-wrap', background: '#f8fafc', padding: 16, borderRadius: 10, border: '1px solid #eef0f4', minHeight: 60 }}>{(wd.description != null && String(wd.description).trim() !== '') ? String(wd.description).trim() : '-'}</div>
- </div>
- </div>
- );
- }
- const isReview = nodeType === 'review';
- const isIsolation = nodeType === 'isolation';
- const isReleaseIsolation = nodeType === 'releaseIsolation';
- const isReturnLock = nodeType === 'returnLock';
-
- console.log('TaskManagement: 节点类型判断结果', {
- isReview,
- isIsolation,
- isReleaseIsolation,
- isReturnLock,
- nodeType,
- 'detailData.type': detailData.type,
- 'detailData.nodeType': detailData.nodeType,
- 'typeof detailData.type': typeof detailData.type,
- 'String(detailData.type)': String(detailData.type)
- });
- // 当前节点是否有表单:无 formId 的节点(如完成/结束)不应渲染自定义表单
- const hasFormIdForCurrentNode = detailData?.formId !== undefined && detailData?.formId !== null && (
- typeof detailData.formId === 'number' || (typeof detailData.formId === 'string' && String(detailData.formId).trim() !== '')
- );
- // 隔离/方案节点、解除隔离节点和还锁节点
- if (isIsolation || isReleaseIsolation || isReturnLock) {
- const isolationType = String(detailData?.isolationType ?? '').trim();
- const isLockoutTagout = isolationType === '1'; // 上锁挂牌
- const isBlindPlate = isolationType === '0'; // 盲板
- const isDismantle = isolationType === '2'; // 拆除
- const showLockCabinet = isReturnLock || (isLockoutTagout && (isIsolation || isReleaseIsolation));
- const showDeviceForm = (isIsolation || isReleaseIsolation) && (isBlindPlate || isDismantle);
- if (showDeviceForm) {
- const deviceLabel = isReleaseIsolation
- ? (isBlindPlate ? t('form.releaseBlindPlateDeviceNo') : t('form.restoreDeviceNo'))
- : (isBlindPlate ? t('form.blindPlateDeviceNo') : t('form.dismantleDeviceNo'));
- const formIdVal = detailData?.formId;
- const hasFormIdForBlindDismantle = formIdVal !== undefined && formIdVal !== null && (
- typeof formIdVal === 'number' || (typeof formIdVal === 'string' && String(formIdVal).trim() !== '')
- );
- return (
- <div key="isolation-device-form" style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
- <div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '24px', background: 'transparent' }}>
- <div style={{ width: '100%' }}>
- <div style={{ marginBottom: 26 }}>
- <label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 600, color: '#1f2937', marginBottom: 12 }}>
- <span style={{ width: 28, height: 28, borderRadius: 8, background: '#e6f4ff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
- <BarcodeOutlined style={{ fontSize: 14, color: '#1677ff' }} />
- </span>
- {deviceLabel}
- </label>
- <Input
- value={isolationDeviceNumber}
- onChange={(e) => !isApproved && setIsolationDeviceNumber(e.target.value)}
- placeholder={`请输入${deviceLabel}`}
- maxLength={100}
- size="large"
- disabled={isApproved}
- prefix={<BarcodeOutlined style={{ color: '#91caff', marginRight: 8 }} />}
- style={{ borderRadius: 10, borderColor: '#d9e8ff', fontSize: 15 }}
- />
- </div>
- <div>
- <label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 14, fontWeight: 600, color: '#1f2937', marginBottom: 12 }}>
- <span style={{ width: 28, height: 28, borderRadius: 8, background: '#e6f4ff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
- <UploadOutlined style={{ fontSize: 14, color: '#1677ff' }} />
- </span>
- 附件上传
- {!isApproved && <span style={{ marginLeft: 8, fontSize: 12, fontWeight: 400, color: '#69b1ff', background: '#e6f4ff', padding: '2px 8px', borderRadius: 6 }}>支持拖拽</span>}
- </label>
- {isApproved && isolationFileList.length > 0 ? (
- <div
- style={{
- borderRadius: 12,
- padding: '32px 20px',
- background: 'linear-gradient(180deg, #f5faff 0%, #e8f4ff 50%, #e0efff 100%)',
- border: '2px solid #91caff',
- minHeight: 180,
- display: 'flex',
- flexWrap: 'wrap',
- gap: 16,
- alignItems: 'center',
- justifyContent: 'center',
- alignContent: 'center',
- }}
- >
- <Image.PreviewGroup>
- {isolationFileList.filter((f: any) => {
- const u = f.url || f.response;
- return /\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(String(u || '')) || /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(String(f.name || ''));
- }).map((file: any) => (
- <Image
- key={file.uid}
- width={160}
- height={160}
- src={file.url || file.response}
- style={{ objectFit: 'cover', borderRadius: 10, cursor: 'pointer', flexShrink: 0 }}
- alt={file.name}
- />
- ))}
- </Image.PreviewGroup>
- {isolationFileList.filter((f: any) => {
- const u = f.url || f.response;
- return !/\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i.test(String(u || '')) && !/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(String(f.name || ''));
- }).map((file: any) => (
- <a
- key={file.uid}
- href={file.url || file.response}
- target="_blank"
- rel="noopener noreferrer"
- style={{ display: 'block', padding: '12px 16px', background: '#fff', borderRadius: 10, color: '#1677ff', border: '1px solid #91caff', maxWidth: 240, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
- >
- {file.name}
- </a>
- ))}
- </div>
- ) : isApproved ? (
- <div style={{ padding: 24, textAlign: 'center', color: '#8c8c8c', background: '#fafafa', borderRadius: 12 }}>暂无附件</div>
- ) : (
- <Upload.Dragger
- fileList={isolationFileList}
- onChange={({ fileList }) => setIsolationFileList(fileList.slice(-5))}
- multiple
- showUploadList={{ showRemoveIcon: true }}
- customRequest={async ({ file, onSuccess, onError }) => {
- try {
- const url = await fileApi.upload(file as File);
- onSuccess?.(url);
- } catch (e: any) {
- onError?.(e);
- message.error(e?.message || '文件上传失败');
- }
- }}
- style={{
- borderRadius: 12,
- padding: '32px 20px',
- background: 'linear-gradient(180deg, #f5faff 0%, #e8f4ff 50%, #e0efff 100%)',
- border: '2px dashed #91caff',
- }}
- >
- <p className="ant-upload-drag-icon" style={{ marginBottom: 12 }}>
- <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' }}>
- <UploadOutlined style={{ fontSize: 32, color: '#1677ff' }} />
- </span>
- </p>
- <p className="ant-upload-text" style={{ margin: 0, color: '#0958d9', fontSize: 15, fontWeight: 500 }}>点击或拖拽文件到此区域上传</p>
- <p className="ant-upload-hint" style={{ margin: '8px 0 0', color: '#69b1ff', fontSize: 13 }}>最多 5 个文件,支持多选</p>
- </Upload.Dragger>
- )}
- </div>
- {/* 盲板/拆除有 formId 时在设备编号和附件上传下方渲染自定义表单,可滚动查看 */}
- {hasFormIdForBlindDismantle && (
- <div style={{ marginTop: 24, paddingTop: 24, borderTop: '1px solid rgba(22, 119, 255, 0.12)' }}>
- {formLoading ? (
- <div className="py-6 text-center text-gray-500">{t('form.formLoading')}</div>
- ) : formData.rule && formData.rule.length > 0 ? (
- (() => {
- const formConfig = formData.option?.formConfig || defaultFormConfig;
- const layoutColumns = formConfig.layoutColumns || 1;
- const gridStyle = layoutColumns > 1 ? {
- display: 'grid',
- gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
- gap: '12px',
- rowGap: '16px',
- } : undefined;
- return (
- <AntdForm
- form={detailForm}
- layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
- size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
- requiredMark={formConfig.hideRequiredMark ? false : undefined}
- labelCol={
- formConfig.labelPosition === 'top' || !formConfig.labelWidth
- ? undefined
- : {
- flex: `${formConfig.labelWidth}px`,
- style: {
- minWidth: `${formConfig.labelWidth}px`,
- textAlign: formConfig.labelPosition === 'right' ? 'right' : 'left',
- },
- }
- }
- wrapperCol={formConfig.labelPosition === 'top' ? undefined : { flex: 'auto', style: { minWidth: 0 } }}
- >
- <div style={gridStyle} className={layoutColumns === 1 ? 'space-y-4' : 'form-detail-grid'}>
- <style>{`.form-detail-grid .ant-form-item { margin-bottom: 12px; } .form-detail-grid .ant-form-item-control { min-width: 0; }`}</style>
- {(formData.rule || []).map((field: any) => {
- const fieldWithDisabled = isApproved ? { ...field, disabled: true, readOnly: true } : field;
- return renderFieldPreview(fieldWithDisabled);
- })}
- </div>
- </AntdForm>
- );
- })()
- ) : null}
- </div>
- )}
- </div>
- </div>
- </div>
- );
- }
- console.log('TaskManagement: ✅ 进入隔离节点渲染分支', { isIsolation, isReleaseIsolation, isReturnLock, nodeType, isolationType });
- // 隔离/方案(上锁挂牌)节点:三步并列分栏展示
- if (isIsolation && isLockoutTagout) {
- const points: any[] = Array.isArray((detailData as any)?.points) ? (detailData as any).points : [];
- const keysList: any[] = Array.isArray((detailData as any)?.keys) ? (detailData as any).keys : [];
- const jtLockerUsers: any[] = Array.isArray((detailData as any)?.nodeUserList)
- ? ((detailData as any).nodeUserList || []).filter((u: any) => String(u?.type || '').toLowerCase() === 'jtlocker')
- : [];
- const jtCoLockerUsers: any[] = Array.isArray((detailData as any)?.nodeUserList)
- ? ((detailData as any).nodeUserList || []).filter((u: any) => String(u?.type || '').toLowerCase() === 'jtcolocker')
- : [];
- const lockPersons: any[] = jtLockerUsers;
- const getPointName = (p: any) =>
- p?.pointName ??
- p?.name ??
- p?.pointLabel ??
- p?.pointCode ??
- (p?.id != null ? `点位${p.id}` : '-');
- const isLocked = (p: any) => Boolean(p?.lockTime) || p?.locked === true || String(p?.status || '').toLowerCase() === 'locked';
- const lockBadgeStyle = (locked: boolean): React.CSSProperties =>
- locked
- ? { backgroundColor: '#15803d', color: '#ffffff' }
- : { backgroundColor: '#e2e8f0', color: '#475569' };
- const getKeyStatus = (k: any) => {
- const status = String(k?.keyStatus ?? '').trim();
- // 钥匙状态(0-待取出 1-已取出 2-已归还)
- if (status === '0') return 'to_take';
- if (status === '1') return 'taken';
- if (status === '2') return 'returned';
- return 'unknown';
- };
- const getUserAvatarSrc = (u: any) =>
- u?.avatar ??
- u?.avatarUrl ??
- u?.headImg ??
- u?.headImage ??
- u?.headUrl ??
- u?.profilePhoto ??
- u?.photo ??
- undefined;
- const getUserName = (u: any) => u?.nickname ?? u?.nickName ?? u?.name ?? u?.username ?? '-';
- const getLockerStatus = (u: any) => {
- // 上锁人状态:0未上锁、1已上锁、2已解锁
- const s = String(u?.status ?? '').trim();
- if (s === '1') return 'locked';
- if (s === '2') return 'unlocked_done';
- if (s === '0') return 'unlocked';
- return 'unknown';
- };
- return (
- <div
- key="isolation-lockout-tabs"
- style={{
- height: '100%',
- display: 'flex',
- flexDirection: 'column',
- overflow: 'hidden',
- }}
- >
- <div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: 24, background: 'transparent' }}>
- <Card
- style={{
- width: '100%',
- borderRadius: 16,
- boxShadow: '0 8px 28px rgba(15, 23, 42, 0.08), 0 2px 8px rgba(0,0,0,0.04)',
- border: '1px solid #e6e8f0',
- background: '#ffffff',
- }}
- bodyStyle={{ padding: 18 }}
- >
- <div
- style={{
- display: 'grid',
- gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
- gap: 0,
- }}
- >
- {/* 第一步:取挂锁,取钥匙 */}
- <div style={{ padding: '0 14px', borderRight: '1px solid #eef0f6', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
- <div style={{ fontSize: 14, fontWeight: 600, color: '#0f172a', marginBottom: 12, minHeight: 22 }}>
- 第一步:取挂锁,取钥匙
- </div>
- <div style={{ padding: '8px 0', flex: 1 }}>
- <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>是否上锁</div>
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10, marginBottom: 14 }}>
- {(lockPersons.length > 0 ? lockPersons : [{ __empty: true }]).map((u: any, idx: number) => {
- if (u.__empty) {
- return (
- <div
- key="empty-locker"
- style={{
- padding: '12px 12px',
- borderRadius: 10,
- border: '1px dashed #cbd5e1',
- background: '#f8fafc',
- color: '#64748b',
- textAlign: 'center',
- fontSize: 13,
- }}
- >
- 暂无上锁人
- </div>
- );
- }
- const name = getUserName(u);
- const avatarSrc = getUserAvatarSrc(u);
- const st = getLockerStatus(u);
- return (
- <div
- key={u.id ?? u.userId ?? u.uid ?? idx}
- style={{
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'space-between',
- gap: 10,
- padding: '10px 10px',
- borderRadius: 10,
- border: '1px solid #e6e8f0',
- background: '#ffffff',
- }}
- >
- <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
- <Avatar size={32} src={avatarSrc} style={{ background: '#e5e7eb', color: '#111827', flexShrink: 0 }}>
- {String(name || '-').slice(0, 1)}
- </Avatar>
- <div style={{ minWidth: 0 }}>
- <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
- {name}
- </div>
- </div>
- </div>
- <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12 }}>
- {st === 'unknown' ? (
- <>
- <WarningOutlined style={{ fontSize: 14, color: '#f97316' }} />
- <span style={{ color: '#f97316' }}>未知</span>
- </>
- ) : st === 'locked' ? (
- <>
- <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} />
- <span style={{ color: '#15803d' }}>已上锁</span>
- </>
- ) : st === 'unlocked_done' ? (
- <>
- <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} />
- <span style={{ color: '#15803d' }}>已解锁</span>
- </>
- ) : (
- <>
- <CloseCircleOutlined style={{ fontSize: 14, color: '#dc2626' }} />
- <span style={{ color: '#dc2626' }}>未上锁</span>
- </>
- )}
- </span>
- </div>
- );
- })}
- </div>
- <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>
- 本次能量隔离任务中需要隔离上锁的点位
- </div>
- <div style={{ width: '100%', maxWidth: 320, display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10 }}>
- {(points.length > 0 ? points : [{ __empty: true }]).map((p: any, idx: number) => {
- if (p.__empty) {
- return (
- <div
- key="empty"
- style={{
- padding: '14px 12px',
- borderRadius: 10,
- border: '1px dashed #cbd5e1',
- background: '#f8fafc',
- color: '#64748b',
- textAlign: 'center',
- fontSize: 13,
- }}
- >
- 暂无点位
- </div>
- );
- }
- const locked = isLocked(p);
- return (
- <div
- key={p.id ?? p.pointId ?? idx}
- style={{
- display: 'flex',
- alignItems: 'center',
- gap: 10,
- padding: '10px 10px',
- borderRadius: 10,
- border: '1px solid #e6e8f0',
- background: '#ffffff',
- }}
- >
- <span
- style={{
- width: 32,
- height: 32,
- borderRadius: 10,
- background: '#f1f5ff',
- display: 'inline-flex',
- alignItems: 'center',
- justifyContent: 'center',
- flexShrink: 0,
- }}
- >
- <EnvironmentOutlined style={{ color: '#1677ff', fontSize: 16 }} />
- </span>
- <div style={{ minWidth: 0, flex: 1 }}>
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
- <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
- {getPointName(p)}
- </div>
- <span className="inline-flex px-3 py-1 rounded-full text-xs font-medium" style={lockBadgeStyle(locked)}>
- {locked ? '已上锁' : '未上锁'}
- </span>
- </div>
- </div>
- </div>
- );
- })}
- </div>
- </div>
- </div>
- {/* 第二步:上锁后归还钥匙 */}
- <div style={{ padding: '0 14px', borderRight: '1px solid #eef0f6', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
- <div style={{ fontSize: 14, fontWeight: 600, color: '#0f172a', marginBottom: 12, minHeight: 22 }}>
- 第二步:上锁后归还钥匙
- </div>
- <div style={{ padding: '8px 0', flex: 1 }}>
- <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>
- 是否有待归还的钥匙
- </div>
- <div style={{ width: '100%', maxWidth: 320, display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10 }}>
- {(keysList.length > 0 ? keysList : [{ __empty: true }]).map((k: any, idx: number) => {
- if (k.__empty) {
- return (
- <div
- key="empty-key"
- style={{
- padding: '14px 12px',
- borderRadius: 10,
- border: '1px dashed #cbd5e1',
- background: '#f8fafc',
- color: '#64748b',
- textAlign: 'center',
- fontSize: 13,
- }}
- >
- 暂无钥匙
- </div>
- );
- }
- const status = getKeyStatus(k);
- const isReturned = status === 'returned';
- let statusText = '未知';
- let statusColor = '#6b7280';
- if (status === 'to_take') {
- statusText = '待取出';
- statusColor = '#f97316';
- } else if (status === 'taken') {
- statusText = '已取出';
- statusColor = '#dc2626';
- } else if (isReturned) {
- statusText = '已归还';
- statusColor = '#15803d';
- }
- const returnedAt =
- k?.giveBackTime ??
- k?.returnTime ??
- k?.returnedTime ??
- k?.backTime ??
- k?.keyReturnTime ??
- k?.updateTime ??
- k?.returnedAt ??
- k?.returnAt;
- return (
- <div
- key={k.id ?? k.keyId ?? idx}
- style={{
- display: 'flex',
- alignItems: 'center',
- gap: 10,
- padding: '10px 10px',
- borderRadius: 10,
- border: '1px solid #e6e8f0',
- background: '#ffffff',
- }}
- >
- <span
- style={{
- width: 32,
- height: 32,
- borderRadius: 10,
- background: '#eff6ff',
- display: 'inline-flex',
- alignItems: 'center',
- justifyContent: 'center',
- flexShrink: 0,
- }}
- >
- <KeyOutlined style={{ color: '#2563eb', fontSize: 16 }} />
- </span>
- <div style={{ minWidth: 0, flex: 1 }}>
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
- <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
- {k?.keyNfc ?? k?.keyName ?? k?.name ?? k?.code ?? '-'}
- </div>
- <span
- style={{
- display: 'inline-flex',
- alignItems: 'center',
- gap: 4,
- fontSize: 12,
- color: statusColor,
- }}
- >
- {status === 'returned' ? (
- <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} />
- ) : status === 'taken' ? (
- <CloseCircleOutlined style={{ fontSize: 14, color: '#dc2626' }} />
- ) : status === 'to_take' ? (
- <WarningOutlined style={{ fontSize: 14, color: '#f97316' }} />
- ) : (
- <WarningOutlined style={{ fontSize: 14, color: '#9ca3af' }} />
- )}
- <span>{statusText}</span>
- </span>
- </div>
- <div style={{ marginTop: 4, fontSize: 12, color: '#64748b' }}>
- 归还时间:{isReturned && returnedAt != null && String(returnedAt).trim() !== '' ? dateFormatter(returnedAt) : '-'}
- </div>
- </div>
- </div>
- );
- })}
- </div>
- </div>
- </div>
- {/* 第三步:共锁人共锁 */}
- <div style={{ padding: '0 14px', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
- <div style={{ fontSize: 14, fontWeight: 600, color: '#0f172a', marginBottom: 12, minHeight: 22 }}>
- 第三步:共锁人共锁
- </div>
- <div style={{ padding: '8px 0', flex: 1 }}>
- <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>是否共锁</div>
- <div style={{ width: '100%', maxWidth: 320, display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10 }}>
- {(jtCoLockerUsers.length > 0 ? jtCoLockerUsers : [{ __empty: true }]).map((u: any, idx: number) => {
- if (u.__empty) {
- return (
- <div
- key="empty-colocker"
- style={{
- padding: '14px 12px',
- borderRadius: 10,
- border: '1px dashed #cbd5e1',
- background: '#f8fafc',
- color: '#64748b',
- textAlign: 'center',
- fontSize: 13,
- }}
- >
- 暂无共锁人
- </div>
- );
- }
- const name = getUserName(u);
- const avatarSrc = getUserAvatarSrc(u);
- // 共锁人状态: 0未共锁、1已共锁、2已解共锁
- const lockedStatus = String(u?.status ?? '').trim();
- const coLockStatusText = lockedStatus === '1' ? '已共锁' : lockedStatus === '2' ? '已解共锁' : '未共锁';
- const coLockStatusColor = lockedStatus === '1' ? '#15803d' : lockedStatus === '2' ? '#15803d' : '#dc2626';
- return (
- <div
- key={u.id ?? u.userId ?? u.uid ?? idx}
- style={{
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'space-between',
- gap: 10,
- padding: '10px 10px',
- borderRadius: 10,
- border: '1px solid #e6e8f0',
- background: '#ffffff',
- }}
- >
- <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
- <Avatar
- size={32}
- src={avatarSrc}
- style={{ background: '#e5e7eb', color: '#111827', flexShrink: 0 }}
- >
- {String(name || '-').slice(0, 1)}
- </Avatar>
- <div style={{ minWidth: 0 }}>
- <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
- {name}
- </div>
- </div>
- </div>
- <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: coLockStatusColor }}>
- {lockedStatus === '1' ? (
- <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} />
- ) : lockedStatus === '2' ? (
- <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} />
- ) : (
- <CloseCircleOutlined style={{ fontSize: 14, color: '#dc2626' }} />
- )}
- <span>{coLockStatusText}</span>
- </span>
- </div>
- );
- })}
- </div>
- </div>
- </div>
- </div>
- </Card>
- </div>
- </div>
- );
- }
- // 解除隔离(上锁挂牌)节点:三步并列分栏展示
- if (isReleaseIsolation && isLockoutTagout) {
- const releaseKeysList: any[] = Array.isArray((detailData as any)?.keys) ? (detailData as any).keys : [];
- const releaseCoLockers: any[] = Array.isArray((detailData as any)?.nodeUserList)
- ? ((detailData as any).nodeUserList || []).filter((u: any) => String(u?.type || '').toLowerCase() === 'jtcolocker')
- : [];
- const releaseLockPersons: any[] = Array.isArray((detailData as any)?.nodeUserList)
- ? ((detailData as any).nodeUserList || []).filter((u: any) => String(u?.type || '').toLowerCase() === 'jtlocker')
- : [];
- const getReleaseKeyStatus = (k: any) => {
- const s = String(k?.keyStatus ?? '').trim();
- // 钥匙状态(0-待取出 1-已取出 2-已归还)
- if (s === '0') return 'to_take';
- if (s === '1') return 'taken';
- if (s === '2') return 'returned';
- return 'unknown';
- };
- const getReleaseUserAvatarSrc = (u: any) =>
- u?.avatar ?? u?.avatarUrl ?? u?.headImg ?? u?.headImage ?? u?.headUrl ?? u?.profilePhoto ?? u?.photo ?? undefined;
- const getReleaseUserName = (u: any) => u?.nickname ?? u?.nickName ?? u?.name ?? u?.username ?? '-';
- return (
- <div key="release-isolation-columns" style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
- <div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: 24, background: 'transparent' }}>
- <Card
- style={{
- width: '100%',
- borderRadius: 16,
- boxShadow: '0 8px 28px rgba(15, 23, 42, 0.08), 0 2px 8px rgba(0,0,0,0.04)',
- border: '1px solid #e6e8f0',
- background: '#ffffff',
- }}
- bodyStyle={{ padding: 18 }}
- >
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 0 }}>
- {/* 第一步:解除共锁 */}
- <div style={{ padding: '0 14px', borderRight: '1px solid #eef0f6', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
- <div style={{ fontSize: 14, fontWeight: 600, color: '#0f172a', marginBottom: 12, minHeight: 22 }}>第一步:解除共锁</div>
- <div style={{ padding: '8px 0', flex: 1 }}>
- <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>是否解除共锁</div>
- <div style={{ width: '100%', maxWidth: 320, display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10 }}>
- {(releaseCoLockers.length > 0 ? releaseCoLockers : [{ __empty: true }]).map((u: any, idx: number) => {
- if (u.__empty) {
- return (
- <div key="empty-release-colocker" style={{ padding: '14px 12px', borderRadius: 10, border: '1px dashed #cbd5e1', background: '#f8fafc', color: '#64748b', textAlign: 'center', fontSize: 13 }}>暂无共锁人</div>
- );
- }
- const name = getReleaseUserName(u);
- const avatarSrc = getReleaseUserAvatarSrc(u);
- // 共锁人状态: 0未共锁、1已共锁、2已解共锁
- const coLockStatus = String(u?.status ?? '').trim();
- const coLockText = coLockStatus === '1' ? '已共锁' : coLockStatus === '2' ? '已解共锁' : '未共锁';
- const coLockColor = coLockStatus === '1' ? '#15803d' : coLockStatus === '2' ? '#15803d' : '#dc2626';
- return (
- <div key={u.id ?? u.userId ?? idx} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, padding: '10px 10px', borderRadius: 10, border: '1px solid #e6e8f0', background: '#ffffff' }}>
- <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
- <Avatar size={32} src={avatarSrc} style={{ background: '#e5e7eb', color: '#111827', flexShrink: 0 }}>{String(name || '-').slice(0, 1)}</Avatar>
- <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{name}</div>
- </div>
- <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: coLockColor }}>
- {coLockStatus === '1' ? <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} /> : coLockStatus === '2' ? <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} /> : <CloseCircleOutlined style={{ fontSize: 14, color: '#dc2626' }} />}
- <span>{coLockText}</span>
- </span>
- </div>
- );
- })}
- </div>
- </div>
- </div>
- {/* 第二步:取钥匙(只显示待取出/已取出,接口若为2已归还则视为已取出) */}
- <div style={{ padding: '0 14px', borderRight: '1px solid #eef0f6', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
- <div style={{ fontSize: 14, fontWeight: 600, color: '#0f172a', marginBottom: 12, minHeight: 22 }}>第二步:取钥匙</div>
- <div style={{ padding: '8px 0', flex: 1 }}>
- <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>钥匙状态</div>
- <div style={{ width: '100%', maxWidth: 320, display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10 }}>
- {(releaseKeysList.length > 0 ? releaseKeysList : [{ __empty: true }]).map((k: any, idx: number) => {
- if (k.__empty) {
- return (
- <div key="empty-release-key" style={{ padding: '14px 12px', borderRadius: 10, border: '1px dashed #cbd5e1', background: '#f8fafc', color: '#64748b', textAlign: 'center', fontSize: 13 }}>暂无钥匙</div>
- );
- }
- const status = getReleaseKeyStatus(k);
- // 第二步只显示两种:0待取出 / 1或2 均显示已取出(已归还说明必然已取出过)
- const step2Taken = status !== 'to_take';
- const step2StatusText = step2Taken ? '已取出' : '待取出';
- return (
- <div key={k.id ?? k.keyId ?? idx} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 10px', borderRadius: 10, border: '1px solid #e6e8f0', background: '#ffffff' }}>
- <span style={{ width: 32, height: 32, borderRadius: 10, background: '#eff6ff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
- <KeyOutlined style={{ color: '#2563eb', fontSize: 16 }} />
- </span>
- <div style={{ minWidth: 0, flex: 1 }}>
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
- <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{k?.keyNfc ?? k?.keyName ?? k?.name ?? '-'}</div>
- <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 12, color: step2Taken ? '#15803d' : '#dc2626' }}>
- {step2Taken ? <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} /> : <CloseCircleOutlined style={{ fontSize: 14, color: '#dc2626' }} />}
- <span>{step2StatusText}</span>
- </span>
- </div>
- </div>
- </div>
- );
- })}
- </div>
- </div>
- </div>
- {/* 第三步:还钥匙 */}
- <div style={{ padding: '0 14px', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
- <div style={{ fontSize: 14, fontWeight: 600, color: '#0f172a', marginBottom: 12, minHeight: 22 }}>第三步:还钥匙</div>
- <div style={{ padding: '8px 0', flex: 1 }}>
- <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>解锁人</div>
- <div style={{ width: '100%', maxWidth: 320, display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10, marginBottom: 14 }}>
- {(releaseLockPersons.length > 0 ? releaseLockPersons : [{ __empty: true }]).map((u: any, idx: number) => {
- if (u.__empty) {
- return (
- <div key="empty-release-locker" style={{ padding: '12px 12px', borderRadius: 10, border: '1px dashed #cbd5e1', background: '#f8fafc', color: '#64748b', textAlign: 'center', fontSize: 13 }}>暂无解锁人</div>
- );
- }
- const name = getReleaseUserName(u);
- const avatarSrc = getReleaseUserAvatarSrc(u);
- // 上锁人状态: 0未上锁、1已上锁、2已解锁
- const lockStatus = String(u?.status ?? '').trim();
- const lockText = lockStatus === '1' ? '已上锁' : lockStatus === '2' ? '已解锁' : '未上锁';
- const lockColor = lockStatus === '1' ? '#15803d' : lockStatus === '2' ? '#15803d' : '#dc2626';
- return (
- <div key={u.id ?? u.userId ?? idx} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, padding: '10px 10px', borderRadius: 10, border: '1px solid #e6e8f0', background: '#ffffff' }}>
- <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
- <Avatar size={32} src={avatarSrc} style={{ background: '#e5e7eb', color: '#111827', flexShrink: 0 }}>{String(name || '-').slice(0, 1)}</Avatar>
- <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{name}</div>
- </div>
- <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: lockColor }}>
- {lockStatus === '1' ? <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} /> : lockStatus === '2' ? <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} /> : <CloseCircleOutlined style={{ fontSize: 14, color: '#dc2626' }} />}
- <span>{lockText}</span>
- </span>
- </div>
- );
- })}
- </div>
- <div style={{ fontSize: 13, color: '#64748b', marginBottom: 10 }}>钥匙状态</div>
- <div style={{ width: '100%', maxWidth: 320, display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10 }}>
- {(releaseKeysList.length > 0 ? releaseKeysList : [{ __empty: true }]).map((k: any, idx: number) => {
- if (k.__empty) {
- return (
- <div key="empty-release-key2" style={{ padding: '14px 12px', borderRadius: 10, border: '1px dashed #cbd5e1', background: '#f8fafc', color: '#64748b', textAlign: 'center', fontSize: 13 }}>暂无钥匙</div>
- );
- }
- const status = getReleaseKeyStatus(k);
- // 第三步显示三种状态:0待取出、1已取出、2已归还
- const step3StatusText = status === 'returned' ? '已归还' : status === 'taken' ? '已取出' : status === 'to_take' ? '待取出' : '未知';
- const step3Color = status === 'returned' ? '#15803d' : status === 'taken' ? '#f97316' : '#dc2626';
- const returnedAt = k?.giveBackTime ?? k?.returnTime ?? k?.returnedTime ?? k?.backTime ?? k?.keyReturnTime ?? k?.updateTime ?? k?.returnedAt ?? k?.returnAt;
- return (
- <div key={k.id ?? k.keyId ?? idx} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 10px', borderRadius: 10, border: '1px solid #e6e8f0', background: '#ffffff' }}>
- <span style={{ width: 32, height: 32, borderRadius: 10, background: '#eff6ff', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
- <KeyOutlined style={{ color: '#2563eb', fontSize: 16 }} />
- </span>
- <div style={{ minWidth: 0, flex: 1 }}>
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
- <div style={{ fontSize: 13, fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{k?.keyNfc ?? k?.keyName ?? k?.name ?? '-'}</div>
- <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 12, color: step3Color }}>
- {status === 'returned' ? <CheckCircleOutlined style={{ fontSize: 14, color: '#15803d' }} /> : status === 'taken' ? <CheckCircleOutlined style={{ fontSize: 14, color: '#f97316' }} /> : <CloseCircleOutlined style={{ fontSize: 14, color: '#dc2626' }} />}
- <span>{step3StatusText}</span>
- </span>
- </div>
- <div style={{ marginTop: 4, fontSize: 12, color: '#64748b' }}>归还时间:{returnedAt != null && String(returnedAt).trim() !== '' ? dateFormatter(returnedAt) : '-'}</div>
- </div>
- </div>
- );
- })}
- </div>
- </div>
- </div>
- </div>
- </Card>
- </div>
- </div>
- );
- }
- const isolationContent = (
- <div
- key="isolation-content"
- style={{
- flex: 1,
- minHeight: 0,
- overflow: 'hidden',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- padding: '24px',
- background: 'linear-gradient(165deg, #f0f7ff 0%, #fafbff 45%, #fff 100%)',
- }}
- >
- <Card
- style={{
- width: '100%',
- maxWidth: 500,
- textAlign: 'center',
- borderRadius: 16,
- boxShadow: '0 8px 32px rgba(22, 119, 255, 0.12), 0 2px 8px rgba(0,0,0,0.04)',
- border: '1px solid rgba(22, 119, 255, 0.15)',
- background: 'linear-gradient(180deg, #ffffff 0%, #fafcff 100%)',
- }}
- bodyStyle={{ padding: '48px 40px' }}
- >
- <div style={{ marginBottom: 28 }}>
- <span
- style={{
- width: 88,
- height: 88,
- borderRadius: 20,
- 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)',
- }}
- >
- <LockOutlined style={{ fontSize: 42, color: '#fff' }} />
- </span>
- </div>
- <div
- style={{
- fontSize: 18,
- fontWeight: 600,
- color: '#1f2937',
- lineHeight: 1.6,
- marginBottom: 8,
- }}
- >
- {t('form.lockCabinetTip')}
- </div>
- <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13, color: '#69b1ff' }}>
- <KeyOutlined />
- <span>完成取锁、取钥匙后,任务将自动推进</span>
- </div>
- </Card>
- </div>
- );
- return isolationContent;
- }
- // 审核类型节点 (review) - 表单直接放在外层,取消内置白框避免挤压
- if (isReview) {
- return (
- <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
- <div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '24px', background: 'transparent' }}>
- <div style={{ width: '100%' }}>
- {/* 自定义表单:仅当前节点有 formId 时渲染,避免完成/结束节点误展示上一节点表单 */}
- {formLoading ? (
- <div className="py-8 text-center text-gray-500">{t('form.formLoading')}</div>
- ) : hasFormIdForCurrentNode && formData.rule && formData.rule.length > 0 ? (
- <div className="space-y-4">
- {(() => {
- const formConfig = formData.option?.formConfig || defaultFormConfig;
- const layoutColumns = formConfig.layoutColumns || 1;
- const gridStyle = layoutColumns > 1 ? {
- display: 'grid',
- gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
- gap: '12px',
- rowGap: '16px',
- } : undefined;
- return (
- <AntdForm
- form={detailForm}
- layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
- size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
- requiredMark={formConfig.hideRequiredMark ? false : undefined}
- labelCol={
- formConfig.labelPosition === 'top' || !formConfig.labelWidth
- ? undefined
- : {
- flex: `${formConfig.labelWidth}px`,
- style: {
- minWidth: `${formConfig.labelWidth}px`,
- textAlign: formConfig.labelPosition === 'right' ? 'right' : 'left',
- },
- }
- }
- wrapperCol={formConfig.labelPosition === 'top' ? undefined : { flex: 'auto', style: { minWidth: 0 } }}
- >
- <div style={gridStyle} className={layoutColumns === 1 ? 'space-y-4' : 'form-detail-grid'}>
- <style>{`.form-detail-grid .ant-form-item { margin-bottom: 12px; } .form-detail-grid .ant-form-item-control { min-width: 0; }`}</style>
- {(formData.rule || []).map((field: any) => {
- const fieldWithDisabled = isApproved ? { ...field, disabled: true, readOnly: true } : field;
- return renderFieldPreview(fieldWithDisabled);
- })}
- </div>
- </AntdForm>
- );
- })()}
- </div>
- ) : (
- <div style={{ textAlign: 'center', padding: '32px 24px' }}>
- <span style={{ width: 72, height: 72, borderRadius: 18, background: '#e5e7eb', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', marginBottom: 20 }}>
- <CheckCircleOutlined style={{ fontSize: 36, color: '#6b7280' }} />
- </span>
- <div style={{ fontSize: 18, fontWeight: 600, color: '#1f2937', lineHeight: 1.6 }}>
- 无需操作
- </div>
- </div>
- )}
- {/* 审核意见 */}
- <div className="flex items-start" style={{ gap: 16, marginTop: 24, paddingTop: 24, borderTop: '1px solid #e2e8f0' }}>
- <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>
- <div className="flex-1">
- <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' }}
- />
- </div>
- </div>
- </div>
- </div>
- </div>
- );
- }
- // 其他节点类型(录入信息等,需要根据 formId 获取表单)- 样式与创建作业弹框一致:无浅蓝背景、无底部操作按钮
- return (
- <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
- <div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '24px', background: 'transparent' }}>
- <div style={{ width: '100%' }}>
- {/* 自定义表单:仅当前节点有 formId 时渲染,避免完成/结束节点误展示上一节点表单 */}
- {formLoading ? (
- <div className="py-8 text-center text-gray-500">{t('form.formLoading')}</div>
- ) : hasFormIdForCurrentNode && formData.rule && formData.rule.length > 0 ? (
- <div className="space-y-4">
- {(() => {
- const formConfig = formData.option?.formConfig || defaultFormConfig;
- const layoutColumns = formConfig.layoutColumns || 1;
- const gridStyle = layoutColumns > 1 ? {
- display: 'grid',
- gridTemplateColumns: `repeat(${layoutColumns}, minmax(0, 1fr))`,
- gap: '12px',
- rowGap: '16px',
- } : undefined;
-
- return (
- <AntdForm
- form={detailForm}
- layout={formConfig.labelPosition === 'top' ? 'vertical' : formConfig.labelPosition === 'left' ? 'horizontal' : 'horizontal'}
- size={formConfig.formSize === 'default' ? 'middle' : formConfig.formSize}
- requiredMark={formConfig.hideRequiredMark ? false : undefined}
- labelCol={
- formConfig.labelPosition === 'top' || !formConfig.labelWidth
- ? undefined
- : {
- flex: `${formConfig.labelWidth}px`,
- style: {
- minWidth: `${formConfig.labelWidth}px`,
- textAlign: formConfig.labelPosition === 'right' ? 'right' : 'left',
- },
- }
- }
- wrapperCol={formConfig.labelPosition === 'top' ? undefined : { flex: 'auto', style: { minWidth: 0 } }}
- >
- <div
- style={gridStyle}
- className={layoutColumns === 1 ? 'space-y-4' : 'form-detail-grid'}
- >
- <style>{`
- .form-detail-grid .ant-form-item { margin-bottom: 12px; }
- .form-detail-grid .ant-form-item-control { min-width: 0; }
- `}</style>
- {(formData.rule || []).map((field: any) => {
- // 如果已通过,禁用所有字段
- const fieldWithDisabled = isApproved ? { ...field, disabled: true, readOnly: true } : field;
- return renderFieldPreview(fieldWithDisabled);
- })}
- </div>
- </AntdForm>
- );
- })()}
- </div>
- ) : (
- <div
- style={{
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- padding: '24px 0',
- }}
- >
- <div style={{ width: '100%', textAlign: 'center' }}>
- <div style={{ marginBottom: 24 }}>
- <span
- style={{
- width: 88,
- height: 88,
- borderRadius: 20,
- background: '#e5e7eb',
- display: 'inline-flex',
- alignItems: 'center',
- justifyContent: 'center',
- }}
- >
- <CheckCircleOutlined style={{ fontSize: 42, color: '#6b7280' }} />
- </span>
- </div>
- <div style={{ fontSize: 18, fontWeight: 600, color: '#1f2937', lineHeight: 1.6 }}>
- 无需操作
- </div>
- </div>
- </div>
- )}
- </div>
- </div>
- </div>
- );
- })()}
- </div>
- <div
- style={{
- padding: '20px 24px 24px',
- borderTop: '1px solid #eef0f4',
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- background: '#f8fafc',
- }}
- >
- <Button
- size="large"
- type="default"
- style={{
- minWidth: 180,
- height: 44,
- borderRadius: 10,
- fontSize: 15,
- fontWeight: 500,
- borderColor: '#e2e8f0',
- color: '#334155',
- boxShadow: '0 1px 2px rgba(0,0,0,0.04)',
- background: '#ffffff',
- }}
- onClick={() => {
- console.log('TaskManagement: 弹框关闭(底部关闭按钮)');
- setDetailVisible(false);
- setDetailData(null);
- setFormData({ rule: [], option: {} });
- setFormLoading(false);
- setOriginalFields([]);
- setOriginalConf('');
- detailForm.resetFields();
- setApprovalComment('');
- setIsolationDeviceNumber('');
- setIsolationFileList([]);
- }}
- >
- 关 闭
- </Button>
- </div>
- </div>
- );
- }
- return <div className="py-8 text-center text-gray-500">{t('form.noData')}</div>;
- })()}
- </Modal>
- </div>
- );
- }
|