| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851 |
- <template>
- <div class="app-container">
- <el-card class="box-card">
- <!-- 培训申请子选项卡 -->
- <el-tabs v-model="trainingSubTab" @tab-click="handleTrainingSubTabClick">
- <el-tab-pane label="培训申请" name="application" v-hasPermi="['rs:trainingapplication:list']">
- <!-- 搜索区域 -->
- <transition :enter-active-class="animate.searchAnimate.enter" :leave-active-class="animate.searchAnimate.leave">
- <div v-show="showApplicationSearch" class="mb-[10px]">
- <el-card shadow="hover">
- <el-form ref="applicationQueryForm" :model="applicationQueryParams" label-width="100px">
- <el-row :gutter="20">
- <el-col :span="6">
- <el-form-item label="课程名称" prop="courseName">
- <el-input
- v-model="applicationQueryParams.courseName"
- placeholder="请输入课程名称"
- clearable
- @keyup.enter="handleApplicationQuery"
- />
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="培训地点" prop="location">
- <el-input
- v-model="applicationQueryParams.location"
- placeholder="请输入培训地点"
- clearable
- @keyup.enter="handleApplicationQuery"
- />
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="状态" prop="status">
- <el-select v-model="applicationQueryParams.status" placeholder="请选择状态" clearable style="width: 100%">
- <el-option v-for="dict in applicationStatusOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row>
- <el-col :span="24" class="search-btn-container">
- <el-form-item>
- <el-button type="primary" icon="Search" @click="handleApplicationQuery">搜索</el-button>
- <el-button icon="Refresh" @click="resetApplicationQuery">重置</el-button>
- </el-form-item>
- </el-col>
- </el-row>
- </el-form>
- </el-card>
- </div>
- </transition>
- <!-- 操作按钮区域 -->
- <el-card shadow="never" class="button-card table-card">
- <template #header>
- <el-row :gutter="10" class="mb8">
- <el-col :span="1.5">
- <el-button type="primary" plain icon="Plus" @click="handleAddApplication" v-hasPermi="['rs:trainingapplication:add']"
- >申请培训</el-button
- >
- </el-col>
- <el-col :span="1.5">
- <el-button
- icon="Download"
- @click="handleExportTrainingApplication"
- v-hasPermi="['rs:trainingapplication:export']"
- style="display: none"
- >导出</el-button
- >
- </el-col>
- <right-toolbar v-model:showSearch="showApplicationSearch" @queryTable="getApplicationList"></right-toolbar>
- </el-row>
- </template>
- <!-- 培训申请表格 -->
- <el-table v-loading="applicationLoading" border :data="applicationList" @row-dblclick="handleViewApplicant">
- <el-table-column label="主办单位" align="center" prop="organizerUnit" min-width="160" :show-overflow-tooltip="true" />
- <el-table-column label="培训地点" align="center" prop="location" min-width="120" :show-overflow-tooltip="true" />
- <el-table-column label="培训类型" align="center" prop="trainingType" min-width="120" :show-overflow-tooltip="true" />
- <el-table-column label="开始时间" align="center" prop="startTime" width="120">
- <template #default="scope">
- <span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') || '暂无' }}</span>
- </template>
- </el-table-column>
- <el-table-column label="结束时间" align="center" prop="endTime" width="120">
- <template #default="scope">
- <span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') || '暂无' }}</span>
- </template>
- </el-table-column>
- <el-table-column label="申请时间" align="center" prop="applyTime" width="160">
- <template #default="scope">
- <span>{{ parseTime(scope.row.applyTime) }}</span>
- </template>
- </el-table-column>
- <el-table-column label="申请状态" align="center" prop="status" width="100">
- <template #default="scope">
- <el-tag
- :type="scope.row.status === '0' ? 'warning' : scope.row.status === '1' ? 'success' : scope.row.status === '2' ? 'danger' : 'info'"
- >
- {{ getStatusLabel(scope.row.status) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220">
- <template #default="scope">
- <el-tooltip content="查看详情" placement="top">
- <el-button
- link
- type="primary"
- icon="View"
- @click="handleViewApplicant(scope.row)"
- v-hasPermi="['rs:trainingapplication:query']"
- ></el-button>
- </el-tooltip>
- <!-- 待审核状态:显示撤回按钮 -->
- <el-tooltip content="撤回申请" placement="top" v-if="scope.row.status === '0'">
- <el-button
- link
- type="warning"
- icon="RefreshLeft"
- @click="handleCancelApplication(scope.row)"
- v-hasPermi="['rs:trainingapplication:cancel']"
- ></el-button>
- </el-tooltip>
- <!-- 已拒绝状态:显示重新提交按钮(需要修改并重新提交权限) -->
- <el-tooltip content="重新提交" placement="top" v-if="scope.row.status === '2'">
- <el-button
- link
- type="success"
- icon="RefreshRight"
- @click="handleUpdateAndResubmitApplication(scope.row)"
- v-hasPermi="['rs:trainingapplication:edit', 'rs:trainingapplication:submit']"
- ></el-button>
- </el-tooltip>
- <!-- 已通过和已拒绝状态:显示审批历史 -->
- <el-tooltip content="审批历史" placement="top" v-if="scope.row.status === '1' || scope.row.status === '2'">
- <el-button link type="info" icon="InfoFilled" @click="handleViewHistory(scope.row)"></el-button>
- </el-tooltip>
- <!-- 删除按钮:仅在未通过的状态下显示(待审核、已拒绝、已取消) -->
- <el-tooltip content="删除" placement="top" v-if="scope.row.status !== '1'">
- <el-button
- link
- type="danger"
- icon="Delete"
- @click="handleDeleteApplication(scope.row)"
- v-hasPermi="['rs:trainingapplication:remove']"
- ></el-button>
- </el-tooltip>
- </template>
- </el-table-column>
- </el-table>
- <div class="pagination-container">
- <pagination
- v-show="applicationTotal > 0"
- :total="applicationTotal"
- v-model:page="applicationQueryParams.pageNum"
- v-model:limit="applicationQueryParams.pageSize"
- @pagination="getApplicationList"
- />
- </div>
- </el-card>
- </el-tab-pane>
- <el-tab-pane label="培训记录" name="record" v-hasPermi="['rs:trainingrecord:list']">
- <!-- 搜索区域 -->
- <transition :enter-active-class="animate.searchAnimate.enter" :leave-active-class="animate.searchAnimate.leave">
- <div v-show="showRecordSearch" class="mb-[10px]">
- <el-card shadow="hover">
- <el-form ref="recordQueryForm" :model="recordQueryParams" label-width="100px">
- <el-row :gutter="20">
- <el-col :span="6">
- <el-form-item label="课程名称" prop="courseName">
- <el-input v-model="recordQueryParams.courseName" placeholder="请输入课程名称" clearable @keyup.enter="handleRecordQuery" />
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="培训地点" prop="location">
- <el-input v-model="recordQueryParams.location" placeholder="请输入培训地点" clearable @keyup.enter="handleRecordQuery" />
- </el-form-item>
- </el-col>
- <el-col :span="6">
- <el-form-item label="部门" prop="department">
- <el-input v-model="recordQueryParams.department" placeholder="请输入部门" clearable style="width: 100%" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-row>
- <el-col :span="24" class="search-btn-container">
- <el-form-item>
- <el-button type="primary" icon="Search" @click="handleRecordQuery">搜索</el-button>
- <el-button icon="Refresh" @click="resetRecordQuery">重置</el-button>
- </el-form-item>
- </el-col>
- </el-row>
- </el-form>
- </el-card>
- </div>
- </transition>
- <!-- 培训记录管理内容 -->
- <el-card shadow="never" class="table-card">
- <template #header>
- <el-row :gutter="10" class="mb8">
- <el-col :span="1.5">
- <el-button icon="Download" @click="handleExportRecord" v-hasPermi="['rs:trainingrecord:export']" style="display: none"
- >导出</el-button
- >
- </el-col>
- <right-toolbar v-model:showSearch="showRecordSearch" @queryTable="getRecordList"></right-toolbar>
- </el-row>
- </template>
- <!-- 培训记录表格 -->
- <el-table
- v-loading="recordLoading"
- border
- :data="recordList"
- @selection-change="handleRecordSelectionChange"
- @row-dblclick="handleViewRecord"
- >
- <el-table-column type="selection" width="55" align="center" />
- <!-- 主办单位:从申请表course_name字段关联获取 -->
- <el-table-column label="主办单位" align="center" prop="organizerUnit" min-width="150" :show-overflow-tooltip="true">
- <template #default="scope">
- <span>{{ scope.row.organizerUnit || '暂无' }}</span>
- </template>
- </el-table-column>
- <!-- 培训类型:从申请表training_type字段关联获取 -->
- <el-table-column label="培训类型" align="center" prop="trainingType" min-width="110" :show-overflow-tooltip="true">
- <template #default="scope">
- <span>{{ scope.row.trainingType || '暂无' }}</span>
- </template>
- </el-table-column>
- <!-- 开始时间:从记录表start_time字段获取 -->
- <el-table-column label="开始时间" align="center" prop="startTime" width="120">
- <template #default="scope">
- <span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') || '暂无' }}</span>
- </template>
- </el-table-column>
- <!-- 结束时间:从记录表end_time字段获取 -->
- <el-table-column label="结束时间" align="center" prop="endTime" width="120">
- <template #default="scope">
- <span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') || '暂无' }}</span>
- </template>
- </el-table-column>
- <!-- 同行人员:从申请表companions字段关联获取,格式化为教师姓名 -->
- <el-table-column label="同行人员" align="center" prop="companions" min-width="120" :show-overflow-tooltip="true">
- <template #default="scope">
- <span v-if="scope.row.companions && scope.row.companions !== '[]'">
- {{ formatCompanions(scope.row.companions) }}
- </span>
- <span v-else>无</span>
- </template>
- </el-table-column>
- <el-table-column label="完成状态" align="center" prop="completionStatus" width="100">
- <template #default="scope">
- <el-tag :type="getCompletionStatusType(scope.row.completionStatus)">
- {{ getCompletionStatusLabel(scope.row.completionStatus) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" align="center" width="150">
- <template #default="scope">
- <el-tooltip content="查看" placement="top">
- <el-button
- link
- type="primary"
- icon="View"
- @click.stop="handleViewRecord(scope.row)"
- v-hasPermi="['rs:trainingrecord:query']"
- ></el-button>
- </el-tooltip>
- <el-tooltip content="删除" placement="top">
- <el-button
- link
- type="danger"
- icon="Delete"
- @click.stop="handleDeleteRecord(scope.row)"
- v-hasPermi="['rs:trainingrecord:remove']"
- ></el-button>
- </el-tooltip>
- </template>
- </el-table-column>
- </el-table>
- <div class="pagination-container">
- <pagination
- v-show="recordTotal > 0"
- :total="recordTotal"
- v-model:page="recordQueryParams.pageNum"
- v-model:limit="recordQueryParams.pageSize"
- @pagination="getRecordList"
- />
- </div>
- </el-card>
- </el-tab-pane>
- </el-tabs>
- </el-card>
- <!-- 添加或修改培训课程对话框 -->
- <el-dialog :title="courseDialogTitle" v-model="courseDialogVisible" width="700px" append-to-body>
- <el-form ref="courseFormRef" :model="courseForm" :rules="courseRules" label-width="100px">
- <el-form-item label="课程名称" prop="title">
- <el-input v-model="courseForm.title" placeholder="请输入课程名称" />
- </el-form-item>
- <el-form-item label="课程类别" prop="category">
- <el-select v-model="courseForm.category" placeholder="请选择课程类别" style="width: 100%">
- <el-option v-for="dict in rs_training_category" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="开始时间" prop="startTime">
- <el-date-picker
- v-model="courseForm.startTime"
- type="datetime"
- placeholder="选择开始时间"
- style="width: 100%"
- value-format="YYYY-MM-DD HH:mm:ss"
- />
- </el-form-item>
- <el-form-item label="结束时间" prop="endTime">
- <el-date-picker
- v-model="courseForm.endTime"
- type="datetime"
- placeholder="选择结束时间"
- style="width: 100%"
- value-format="YYYY-MM-DD HH:mm:ss"
- />
- </el-form-item>
- <el-form-item label="学时数" prop="duration">
- <el-input-number v-model="courseForm.duration" :min="1" :max="999" controls-position="right" style="width: 100%" />
- </el-form-item>
- <el-form-item label="培训地点" prop="location">
- <el-input v-model="courseForm.location" placeholder="请输入培训地点" />
- </el-form-item>
- <el-form-item label="课程描述" prop="description">
- <el-input v-model="courseForm.description" type="textarea" placeholder="请输入课程描述" :rows="4" />
- </el-form-item>
- <el-form-item label="状态" prop="status" v-if="courseForm.courseId">
- <el-radio-group v-model="courseForm.status">
- <el-radio label="0">正常</el-radio>
- <el-radio label="1">结束</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="备注" prop="remark">
- <el-input v-model="courseForm.remark" type="textarea" placeholder="请输入备注" />
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button :loading="courseDialogLoading" type="primary" @click="submitCourseForm">确 定</el-button>
- <el-button @click="cancelCourseDialog">取 消</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 查看培训课程详情对话框 -->
- <el-dialog title="培训课程详情" v-model="courseDetailDialogVisible" width="700px" append-to-body>
- <el-descriptions :column="2" border>
- <el-descriptions-item label="课程名称">{{ courseDetail.title }}</el-descriptions-item>
- <el-descriptions-item label="课程类别">{{ getCategoryLabel(courseDetail.category) }}</el-descriptions-item>
- <el-descriptions-item label="开始时间">{{ parseTime(courseDetail.startTime) }}</el-descriptions-item>
- <el-descriptions-item label="结束时间">{{ parseTime(courseDetail.endTime) }}</el-descriptions-item>
- <el-descriptions-item label="学时数">{{ courseDetail.duration }}</el-descriptions-item>
- <el-descriptions-item label="培训地点">{{ courseDetail.location || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="已报名人数">{{ courseDetail.currentParticipants || 0 }}</el-descriptions-item>
- <el-descriptions-item label="状态">
- <el-tag :type="courseDetail.status === '0' ? 'success' : 'info'">
- {{ courseDetail.status === '0' ? '正常' : '结束' }}
- </el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="创建时间">{{ parseTime(courseDetail.createTime) }}</el-descriptions-item>
- <el-descriptions-item label="课程描述" :span="2">
- <div class="detail-text">{{ courseDetail.description || '暂无描述' }}</div>
- </el-descriptions-item>
- <el-descriptions-item label="备注" :span="2">{{ courseDetail.remark || '暂无备注' }}</el-descriptions-item>
- </el-descriptions>
- <template #footer>
- <div class="dialog-footer">
- <el-button
- type="primary"
- @click="handleViewApplications(courseDetail)"
- v-if="courseDetail.courseId"
- v-hasPermi="['rs:trainingapplication:list']"
- >查看申请记录</el-button
- >
- <el-button @click="courseDetailDialogVisible = false">关 闭</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 申请详情对话框 -->
- <el-dialog title="申请详情" v-model="applicationDetailOpen" width="800px" append-to-body>
- <el-descriptions :column="2" border>
- <!-- 情况基本信息 -->
- <el-descriptions-item label="申请人">{{ applicationDetail.nickName || applicationDetail.userName }}</el-descriptions-item>
- <el-descriptions-item label="所在部门(系部)">{{ applicationDetail.department }}</el-descriptions-item>
- <el-descriptions-item label="现职称">{{ applicationDetail.currentTitle || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="现岗位">{{ applicationDetail.currentPosition || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="参加工作时间">{{ parseTime(applicationDetail.workStartTime) || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="联系电话">{{ applicationDetail.phone || '暂无' }}</el-descriptions-item>
- <!-- 培训情况信息 -->
- <el-descriptions-item label="主办单位">{{ applicationDetail.organizerUnit || applicationDetail.courseName || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="培训地点">{{ applicationDetail.location }}</el-descriptions-item>
- <el-descriptions-item label="开始时间">{{ parseTime(applicationDetail.startTime) }}</el-descriptions-item>
- <el-descriptions-item label="结束时间">{{ parseTime(applicationDetail.endTime) }}</el-descriptions-item>
- <el-descriptions-item label="培训类型">{{ applicationDetail.trainingType || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="培训费用">{{
- applicationDetail.trainingFee ? `${applicationDetail.trainingFee}元` : '暂无'
- }}</el-descriptions-item>
- <el-descriptions-item label="申请状态">
- <el-tag
- :type="
- applicationDetail.status === '0'
- ? 'warning'
- : applicationDetail.status === '1'
- ? 'success'
- : applicationDetail.status === '2'
- ? 'danger'
- : 'info'
- "
- >
- {{ getStatusLabel(applicationDetail.status) }}
- </el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="申请时间">{{ parseTime(applicationDetail.applyTime) }}</el-descriptions-item>
- <el-descriptions-item label="同行人" :span="2">{{ getCompanionNames(applicationDetail.companions) || '无' }}</el-descriptions-item>
- <el-descriptions-item label="培训目的" :span="2">{{
- applicationDetail.trainingPurpose || applicationDetail.reason || '暂无'
- }}</el-descriptions-item>
- <el-descriptions-item label="备注" :span="2">{{ applicationDetail.remark || '无' }}</el-descriptions-item>
- <!-- 显示拒绝原因,如果申请被拒绝 -->
- <el-descriptions-item v-if="applicationDetail.status === '2'" label="拒绝原因" :span="2" class="reject-reason">
- <div class="reject-info">
- <el-alert type="error" :closable="false">
- <template #title>
- <div class="reject-title">申请已被拒绝</div>
- </template>
- <div class="reject-content">{{ applicationDetail.adminRemark || '未提供拒绝原因' }}</div>
- </el-alert>
- <div class="reject-action">
- <el-button type="primary" size="small" @click="handleViewHistory(applicationDetail)">查看完整审批历史</el-button>
- </div>
- </div>
- </el-descriptions-item>
- </el-descriptions>
- <!-- 申请附件显示区域 -->
- <div v-if="applicationDetailAttachmentList.length > 0" class="attachment-section" style="margin-top: 20px">
- <div class="section-title" style="margin-bottom: 15px; padding: 10px; border-left: 4px solid #409eff; background-color: #f5f7fa">
- <el-icon style="margin-right: 8px"><Paperclip /></el-icon>
- <strong>申请附件</strong>
- </div>
- <el-table :data="applicationDetailAttachmentList" border size="small">
- <el-table-column label="序号" type="index" width="60" align="center" />
- <el-table-column label="文件名称" prop="name" align="left" show-overflow-tooltip />
- <el-table-column label="上传时间" prop="uploadTime" width="180" align="center">
- <template #default="scope">
- {{ parseTime(scope.row.uploadTime) }}
- </template>
- </el-table-column>
- <el-table-column label="操作" width="100" align="center">
- <template #default="scope">
- <el-button type="primary" link icon="Download" @click="handleDownloadApplicationAttachment(scope.row)" size="small"> 下载 </el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="applicationDetailOpen = false">关闭</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 审批历史对话框 -->
- <el-dialog title="审批历史记录" v-model="approvalHistoryOpen" width="700px" append-to-body>
- <el-timeline>
- <el-timeline-item
- v-for="(approval, index) in approvalHistoryList"
- :key="index"
- :timestamp="parseTime(approval.approvalTime)"
- :type="approval.status === '1' ? 'success' : approval.status === '2' ? 'danger' : 'primary'"
- >
- <div class="approval-history-item">
- <div class="approval-header">
- <span class="approver-name">{{ approval.approverName }}</span>
- <span class="approval-level">[{{ getApprovalLevelLabel(approval.approvalLevel) }}]</span>
- <el-tag :type="approval.status === '0' ? 'info' : approval.status === '1' ? 'success' : 'danger'" size="small">
- {{ approval.status === '0' ? '待审核' : approval.status === '1' ? '已通过' : '已拒绝' }}
- </el-tag>
- </div>
- <div class="approval-opinion">{{ approval.opinion || '无审批意见' }}</div>
- </div>
- </el-timeline-item>
- </el-timeline>
- <div v-if="approvalHistoryList.length === 0" class="no-data">暂无审批记录</div>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="approvalHistoryOpen = false">关闭</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 培训申请对话框 -->
- <el-dialog :title="applicationDialogTitle" v-model="applicationDialogVisible" width="800px" append-to-body>
- <el-form ref="applicationFormRef" :model="applicationForm" :rules="applicationRules" label-width="140px">
- <!-- 基本情况信息 -->
- <el-divider content-position="left">
- <span style="font-weight: bold; color: #409eff">基本情况</span>
- </el-divider>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="姓名" prop="userName">
- <el-input v-model="applicationForm.userName" placeholder="申请人姓名" readonly class="readonly-input" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="所在部门(系部)" prop="department">
- <el-input v-model="applicationForm.department" placeholder="请输入所属部门" readonly class="readonly-input" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="现职称" prop="currentTitle">
- <el-input v-model="applicationForm.currentTitle" placeholder="请输入现职称" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="现岗位" prop="currentPosition">
- <el-radio-group v-model="applicationForm.currentPosition" style="display: flex; flex-wrap: nowrap; align-items: center">
- <el-radio label="教学" style="margin-right: 20px">教学</el-radio>
- <el-radio label="管理" style="margin-right: 20px">管理</el-radio>
- <el-radio label="其他">其他</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="参加工作时间" prop="workStartTime">
- <el-date-picker
- v-model="applicationForm.workStartTime"
- type="date"
- placeholder="选择参加工作时间"
- style="width: 100%"
- value-format="YYYY-MM-DD"
- />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="联系电话" prop="phone">
- <el-input v-model="applicationForm.phone" placeholder="请输入联系电话" />
- </el-form-item>
- </el-col>
- </el-row>
- <!-- 培训情况信息 -->
- <el-divider content-position="left">
- <span style="font-weight: bold; color: #409eff">培训情况</span>
- </el-divider>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="主办单位" prop="organizerUnit">
- <el-input v-model="applicationForm.organizerUnit" placeholder="请输入主办单位" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="培训地点" prop="location">
- <el-input v-model="applicationForm.location" placeholder="请输入培训地点" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-form-item label="培训时间" required>
- <el-col :span="11">
- <el-form-item prop="startTime">
- <el-date-picker v-model="applicationForm.startTime" type="date" placeholder="开始日期" style="width: 100%" value-format="YYYY-MM-DD" />
- </el-form-item>
- </el-col>
- <el-col :span="2" class="text-center">
- <span class="text-gray-500">至</span>
- </el-col>
- <el-col :span="11">
- <el-form-item prop="endTime">
- <el-date-picker v-model="applicationForm.endTime" type="date" placeholder="结束日期" style="width: 100%" value-format="YYYY-MM-DD" />
- </el-form-item>
- </el-col>
- </el-form-item>
- <el-form-item label="培训类型" prop="trainingType">
- <div style="display: flex; flex-wrap: wrap; gap: 20px; align-items: center">
- <el-radio-group v-model="applicationForm.trainingType" style="display: flex; flex-wrap: nowrap; gap: 20px">
- <el-radio label="教学">教学</el-radio>
- <el-radio label="德育">德育</el-radio>
- <el-radio label="行政">行政</el-radio>
- <el-radio label="管理">管理</el-radio>
- <el-radio label="其他">其他</el-radio>
- </el-radio-group>
- <el-input
- v-if="applicationForm.trainingType === '其他'"
- v-model="applicationForm.trainingTypeOther"
- placeholder="请输入具体培训类型"
- style="width: 200px; margin-left: 10px"
- clearable
- />
- </div>
- </el-form-item>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="同行人" prop="companions">
- <TeacherMultiSelector v-model="selectedCompanions" placeholder="请选择同行人员" @change="handleCompanionsChange" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="培训费用" prop="trainingFee">
- <el-input-number v-model="applicationForm.trainingFee" :min="0" :precision="2" style="width: 100%" placeholder="请输入培训费用">
- <template #append>元</template>
- </el-input-number>
- </el-form-item>
- </el-col>
- </el-row>
- <el-form-item label="培训目的" prop="trainingPurpose">
- <el-input v-model="applicationForm.trainingPurpose" type="textarea" placeholder="请输入培训目的" :rows="3" />
- </el-form-item>
- <el-form-item label="备注" prop="remark">
- <el-input v-model="applicationForm.remark" type="textarea" placeholder="请输入备注" :rows="2" />
- </el-form-item>
- <!-- 上传附件 -->
- <el-form-item label="上传附件">
- <div class="attachment-upload-container">
- <el-upload
- v-model:file-list="applicationAttachmentFileList"
- class="attachment-upload-demo"
- drag
- list-type="picture-card"
- :action="applicationUploadAction"
- :headers="applicationUploadHeaders"
- :data="applicationUploadData"
- :auto-upload="true"
- :before-upload="beforeApplicationAttachmentUpload"
- :on-success="handleApplicationAttachmentSuccess"
- :on-error="handleApplicationAttachmentError"
- :on-remove="handleRemoveApplicationAttachment"
- :limit="10"
- :show-file-list="true"
- accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.xls,.xlsx"
- >
- <!-- 拖拽上传区域 -->
- <div class="el-upload-dragger-attachment">
- <el-icon class="el-icon--upload"><Upload /></el-icon>
- <div class="el-upload__text">将附件文件拖到此处,或<em>点击上传</em></div>
- </div>
- <!-- 自定义文件卡片 -->
- <template #file="{ file }">
- <div class="upload-file-card">
- <!-- 缩略图区域 -->
- <div class="upload-file-thumb">
- <image-preview
- v-if="isImageFile(file) && file.url && getFileOssId(file)"
- :src="getFileOssId(file)"
- class="upload-thumb-image"
- :isOssId="true"
- />
- <div
- v-else-if="isImageFile(file) && file.url"
- class="upload-file-image"
- @click="handlePreviewApplicationAttachment(file)"
- style="cursor: pointer"
- >
- <img :src="file.url" :alt="file.name" style="width: 100%; height: 100%; object-fit: cover" />
- </div>
- <div v-else class="upload-file-placeholder">
- <el-icon><Document /></el-icon>
- <span class="file-ext">{{ getFileExt(file.name) }}</span>
- </div>
- <!-- loading 遮罩 -->
- <div v-if="file.status === 'uploading'" class="upload-file-loading">
- <el-icon class="is-loading"><Loading /></el-icon>
- <span class="loading-text">上传中...</span>
- </div>
- </div>
- <!-- 文件名称 -->
- <div class="upload-file-name" :title="file.name">
- {{ getShortFileName(file.name) }}
- </div>
- <!-- 操作按钮区域 -->
- <div class="upload-file-card-actions" v-if="file.status && file.status !== 'ready'">
- <el-button-group size="small">
- <el-button
- type="success"
- @click.stop="handleDownloadApplicationAttachment(file)"
- title="下载"
- :disabled="file.status !== 'success'"
- >
- <el-icon><Download /></el-icon>
- </el-button>
- <el-button type="danger" @click.stop="confirmRemoveApplicationAttachment(file)" title="删除">
- <el-icon><Delete /></el-icon>
- </el-button>
- </el-button-group>
- </div>
- </div>
- </template>
- </el-upload>
- </div>
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button :loading="applicationDialogLoading" type="primary" @click="submitApplicationForm">确 定</el-button>
- <el-button @click="cancelApplicationDialog">取 消</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 培训记录对话框 -->
- <el-dialog :title="recordDialogTitle" v-model="recordDialogVisible" width="700px" append-to-body>
- <el-form ref="recordFormRef" :model="recordForm" :rules="recordRules" label-width="100px">
- <el-form-item label="课程名称">
- <el-input v-model="recordForm.courseName" :disabled="true" />
- </el-form-item>
- <el-form-item label="学时">
- <el-input v-model="recordForm.duration" :disabled="true" />
- </el-form-item>
- <el-form-item label="完成状态" prop="completionStatus">
- <el-radio-group v-model="recordForm.completionStatus">
- <el-radio label="1">已完成</el-radio>
- <el-radio label="2">未完成</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="完成时间" prop="completionTime">
- <el-date-picker
- v-model="recordForm.completionTime"
- type="datetime"
- placeholder="选择完成时间"
- style="width: 100%"
- value-format="YYYY-MM-DD HH:mm:ss"
- />
- </el-form-item>
- <el-form-item label="证书编号" prop="certificateNumber">
- <el-input v-model="recordForm.certificateNumber" placeholder="请输入证书编号(如有)" />
- </el-form-item>
- <el-form-item label="培训证书" prop="certificatePath">
- <div class="certificate-upload-container">
- <el-upload
- v-model:file-list="recordCertificateFileList"
- class="certificate-upload-demo"
- drag
- list-type="picture-card"
- :action="recordCertificateUploadAction"
- :headers="recordCertificateUploadHeaders"
- :data="recordCertificateUploadData"
- :auto-upload="true"
- :before-upload="beforeRecordCertificateUpload"
- :on-success="handleRecordCertificateSuccess"
- :on-error="handleRecordCertificateError"
- :on-remove="handleRemoveCertificate"
- :limit="15"
- :show-file-list="true"
- accept=".pdf,.jpg,.jpeg,.png"
- >
- <!-- 拖拽上传区域 -->
- <div class="el-upload-dragger-certificate">
- <el-icon class="el-icon--upload"><Upload /></el-icon>
- <div class="el-upload__text">将证书文件拖到此处,或<em>点击上传</em></div>
- <div class="el-upload__tip" style="margin-top: 4px; font-size: 11px; color: #909399">
- 支持PDF/JPG/PNG格式,文件大小不超过10MB,最多上传15个文件
- </div>
- </div>
- <!-- 自定义文件卡片 -->
- <template #file="{ file }">
- <div class="upload-file-card">
- <!-- 缩略图区域 -->
- <div class="upload-file-thumb">
- <image-preview
- v-if="isImageFile(file) && file.url && getFileOssId(file)"
- :src="getFileOssId(file)"
- class="upload-thumb-image"
- :isOssId="true"
- />
- <div
- v-else-if="isImageFile(file) && file.url"
- class="upload-file-image"
- @click="handlePreviewCertificate(file)"
- style="cursor: pointer"
- >
- <img :src="file.url" :alt="file.name" style="width: 100%; height: 100%; object-fit: cover" />
- </div>
- <div v-else class="upload-file-placeholder">
- <el-icon><Document /></el-icon>
- <span class="file-ext">{{ getFileExt(file.name) }}</span>
- </div>
- <!-- loading 遮罩 -->
- <div v-if="file.status === 'uploading'" class="upload-file-loading">
- <el-icon class="is-loading"><Loading /></el-icon>
- <span class="loading-text">上传中...</span>
- </div>
- </div>
- <!-- 文件名称 -->
- <div class="upload-file-name" :title="file.name">
- {{ getShortFileName(file.name) }}
- </div>
- <!-- 操作按钮区域 -->
- <div class="upload-file-card-actions" v-if="file.status && file.status !== 'ready'">
- <el-button-group size="small">
- <el-button type="success" @click.stop="handleDownloadCertificate(file)" title="下载" :disabled="file.status !== 'success'">
- <el-icon><Download /></el-icon>
- </el-button>
- <el-button type="danger" @click.stop="confirmRemoveCertificate(file)" title="删除">
- <el-icon><Delete /></el-icon>
- </el-button>
- </el-button-group>
- </div>
- </div>
- </template>
- </el-upload>
- </div>
- </el-form-item>
- <el-form-item label="培训反馈" prop="feedback">
- <el-input v-model="recordForm.feedback" type="textarea" :rows="3" placeholder="请输入培训反馈意见" />
- </el-form-item>
- <el-form-item label="备注" prop="remark">
- <el-input v-model="recordForm.remark" type="textarea" placeholder="请输入备注" />
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button :loading="recordDialogLoading" type="primary" @click="submitRecordForm">确 定</el-button>
- <el-button @click="cancelRecordDialog">取 消</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 培训记录详情对话框 -->
- <el-dialog title="培训记录详情" v-model="recordDetailDialogVisible" width="800px" append-to-body>
- <el-descriptions :column="2" border>
- <!-- 学员基本信息 -->
- <el-descriptions-item label="学员姓名">{{ recordDetail.userName }}</el-descriptions-item>
- <el-descriptions-item label="所在部门(系部)">{{ recordDetail.department || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="现职称">{{ recordDetail.currentTitle || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="现岗位">{{ recordDetail.currentPosition || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="参加工作时间">{{ parseTime(recordDetail.workStartTime) || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="联系电话">{{ recordDetail.phone || '暂无' }}</el-descriptions-item>
- <!-- 培训情况信息 -->
- <el-descriptions-item label="主办单位">{{ recordDetail.organizerUnit || recordDetail.courseName || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="培训地点">{{ recordDetail.location || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="开始时间">{{ parseTime(recordDetail.startTime) || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="结束时间">{{ parseTime(recordDetail.endTime) || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="培训类型">{{ recordDetail.trainingType || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="培训费用">{{ recordDetail.trainingFee ? `${recordDetail.trainingFee}元` : '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="申请时间">{{ parseTime(recordDetail.applyTime) || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="完成状态">
- <el-tag :type="getCompletionStatusType(recordDetail.completionStatus)">
- {{ getCompletionStatusLabel(recordDetail.completionStatus) }}
- </el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="同行人" :span="2">{{ getCompanionNames(recordDetail.companions) || '无' }}</el-descriptions-item>
- <el-descriptions-item label="培训目的" :span="2">{{ recordDetail.trainingPurpose || recordDetail.reason || '暂无' }}</el-descriptions-item>
- <el-descriptions-item label="备注" :span="2">{{ recordDetail.remark || '暂无备注' }}</el-descriptions-item>
- </el-descriptions>
- <!-- 申请附件显示区域 -->
- <div v-if="recordDetailAttachmentList.length > 0" class="attachment-section" style="margin-top: 20px">
- <div class="section-title" style="margin-bottom: 15px; padding: 10px; border-left: 4px solid #409eff; background-color: #f5f7fa">
- <el-icon style="margin-right: 8px"><Paperclip /></el-icon>
- <strong>申请附件</strong>
- </div>
- <el-table :data="recordDetailAttachmentList" border size="small">
- <el-table-column label="序号" type="index" width="60" align="center" />
- <el-table-column label="文件名称" prop="name" align="left" show-overflow-tooltip />
- <el-table-column label="上传时间" prop="uploadTime" width="180" align="center">
- <template #default="scope">
- {{ parseTime(scope.row.uploadTime) }}
- </template>
- </el-table-column>
- <el-table-column label="操作" width="100" align="center">
- <template #default="scope">
- <el-button type="primary" link icon="Download" @click="handleDownloadApplicationAttachment(scope.row)" size="small"> 下载 </el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="recordDetailDialogVisible = false">关 闭</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 上传证书对话框 -->
- <el-dialog title="上传培训证书" v-model="certificateDialogVisible" width="500px" append-to-body>
- <el-upload
- ref="uploadRef"
- :action="`/dev-api/rs/training/record/certificate/upload/${certificateUploadData.recordId}`"
- :headers="uploadHeaders"
- :before-upload="beforeCertificateUpload"
- :on-success="handleCertificateSuccess"
- :on-error="handleCertificateError"
- :file-list="certificateFileList"
- :limit="1"
- :auto-upload="true"
- accept=".pdf,.jpg,.jpeg,.png"
- class="certificate-upload"
- >
- <el-button type="primary">选择文件</el-button>
- <template #tip>
- <div class="el-upload__tip">只能上传PDF/JPG/PNG文件,且不超过10MB</div>
- </template>
- </el-upload>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="certificateDialogVisible = false">取 消</el-button>
- </div>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts" name="TrainingManagement">
- import { ref, reactive, onMounted, onActivated, getCurrentInstance, ComponentInternalInstance, watch } from 'vue';
- import {
- Plus,
- Edit,
- Delete,
- Search,
- Download,
- Upload,
- View,
- Document,
- RefreshRight,
- RefreshLeft,
- InfoFilled,
- Loading,
- Picture,
- Paperclip
- } from '@element-plus/icons-vue';
- import ImagePreview from '@/components/ImagePreview/index.vue';
- import {
- // 培训课程相关接口已废弃 (2025-01-03)
- // listTrainingCourse, getTrainingCourse, addTrainingCourse, updateTrainingCourse, delTrainingCourse, toggleCourseStatus, exportTrainingCourse,
- listTrainingApplication,
- getTrainingApplication,
- addTrainingApplication,
- updateTrainingApplication,
- cancelTrainingApplication,
- exportTrainingApplication,
- listMyTrainingApplication,
- listTrainingRecord,
- getTrainingRecord,
- addTrainingRecord,
- updateTrainingRecord,
- delTrainingRecord,
- exportTrainingRecord,
- submitTrainingApplication,
- updateApplicationStatus,
- deleteTrainingApplication,
- getTrainingApprovalHistory,
- resubmitTrainingApplication,
- batchQueryCompanions,
- updateRecordAttachments,
- listAllApprovalRecords
- } from '@/api/rs/training';
- import { parseTime } from '@/utils/ruoyi';
- import request from '@/utils/request';
- import { ElLoading } from 'element-plus';
- import type {
- CourseQuery,
- CourseVO,
- CourseForm,
- RecordQuery,
- RecordVO,
- RecordForm,
- ApplicationQuery,
- ApplicationVO,
- ApplicationForm
- } from '@/api/rs/training';
- import { listDept, getDept } from '@/api/system/dept';
- import type { DeptVO } from '@/api/system/dept/types';
- import { getFilePreviewUrl } from '@/api/system/file';
- import { getToken } from '@/utils/auth';
- import { getInfo as getUserInfo } from '@/api/login';
- import { useUserStore } from '@/store/modules/user';
- // 🔧 修复雪花ID精度丢失问题:使用培训申请模块的本地组件
- // 原因:公共组件不能修改,会影响其他模块。在本地复制并修复雪花ID转Number导致的精度丢失问题
- import TeacherMultiSelector from './components/TeacherMultiSelector.vue';
- import { TeacherVO } from '@/api/system/teacher/types';
- import { listTeacher } from '@/api/system/teacher';
- const { proxy } = getCurrentInstance() as ComponentInternalInstance;
- const userStore = useUserStore();
- // 环境变量
- const baseApi = import.meta.env.VITE_APP_BASE_API;
- const clientId = import.meta.env.VITE_APP_CLIENT_ID;
- const animate = proxy?.animate || { searchAnimate: { enter: 'animate__fadeInUp', leave: 'animate__fadeOutDown' } };
- // ===== 申请培训附件上传配置(统一使用computed方式) =====
- const applicationUploadAction = computed(() => {
- const api = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload';
- console.log('[申请培训附件上传配置] 上传地址:', api);
- return api;
- });
- const applicationUploadHeaders = computed(() => {
- const token = getToken() || '';
- const headers = {
- 'Authorization': `Bearer ${token}`,
- 'clientid': import.meta.env.VITE_APP_CLIENT_ID || 'e5cd7e4891bf95d1d19206ce24a7b32e'
- };
- console.log('[申请培训附件上传配置] 请求头:', headers);
- return headers;
- });
- const applicationUploadData = computed(() => {
- return {
- bizType: 'training_application_attachment'
- };
- });
- // ===== 培训证书上传配置(统一使用computed方式) =====
- const recordCertificateUploadAction = computed(() => {
- const api = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload';
- console.log('[培训证书上传配置] 上传地址:', api);
- return api;
- });
- const recordCertificateUploadHeaders = computed(() => {
- const token = getToken() || '';
- const headers = {
- 'Authorization': `Bearer ${token}`,
- 'clientid': import.meta.env.VITE_APP_CLIENT_ID || 'e5cd7e4891bf95d1d19206ce24a7b32e'
- };
- console.log('[培训证书上传配置] 请求头:', headers);
- return headers;
- });
- const recordCertificateUploadData = computed(() => {
- return {
- bizType: 'training_certificate_attachment'
- };
- });
- // 培训申请相关
- // 培训课程遮罩层
- const trainingLoading = ref(false);
- // 培训选中数组
- const trainingIds = ref<Array<string | number>>([]);
- // 培训非单个禁用
- const trainingSingle = ref(true);
- // 培训非多个禁用
- const trainingMultiple = ref(true);
- // 显示培训搜索条件
- const showTrainingSearch = ref(true);
- // 显示申请搜索条件
- const showApplicationSearch = ref(true);
- // 培训总条数
- const trainingTotal = ref(0);
- // 培训课程表格数据
- const courseList = ref<CourseVO[]>([]);
- // 当前激活的子选项卡
- const trainingSubTab = ref('application'); // 默认显示培训申请选项卡
- // 添加缓存状态标记
- const isInitialized = ref(false);
- const isDataLoaded = ref(false);
- // 课程类别选项改为字典
- const { rs_training_category, rs_training_application_status } = toRefs<any>(
- proxy?.useDict('rs_training_category', 'rs_training_application_status') || {}
- );
- // 课程状态选项(计算型)
- const getCourseStatusType = (row: CourseVO): string => {
- const now = new Date().getTime();
- const startTime = new Date(row.startTime).getTime();
- const endTime = new Date(row.endTime).getTime();
- if (row.status !== '0') {
- return 'info'; // 停用状态
- } else if (now < startTime) {
- return 'warning'; // 待开课
- } else if (now >= startTime && now <= endTime) {
- return 'success'; // 进行中
- } else {
- return 'info'; // 已结束,返回默认颜色
- }
- };
- const getCourseStatusLabel = (row: CourseVO): string => {
- const now = new Date().getTime();
- const startTime = new Date(row.startTime).getTime();
- const endTime = new Date(row.endTime).getTime();
- if (row.status !== '0') {
- return '已结束';
- } else if (now < startTime) {
- return '待开课';
- } else if (now >= startTime && now <= endTime) {
- return '进行中';
- } else {
- return '已结束';
- }
- };
- // 培训课程查询参数
- const trainingQueryParams = reactive<CourseQuery>({
- pageNum: 1,
- pageSize: 10,
- title: '',
- category: '',
- status: '',
- params: {}
- });
- // 培训课程对话框相关
- const courseDialogVisible = ref(false);
- const courseDialogTitle = ref('');
- const courseDialogLoading = ref(false);
- const courseForm = reactive<CourseForm>({
- courseId: undefined,
- title: '',
- category: '',
- description: '',
- startTime: '',
- endTime: '',
- duration: 0,
- location: '',
- status: '0',
- remark: ''
- });
- // 培训课程表单校验规则
- const courseRules = reactive({
- title: [
- { required: true, message: '课程名称不能为空', trigger: 'blur' },
- { min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
- ],
- category: [{ required: true, message: '课程类别不能为空', trigger: 'change' }],
- startTime: [{ required: true, message: '开始时间不能为空', trigger: 'change' }],
- endTime: [{ required: true, message: '结束时间不能为空', trigger: 'change' }],
- duration: [{ required: true, message: '学时数不能为空', trigger: 'blur' }]
- } as any);
- // 培训课程表单引用
- const courseFormRef = ref<any>(null);
- // 培训课程详情
- const courseDetailDialogVisible = ref(false);
- const courseDetail = reactive<CourseVO>({
- courseId: undefined,
- title: '',
- category: '',
- description: '',
- startTime: '',
- endTime: '',
- duration: 0,
- location: '',
- maxParticipants: 0,
- currentParticipants: 0,
- status: '0',
- applicationCount: 0,
- createTime: '',
- updateTime: '',
- createBy: '',
- updateBy: '',
- remark: ''
- });
- // 培训申请相关
- const applicationLoading = ref(false);
- const applicationList = ref<any[]>([]);
- const applicationTotal = ref(0);
- const applicationQueryForm = ref<any>(null);
- const applicationQueryParams = reactive({
- pageNum: 1,
- pageSize: 10,
- courseId: undefined as string | number | undefined,
- courseName: '',
- location: '',
- duration: undefined as number | undefined,
- status: '',
- excludeStatus: '1', // 默认排除已通过状态的数据
- params: {}
- });
- // 申请状态选项改为字典
- const applicationStatusOptions = rs_training_application_status;
- // 教师列表数据
- const teacherList = ref<
- Array<{
- teacherId: string | number;
- teacherName: string;
- teacherType: string;
- }>
- >([]);
- // 申请详情相关
- const applicationDetailOpen = ref(false);
- const applicationDetail = reactive<any>({
- applicationId: 0,
- courseId: 0,
- courseName: '',
- userId: 0,
- userName: '',
- department: '',
- status: '0',
- applyTime: '',
- approvalTime: '',
- remark: '',
- adminRemark: '',
- certificatePath: '',
- attachmentIds: '' // 添加附件ID字段
- });
- // 审批历史相关
- const approvalHistoryOpen = ref(false);
- const approvalHistoryList = ref<any[]>([]);
- // 培训记录相关
- const recordLoading = ref(false);
- const recordList = ref<RecordVO[]>([]);
- const recordTotal = ref(0);
- const showRecordSearch = ref(true);
- const recordQueryParams = reactive<RecordQuery>({
- pageNum: 1,
- pageSize: 20,
- courseId: undefined as string | number | undefined,
- courseName: '',
- userId: undefined as string | number | undefined,
- userName: '',
- location: '',
- department: '',
- completionStatus: '',
- includeApprovedApplications: true, // 包含已通过审批的申请数据
- params: {}
- });
- // 培训记录对话框相关
- const recordDialogVisible = ref(false);
- const recordDialogTitle = ref('');
- const recordDialogLoading = ref(false);
- const recordFormRef = ref<any>(null);
- const recordQueryForm = ref<any>(null);
- const recordForm = reactive<RecordForm>({
- recordId: undefined,
- courseId: undefined,
- userId: undefined,
- completionStatus: '0',
- score: '',
- completionTime: '',
- attachmentIds: '',
- remark: ''
- });
- // 培训记录表单校验规则
- const recordRules = reactive({
- completionStatus: [{ required: true, message: '请选择完成状态', trigger: 'change' }]
- });
- // 培训记录详情
- const recordDetailDialogVisible = ref(false);
- const recordDetail = reactive<any>({
- recordId: undefined,
- courseId: undefined,
- courseName: '',
- userId: undefined,
- userName: '',
- completionStatus: '',
- score: '',
- completionTime: '',
- certificatePath: '',
- createTime: '',
- updateTime: '',
- createBy: '',
- updateBy: '',
- remark: '',
- // 从培训申请获取的详细信息
- department: '',
- currentTitle: '',
- currentPosition: '',
- workStartTime: '',
- phone: '',
- organizerUnit: '',
- trainingType: '',
- trainingFee: '',
- companions: '',
- trainingPurpose: '',
- location: '',
- startTime: '',
- endTime: '',
- applyTime: ''
- });
- // 培训记录多选框选中数据
- const recordIds = ref<Array<string | number>>([]);
- // 培训记录非单个禁用
- const recordSingle = ref(true);
- // 培训记录非多个禁用
- const recordMultiple = ref(true);
- // 课程选项
- const courseOptions = ref<CourseVO[]>([]);
- // 用户选项
- const userOptions = ref<any[]>([]);
- // 证书上传相关
- const certificateDialogVisible = ref(false);
- const uploadRef = ref();
- const certificateFileList = ref<any[]>([]);
- const certificateUploadData = reactive({
- recordId: '',
- isApplication: false
- });
- // 培训记录修改对话框中的证书上传
- const recordCertificateUploadRef = ref();
- const recordCertificateFileList = ref<any[]>([]);
- const uploadHeaders = {
- Authorization: `Bearer ${getToken()}`
- };
- // 培训申请对话框相关
- const applicationDialogVisible = ref(false);
- const applicationDialogTitle = ref('');
- const isResubmitMode = ref(false); // 标记是否为重新提交模式
- const applicationDialogLoading = ref(false);
- const applicationFormRef = ref<any>(null);
- const applicationAttachmentFileList = ref<any[]>([]); // 申请培训附件文件列表
- const applicationDetailAttachmentList = ref<any[]>([]); // 申请详情附件文件列表
- const recordDetailAttachmentList = ref<any[]>([]); // 培训记录详情附件文件列表
- const applicationForm = reactive<{
- applicationId?: number | string;
- // 情况基本信息
- userName: string; // 姓名
- department: string; // 所在部门(系部)
- currentTitle: string; // 现职称
- currentPosition: string; // 现岗位
- workStartTime: string; // 参加工作时间
- phone: string; // 联系电话
- // 培训情况信息
- organizerUnit: string; // 主办单位
- location: string; // 培训地点
- startTime: string; // 开始时间
- endTime: string; // 结束时间
- trainingType: string; // 培训类型
- trainingTypeOther: string; // 其他培训类型描述
- trainingFee: number; // 培训费用
- companions: (string | number)[]; // 同行人
- trainingPurpose: string; // 培训目的
- remark: string; // 备注
- // 兼容原有字段
- courseName: string; // 培训课程(用于后端兼容)
- duration: number; // 学时(用于后端兼容)
- reason: string; // 申请理由(用于后端兼容)
- attachmentIds: string[]; // 附件ID列表
- }>({
- applicationId: undefined,
- // 情况基本信息
- userName: '',
- department: '',
- currentTitle: '',
- currentPosition: '',
- workStartTime: '',
- phone: '',
- // 培训情况信息
- organizerUnit: '',
- location: '',
- startTime: '',
- endTime: '',
- trainingType: '',
- trainingTypeOther: '',
- trainingFee: 0,
- companions: [],
- trainingPurpose: '',
- remark: '',
- // 兼容原有字段
- courseName: '',
- duration: 0,
- reason: '',
- attachmentIds: [] as string[] // 附件ID列表
- });
- // 同行人选择器相关
- const selectedCompanions = ref<TeacherVO[]>([]);
- // 同行人姓名缓存(用于详情展示)
- const companionNamesCache = ref<Record<string, string>>({});
- // 培训申请表单校验规则
- const applicationRules = reactive<any>({
- // 情况基本信息验证
- userName: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
- department: [{ required: true, message: '所属部门不能为空', trigger: 'blur' }],
- currentTitle: [{ required: true, message: '请输入现职称', trigger: 'blur' }],
- currentPosition: [{ required: true, message: '请选择现岗位', trigger: 'change' }],
- workStartTime: [{ required: true, message: '请选择参加工作时间', trigger: 'change' }],
- phone: [
- { required: true, message: '请输入联系电话', trigger: 'blur' },
- { pattern: /^1[3-9]\d{9}$/ as RegExp, message: '请输入正确的手机号码', trigger: 'blur' }
- ],
- // 培训情况信息验证
- organizerUnit: [{ required: true, message: '请输入主办单位', trigger: 'blur' }],
- location: [{ required: true, message: '请输入培训地点', trigger: 'blur' }],
- startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
- endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
- trainingType: [{ required: true, message: '请选择培训类型', trigger: 'change' }],
- trainingTypeOther: [
- {
- validator: (rule: any, value: string, callback: Function) => {
- if (applicationForm.trainingType === '其他' && (!value || value.trim() === '')) {
- callback(new Error('请输入具体的培训类型'));
- } else {
- callback();
- }
- },
- trigger: 'blur'
- }
- ],
- trainingFee: [{ required: true, message: '请输入培训费用', trigger: 'blur' }],
- trainingPurpose: [{ required: true, message: '请输入培训目的', trigger: 'blur' }]
- });
- // 监听培训类型变化,清空其他类型输入
- watch(
- () => applicationForm.trainingType,
- (newType) => {
- if (newType !== '其他') {
- applicationForm.trainingTypeOther = '';
- }
- }
- );
- // 部门选项 - 从数据库sys_dept表获取
- const departmentOptions = ref<Array<{ value: string; label: string }>>([]);
- /** 查询培训课程列表 - 已废弃 (2025-01-03) */
- function getTrainingList() {
- trainingLoading.value = true;
- // 培训课程模块已废弃,课程信息现在通过培训申请管理
- console.warn('培训课程模块已废弃,请使用培训申请功能');
- courseList.value = [];
- trainingTotal.value = 0;
- trainingLoading.value = false;
- }
- /** 获取部门列表 */
- function getDepartmentList() {
- listDept({
- pageNum: 1,
- pageSize: 1000, // 获取所有部门
- status: 0
- })
- .then((response) => {
- const deptList = response.data || [];
- // 过滤出二级学院和行政单位下的部门,作为培训部门选项
- const filteredDepts = deptList.filter((dept: DeptVO) => {
- // 包含二级学院(101)和行政单位(102)下的部门,排除顶级部门
- return dept.parentId === 101 || dept.parentId === 102;
- });
- departmentOptions.value = filteredDepts.map((dept: DeptVO) => ({
- value: dept.deptName,
- label: dept.deptName
- }));
- console.log('培训申请部门选项已更新:', departmentOptions.value);
- })
- .catch((error) => {
- console.error('获取部门列表失败:', error);
- // 使用备用部门数据
- departmentOptions.value = [
- { value: '人文学院', label: '人文学院' },
- { value: '智能控制学院', label: '智能控制学院' },
- { value: '智能制造学院', label: '智能制造学院' },
- { value: '医药健康学院', label: '医药健康学院' },
- { value: '餐饮休闲学院', label: '餐饮休闲学院' },
- { value: '数字信息学院', label: '数字信息学院' },
- { value: '生态园林学院', label: '生态园林学院' },
- { value: '人事处', label: '人事处' },
- { value: '教务处', label: '教务处' },
- { value: '学生处', label: '学生处' },
- { value: '总务处', label: '总务处' }
- ];
- });
- }
- /** 获取课程类别标签 */
- function getCategoryLabel(value: string): string {
- const option = categoryOptions.value.find((item) => item.value === value);
- return option ? option.label : value;
- }
- /** 表单重置 */
- function resetCourseForm() {
- courseForm.courseId = undefined;
- courseForm.title = '';
- courseForm.category = '';
- courseForm.description = '';
- courseForm.startTime = '';
- courseForm.endTime = '';
- courseForm.duration = 0;
- courseForm.location = '';
- courseForm.status = '0';
- courseForm.remark = '';
- courseFormRef.value?.resetFields();
- }
- /** 取消按钮 */
- function cancelCourseDialog() {
- courseDialogVisible.value = false;
- resetCourseForm();
- }
- /** 新增培训课程 */
- function handleAddCourse() {
- resetCourseForm();
- courseDialogVisible.value = true;
- courseDialogTitle.value = '添加培训课程';
- }
- /** 修改培训课程 */
- function handleUpdateCourse(row?: CourseVO) {
- resetCourseForm();
- const courseId = row?.courseId || trainingIds.value[0];
- if (!courseId) {
- return;
- }
- // 培训课程模块已废弃 (2025-01-03)
- console.warn('培训课程编辑功能已废弃,请使用培训申请功能');
- proxy?.$modal.msgWarning('培训课程模块已废弃,请使用培训申请功能');
- }
- /** 提交按钮 */
- function submitCourseForm() {
- courseFormRef.value?.validate((valid: boolean) => {
- if (valid) {
- courseDialogLoading.value = true;
- // 培训课程模块已废弃 (2025-01-03)
- console.warn('培训课程新增/编辑功能已废弃,请使用培训申请功能');
- proxy?.$modal.msgWarning('培训课程模块已废弃,请使用培训申请功能');
- courseDialogLoading.value = false;
- }
- });
- }
- /** 删除培训课程 */
- function handleDeleteCourse(row?: CourseVO) {
- const courseIds = row?.courseId || trainingIds.value;
- if (!courseIds || (Array.isArray(courseIds) && courseIds.length === 0)) {
- return;
- }
- let confirmMessage = '';
- if (row) {
- confirmMessage = `是否确认删除课程"${row.title}"?`;
- } else {
- const selectedItems = courseList.value.filter((item) => trainingIds.value.includes(item.courseId));
- if (selectedItems.length === 1) {
- confirmMessage = `是否确认删除课程"${selectedItems[0].title}"?`;
- } else {
- const names = selectedItems.map((item) => item.title).join('、');
- confirmMessage = `是否确认删除课程"${names}"?`;
- }
- }
- proxy?.$modal.confirm(confirmMessage).then(() => {
- // 培训课程模块已废弃 (2025-01-03)
- console.warn('培训课程删除功能已废弃,请使用培训申请功能');
- proxy?.$modal.msgWarning('培训课程模块已废弃,请使用培训申请功能');
- });
- }
- /** 导出培训课程 */
- function handleExportCourse() {
- proxy?.download(
- 'rs/training/course/export',
- {
- ...trainingQueryParams
- },
- `training_course_${new Date().getTime()}.xlsx`
- );
- }
- /** 查看培训课程详情 - 已废弃 (2025-01-03) */
- function handleViewCourse(row: CourseVO) {
- console.warn('培训课程查看功能已废弃,请使用培训申请功能');
- proxy?.$modal.msgWarning('培训课程模块已废弃,请使用培训申请功能');
- }
- /** 切换课程状态 - 已废弃 (2025-01-03) */
- function handleToggleStatus(row: CourseVO) {
- console.warn('培训课程状态切换功能已废弃,请使用培训申请功能');
- proxy?.$modal.msgWarning('培训课程模块已废弃,请使用培训申请功能');
- }
- /** 查看培训申请记录 */
- function handleViewApplications(row: CourseVO) {
- applicationQueryParams.courseId = row.courseId;
- applicationQueryParams.pageNum = 1;
- applicationTotal.value = 0; // Reset total for new search
- getApplicationList();
- proxy?.$modal.msgSuccess(`查看${row.title}的申请记录`);
- // 此处可以跳转到申请记录页面或打开申请记录弹窗
- }
- /** 培训申请管理相关 */
- function getApplicationList() {
- applicationLoading.value = true;
- // 构建查询参数,自动过滤掉已通过的数据
- const queryParams = {
- ...applicationQueryParams,
- // 排除已通过状态的数据,只显示待审核、已拒绝、已取消等状态
- excludeStatus: '1' // 1表示已通过状态
- };
- // 调用API获取申请列表
- listTrainingApplication(queryParams)
- .then((response) => {
- // 获取申请列表数据
- const applications = response.rows || [];
- // 前端二次过滤,确保不显示已通过的数据
- const filteredApplications = applications.filter((app) => app.status !== '1');
- applicationList.value = filteredApplications;
- applicationTotal.value = filteredApplications.length;
- applicationLoading.value = false;
- })
- .catch(() => {
- applicationLoading.value = false;
- proxy?.$modal.msgError('获取培训申请列表失败');
- });
- }
- /** 获取完成状态的标签类型 */
- const getCompletionStatusType = (status: string) => {
- // 培训记录的完成状态逻辑
- if (status === '1') return 'success'; // 已完成 - 绿色
- if (status === '2') return 'danger'; // 未完成 - 红色
- // 对于空状态或其他状态,由于培训记录都是基于已通过审批的申请生成的,默认显示为已通过
- return 'success'; // 已通过 - 绿色
- };
- /** 获取完成状态的标签文本 */
- const getCompletionStatusLabel = (status: string) => {
- // 培训记录的完成状态逻辑
- if (status === '1') return '已完成';
- if (status === '2') return '未完成';
- // 对于空状态或其他状态,由于培训记录都是基于已通过审批的申请生成的,默认显示为已通过
- return '已通过';
- };
- /** 获取教师列表 */
- function getTeacherList() {
- // 使用 system/teacher 接口获取教师列表,设置大的pageSize以获取所有教师
- listTeacher({
- pageNum: 1,
- pageSize: 9999 // 设置足够大的pageSize以获取所有教师
- })
- .then((response: any) => {
- const teachers = response.rows || [];
- teacherList.value = teachers.map((item: TeacherVO) => ({
- teacherId: item.userId, // 使用 userId 作为 teacherId
- teacherName: item.nickName || item.userName, // 使用 nickName 或 userName
- teacherType: '' // system/teacher 接口没有 teacherType 字段
- }));
- console.log('初始化教师列表加载完成:', teacherList.value.length, '位教师');
- })
- .catch(() => {
- console.error('获取教师列表失败');
- });
- }
- /** 异步加载教师列表用于详情显示(容错版,权限不足时不报错)*/
- async function loadTeacherListForDisplay() {
- try {
- console.log('开始加载教师列表...');
- // 使用 system/teacher 接口获取教师列表,设置大的pageSize以获取所有教师
- const response: any = await listTeacher({
- pageNum: 1,
- pageSize: 9999 // 设置足够大的pageSize以获取所有教师
- });
- const teachers = response.rows || [];
- teacherList.value = teachers.map((item: TeacherVO) => ({
- teacherId: item.userId, // 使用 userId 作为 teacherId
- teacherName: item.nickName || item.userName, // 使用 nickName 或 userName
- teacherType: '' // system/teacher 接口没有 teacherType 字段
- }));
- console.log('教师列表加载完成:', teacherList.value.length, '位教师');
- console.log(
- '教师ID列表:',
- teacherList.value.map((t) => t.teacherId)
- );
- } catch (error: any) {
- console.warn('获取教师列表失败(可能是权限不足),将使用备用查询方式:', error);
- // ⚠️ 不再抛出错误,允许使用备用的同行人查询接口
- teacherList.value = [];
- }
- }
- /** 处理申请详情的附件数据 */
- async function handleApplicationDetailAttachmentData(attachmentIds: string | null | undefined) {
- try {
- if (!attachmentIds || !attachmentIds.toString().trim()) {
- console.log('申请详情没有附件数据,清空附件列表');
- applicationDetailAttachmentList.value = [];
- return;
- }
- console.log('开始处理申请详情附件数据:', attachmentIds);
- const ossIdsArray = attachmentIds
- .toString()
- .split(',')
- .map((id) => id.trim())
- .filter((id) => id);
- if (ossIdsArray.length === 0) {
- applicationDetailAttachmentList.value = [];
- return;
- }
- const response = await request({
- url: `/resource/oss/listByIds/${ossIdsArray.join(',')}`,
- method: 'get'
- });
- if (response.code === 200 && response.data) {
- console.log('获取到的申请附件信息:', response.data);
- const attachmentFiles = response.data.map((item: any, index: number) => ({
- uid: `application_detail_${index}_${Date.now()}`,
- name: item.originalName || `申请附件${index + 1}`,
- url: item.url,
- ossId: item.ossId,
- status: 'success',
- fileSize: item.fileSize || 0, // 使用OSS表中的fileSize字段
- uploadTime: item.createTime,
- response: {
- code: 200,
- data: {
- url: item.url,
- ossId: item.ossId
- }
- }
- }));
- applicationDetailAttachmentList.value = attachmentFiles;
- console.log('申请详情附件数据处理完成,共', attachmentFiles.length, '个附件');
- } else {
- console.warn('获取申请附件信息失败:', response);
- applicationDetailAttachmentList.value = [];
- }
- } catch (error) {
- console.error('处理申请详情附件数据失败:', error);
- applicationDetailAttachmentList.value = [];
- }
- }
- /** 新增培训申请 */
- async function handleAddApplication() {
- resetApplicationForm();
- applicationDialogVisible.value = true;
- applicationDialogTitle.value = '申请培训';
- // 加载课程选项
- if (courseOptions.value && courseOptions.value.length === 0) {
- loadCourseOptions();
- }
- // 加载教师列表
- if (teacherList.value.length === 0) {
- getTeacherList();
- }
- // 自动获取登录用户的部门信息和申请人信息
- try {
- // 确保用户信息已加载
- if (!userStore.userId) {
- await userStore.getInfo();
- }
- console.log('开始自动填充用户信息:', {
- userId: userStore.userId,
- userName: userStore.name,
- nickname: userStore.nickname,
- cachedDeptName: userStore.deptName
- });
- // 自动填充申请人姓名(使用用户昵称,即真实姓名)
- const displayName = userStore.nickname || userStore.name || `用户${userStore.userId}`;
- applicationForm.userName = displayName;
- console.log('申请人姓名自动填充:', {
- userId: userStore.userId,
- displayName: displayName,
- nickname: userStore.nickname,
- userName: userStore.name
- });
- // 重新获取用户信息以确保获取到deptId
- await userStore.getInfo();
- const userInfo = await getUserInfo();
- const userDeptId = userInfo.data.user.deptId;
- console.log('用户信息详情:', {
- userId: userStore.userId,
- deptId: userDeptId,
- cachedDeptName: userStore.deptName
- });
- // 如果有deptId,从数据库sys_dept表实时获取部门名称
- if (userDeptId) {
- console.log('开始从数据库sys_dept表获取部门信息 - deptId:', userDeptId);
- try {
- const deptResponse = await getDept(userDeptId);
- if (deptResponse.code === 200 && deptResponse.data) {
- const realDeptName = deptResponse.data.deptName;
- applicationForm.department = realDeptName;
- console.log('培训申请部门自动匹配成功:', {
- userId: userStore.userId,
- userDeptId: userDeptId,
- deptNameFromDatabase: realDeptName,
- 关联逻辑: 'sys_user.dept_id -> sys_dept.dept_name'
- });
- } else {
- console.warn('从数据库获取部门信息失败,使用缓存的部门名称');
- applicationForm.department = userStore.deptName || '';
- }
- } catch (deptError) {
- console.error('查询部门信息API调用失败:', deptError);
- applicationForm.department = userStore.deptName || '';
- }
- } else {
- console.warn('用户deptId不存在,使用缓存的部门名称');
- applicationForm.department = userStore.deptName || '';
- }
- } catch (error) {
- console.error('获取用户信息失败:', error);
- // 如果获取失败,使用空字符串,用户可以手动输入
- applicationForm.department = '';
- applicationForm.userName = `用户${userStore.userId || '未知'}`;
- }
- }
- /** 重置培训申请表单 */
- function resetApplicationForm() {
- applicationForm.applicationId = undefined;
- // 情况基本信息
- applicationForm.userName = '';
- applicationForm.department = '';
- applicationForm.currentTitle = '';
- applicationForm.currentPosition = '';
- applicationForm.workStartTime = '';
- applicationForm.phone = '';
- // 培训情况信息
- applicationForm.organizerUnit = '';
- applicationForm.location = '';
- applicationForm.startTime = '';
- applicationForm.endTime = '';
- applicationForm.trainingType = '';
- applicationForm.trainingTypeOther = '';
- applicationForm.trainingFee = 0;
- applicationForm.companions = [];
- applicationForm.trainingPurpose = '';
- applicationForm.remark = '';
- // 兼容原有字段
- applicationForm.courseName = '';
- applicationForm.duration = 0;
- applicationForm.reason = '';
- applicationForm.attachmentIds = [];
- applicationAttachmentFileList.value = [];
- selectedCompanions.value = [];
- applicationFormRef.value?.resetFields();
- }
- /** 处理同行人变化 */
- function handleCompanionsChange(teachers: TeacherVO[]) {
- selectedCompanions.value = teachers;
- applicationForm.companions = teachers.map((teacher) => teacher.userId);
- }
- /** 加载编辑时的同行人数据 */
- async function loadCompanionsForEdit() {
- if (applicationForm.companions && applicationForm.companions.length > 0) {
- try {
- // 这里需要根据userId获取教师信息
- // 由于TeacherMultiSelector组件需要完整的TeacherVO对象
- // 我们需要调用API获取教师详细信息
- const teacherPromises = applicationForm.companions.map(async (userId) => {
- try {
- // 这里需要调用获取教师详情的API
- // 暂时使用teacherList中的数据进行匹配
- const teacher = teacherList.value.find((t) => t.teacherId === userId);
- if (teacher) {
- return {
- userId: teacher.teacherId,
- nickName: teacher.teacherName,
- userName: teacher.teacherName,
- phonenumber: '',
- email: '',
- sex: '0',
- deptName: ''
- } as TeacherVO;
- }
- return null;
- } catch (error) {
- console.error('获取教师信息失败:', error);
- return null;
- }
- });
- const teachers = await Promise.all(teacherPromises);
- selectedCompanions.value = teachers.filter((t) => t !== null) as TeacherVO[];
- } catch (error) {
- console.error('加载同行人数据失败:', error);
- selectedCompanions.value = [];
- }
- } else {
- selectedCompanions.value = [];
- }
- }
- /** 取消培训申请对话框 */
- function cancelApplicationDialog() {
- applicationDialogVisible.value = false;
- isResubmitMode.value = false; // 重置重新提交模式
- resetApplicationForm();
- }
- /** 提交培训申请表单 */
- function submitApplicationForm() {
- applicationFormRef.value?.validate((valid: boolean) => {
- if (valid) {
- console.log('[提交培训申请] 提交前检查');
- console.log('[提交培训申请] applicationForm.attachmentIds:', applicationForm.attachmentIds);
- console.log('[提交培训申请] applicationAttachmentFileList:', applicationAttachmentFileList.value);
- applicationDialogLoading.value = true;
- // 创建符合API要求的数据结构
- const apiData: any = {
- courseId: 0, // 使用0表示自定义课程,确保是数字类型
- // 基本字段(兼容原有API)
- courseName: applicationForm.organizerUnit || '自定义培训', // 使用主办单位作为课程名称
- department: applicationForm.department,
- reason: applicationForm.trainingPurpose || '培训目的', // 使用培训目的作为申请理由
- remark: applicationForm.remark,
- location: applicationForm.location,
- duration: Number(applicationForm.trainingFee) || 0, // 使用培训费用作为学时的临时方案
- startTime: applicationForm.startTime,
- endTime: applicationForm.endTime,
- // 新增字段 - 将表单数据扩展字段添加到API中
- currentTitle: applicationForm.currentTitle,
- currentPosition: applicationForm.currentPosition,
- workStartTime: applicationForm.workStartTime,
- phone: applicationForm.phone,
- organizerUnit: applicationForm.organizerUnit,
- trainingType: applicationForm.trainingType === '其他' ? applicationForm.trainingTypeOther : applicationForm.trainingType,
- trainingFee: Number(applicationForm.trainingFee),
- trainingPurpose: applicationForm.trainingPurpose,
- companions: JSON.stringify(applicationForm.companions || []),
- // 附件信息
- attachmentIds: applicationForm.attachmentIds.join(',')
- };
- console.log('[提交培训申请] 最终提交的API数据:', apiData);
- console.log('[提交培训申请] attachmentIds字段值:', apiData.attachmentIds);
- // 如果是编辑现有申请,添加applicationId
- if (applicationForm.applicationId) {
- apiData.applicationId = applicationForm.applicationId;
- // 检查是否是被拒绝的申请重新提交
- const currentApplication = applicationList.value.find((item) => item.applicationId === applicationForm.applicationId);
- if (currentApplication && currentApplication.status === '2') {
- // 如果是被拒绝的申请重新提交,将状态重置为待审核
- apiData.status = '0';
- }
- // 如果是重新提交模式,使用专门的重新提交API
- if (isResubmitMode.value) {
- resubmitTrainingApplication(applicationForm.applicationId, apiData)
- .then(() => {
- proxy?.$modal.msgSuccess('修改并重新提交成功!您的申请已重新进入审批流程。');
- isResubmitMode.value = false; // 重置重新提交模式
- applicationDialogVisible.value = false;
- getApplicationList();
- })
- .catch((error) => {
- console.error('重新提交申请失败:', error);
- proxy?.$modal.msgError(`重新提交失败: ${error.message || '服务器错误'}`);
- })
- .finally(() => {
- applicationDialogLoading.value = false;
- });
- } else {
- // 使用更新API
- updateTrainingApplication(apiData)
- .then(() => {
- proxy?.$modal.msgSuccess('申请更新成功');
- applicationDialogVisible.value = false;
- getApplicationList();
- })
- .catch((error) => {
- console.error('更新申请失败:', error);
- proxy?.$modal.msgError(`更新失败: ${error.message || '服务器错误'}`);
- })
- .finally(() => {
- applicationDialogLoading.value = false;
- });
- }
- } else {
- // 新增申请
- console.log('提交的培训申请数据:', apiData); // 调试日志
- // 使用无需权限验证的API
- submitTrainingApplication(apiData)
- .then((response) => {
- console.log('提交申请响应:', response); // 记录成功响应
- proxy?.$modal.msgSuccess('申请提交成功');
- applicationDialogVisible.value = false;
- getApplicationList();
- })
- .catch((error) => {
- // 打印详细错误信息
- console.error('提交申请失败:', error);
- console.error('错误详情:', error.response ? error.response.data : '无响应数据');
- // 如果是权限问题,提示用户
- if (error.response && error.response.status === 401) {
- proxy?.$modal.msgError('权限验证失败,请重新登录');
- } else if (error.response && error.response.status === 403) {
- proxy?.$modal.msgError('您没有权限执行此操作');
- } else {
- proxy?.$modal.msgError(`提交失败: ${error.message || '服务器错误'}`);
- }
- })
- .finally(() => {
- applicationDialogLoading.value = false;
- });
- }
- }
- });
- }
- /** 导出培训申请 */
- function handleExportTrainingApplication() {
- proxy?.download(
- 'rs/training/application/export',
- {
- ...applicationQueryParams
- },
- `training_application_${new Date().getTime()}.xlsx`
- );
- }
- /** 获取申请状态标签 */
- function getStatusLabel(value: string): string {
- const option = [
- { value: '0', label: '待审核' },
- { value: '1', label: '已通过' },
- { value: '2', label: '已拒绝' },
- { value: '3', label: '已取消' },
- { value: '4', label: '已完成' }
- ].find((item) => item.value === value);
- return option ? option.label : value;
- }
- /** 格式化文件大小 */
- function formatFileSize(bytes: number | string | undefined): string {
- if (!bytes || bytes === 0) return '未知大小';
- // 如果是字符串且包含中文,直接返回
- if (typeof bytes === 'string' && /[\u4e00-\u9fa5]/.test(bytes)) {
- return bytes;
- }
- const size = typeof bytes === 'string' ? parseInt(bytes) : bytes;
- if (isNaN(size)) return '未知大小';
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
- const i = Math.floor(Math.log(size) / Math.log(k));
- return parseFloat((size / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
- }
- /** 异步加载同行人姓名并缓存(使用新的批量查询接口)*/
- async function loadCompanionNames(companions: (string | number)[] | string | null | undefined): Promise<string> {
- if (!companions) {
- return '';
- }
- // 生成缓存key
- const cacheKey = typeof companions === 'string' ? companions : JSON.stringify(companions);
- // 如果已缓存,直接返回
- if (companionNamesCache.value[cacheKey]) {
- return companionNamesCache.value[cacheKey];
- }
- let companionArray: number[] = [];
- // 解析同行人ID列表
- if (typeof companions === 'string') {
- try {
- const parsed = JSON.parse(companions);
- if (Array.isArray(parsed)) {
- companionArray = parsed.map((id) => Number(id));
- }
- console.log('[同行人解析] JSON解析成功:', companionArray);
- } catch (e) {
- // 如果JSON解析失败,可能是逗号分隔的字符串
- companionArray = companions
- .split(',')
- .map((id) => Number(id.trim()))
- .filter((id) => !isNaN(id));
- console.log('[同行人解析] 使用逗号分隔解析:', companionArray);
- }
- } else if (Array.isArray(companions)) {
- companionArray = companions.map((id) => Number(id));
- console.log('[同行人解析] 直接使用数组:', companionArray);
- }
- if (companionArray.length === 0) {
- return '';
- }
- try {
- // 调用新的批量查询接口(不需要system:teacher:list权限)
- console.log('[同行人查询] 调用批量查询接口:', companionArray);
- const response: any = await batchQueryCompanions(companionArray);
- const users = response.data || [];
- console.log('[同行人查询] 查询结果:', users.length, '个用户');
- // 拼接姓名
- const names = companionArray
- .map((userId) => {
- const user = users.find((u: any) => u.userId === userId);
- const name = user ? user.nickName || user.userName : `未知用户(ID:${userId})`;
- console.log(`[同行人查询] ID ${userId} -> ${name}`);
- return name;
- })
- .join('、');
- // 缓存结果
- companionNamesCache.value[cacheKey] = names;
- console.log('[同行人查询] 最终结果:', names);
- return names;
- } catch (error) {
- console.error('[同行人查询] 接口调用失败:', error);
- // 降级方案:显示ID列表
- const fallback = `同行人ID: ${companionArray.join('、')}`;
- companionNamesCache.value[cacheKey] = fallback;
- return fallback;
- }
- }
- /** 获取同行人姓名列表(同步版,从缓存读取)*/
- function getCompanionNames(companions: (string | number)[] | string | null | undefined): string {
- if (!companions) {
- return '';
- }
- const cacheKey = typeof companions === 'string' ? companions : JSON.stringify(companions);
- // 从缓存返回
- if (companionNamesCache.value[cacheKey]) {
- return companionNamesCache.value[cacheKey];
- }
- // 如果缓存中没有,返回"加载中..."并异步加载
- loadCompanionNames(companions);
- return '加载中...';
- }
- /** 查看申请详情 */
- async function handleViewApplicant(row: any) {
- console.log('[查看申请详情] 原始数据:', {
- userId: row.userId,
- userName: row.userName,
- nickName: row.nickName,
- attachmentIds: row.attachmentIds // 添加附件ID日志
- });
- // 如果行数据中没有完整的课程名称,尝试从课程列表获取
- if (!row.courseName && row.courseId) {
- const course = courseList.value.find((c) => c.courseId === row.courseId);
- if (course) {
- row.courseName = course.title;
- }
- }
- // 复制申请详情
- Object.assign(applicationDetail, row);
- console.log('[查看申请详情] 详情数据已设置:', {
- userName: applicationDetail.userName,
- nickName: applicationDetail.nickName,
- companions: applicationDetail.companions,
- attachmentIds: applicationDetail.attachmentIds
- });
- // 预加载同行人信息(使用新接口,不需要system:teacher:list权限)
- if (row.companions) {
- await loadCompanionNames(row.companions);
- console.log('[查看申请详情] 同行人信息已预加载');
- }
- // 加载申请详情的附件数据
- await handleApplicationDetailAttachmentData(row.attachmentIds);
- applicationDetailOpen.value = true;
- }
- /** 修改培训申请 */
- function handleEditApplication(row: any) {
- resetApplicationForm();
- applicationDialogVisible.value = true;
- applicationDialogTitle.value = '修改培训申请';
- // 复制申请数据到表单 - 新字段结构
- applicationForm.userName = row.userName || userStore.nickname || userStore.name;
- applicationForm.department = row.department;
- applicationForm.currentTitle = row.currentTitle || '';
- applicationForm.currentPosition = row.currentPosition || '';
- applicationForm.workStartTime = row.workStartTime || '';
- applicationForm.phone = row.phone || '';
- applicationForm.organizerUnit = row.organizerUnit || row.courseName || '';
- applicationForm.location = row.location;
- applicationForm.startTime = row.startTime;
- applicationForm.endTime = row.endTime;
- // 处理培训类型和其他类型描述
- const trainingType = row.trainingType || '';
- const predefinedTypes = ['教学', '德育', '行政', '管理', '其他'];
- if (predefinedTypes.includes(trainingType)) {
- applicationForm.trainingType = trainingType;
- applicationForm.trainingTypeOther = '';
- } else {
- // 如果是自定义类型,设置为"其他"并将具体内容放到other字段
- applicationForm.trainingType = '其他';
- applicationForm.trainingTypeOther = trainingType;
- }
- applicationForm.trainingFee = row.trainingFee || 0;
- applicationForm.trainingPurpose = row.trainingPurpose || row.reason || '';
- applicationForm.remark = row.remark;
- // 处理同行人员字段 - 从JSON字符串解析为数组
- if (row.companions) {
- try {
- if (typeof row.companions === 'string') {
- applicationForm.companions = JSON.parse(row.companions);
- } else if (Array.isArray(row.companions)) {
- applicationForm.companions = row.companions;
- } else {
- applicationForm.companions = [];
- }
- } catch (e) {
- console.warn('解析companions字段失败:', e);
- applicationForm.companions = [];
- }
- } else {
- applicationForm.companions = [];
- }
- // 处理附件ID列表 - 从逗号分隔字符串解析为数组
- if (row.attachmentIds) {
- applicationForm.attachmentIds = row.attachmentIds.split(',').filter((id) => id.trim());
- } else {
- applicationForm.attachmentIds = [];
- }
- // 兼容原有字段
- applicationForm.courseName = row.courseName || '';
- applicationForm.duration = row.duration || 0;
- applicationForm.reason = row.reason || '';
- // 保存当前编辑的申请ID
- applicationForm.applicationId = row.applicationId;
- // 加载同行人数据到选择器
- loadCompanionsForEdit();
- }
- /** 撤回培训申请 */
- function handleCancelApplication(row: any) {
- proxy?.$modal
- .confirm(`确认要撤回培训申请"${row.courseName || '未命名'}"吗?`)
- .then(() => {
- return cancelTrainingApplication(row.applicationId);
- })
- .then(() => {
- getApplicationList();
- proxy?.$modal.msgSuccess('申请已撤回');
- })
- .catch((error) => {
- // 如果是用户取消操作,不显示错误提示
- if (error === 'cancel' || error === 'close' || error.message === 'cancel') {
- return;
- }
- console.error('撤回申请失败:', error);
- proxy?.$modal.msgError(`撤回失败: ${error.message || '未知错误'}`);
- });
- }
- /** 修改并重新提交申请(用于已拒绝状态) */
- async function handleUpdateAndResubmitApplication(row: any) {
- try {
- // 获取申请详情
- const response = await getTrainingApplication(row.applicationId);
- if (response.code === 200 && response.data) {
- // 设置表单数据 - 新字段结构
- const data = response.data;
- applicationForm.userName = data.userName || userStore.nickname || userStore.name;
- applicationForm.department = data.department;
- applicationForm.currentTitle = data.currentTitle || '';
- applicationForm.currentPosition = data.currentPosition || '';
- applicationForm.workStartTime = data.workStartTime || '';
- applicationForm.phone = data.phone || '';
- applicationForm.organizerUnit = data.organizerUnit || data.courseName || '';
- applicationForm.location = data.location;
- applicationForm.startTime = data.startTime;
- applicationForm.endTime = data.endTime;
- applicationForm.trainingType = data.trainingType || '';
- applicationForm.trainingFee = data.trainingFee || 0;
- applicationForm.trainingPurpose = data.trainingPurpose || data.reason || '';
- applicationForm.remark = data.remark;
- // 兼容原有字段
- applicationForm.courseName = data.courseName || '';
- applicationForm.duration = data.duration || 0;
- applicationForm.reason = data.reason || '';
- // 设置为编辑模式并标记为重新提交
- applicationForm.applicationId = row.applicationId;
- isResubmitMode.value = true;
- // 打开编辑对话框
- applicationDialogVisible.value = true;
- applicationDialogTitle.value = '修改并重新提交申请';
- } else {
- proxy?.$modal.msgError('获取申请详情失败');
- }
- } catch (error) {
- console.error('获取申请详情失败:', error);
- proxy?.$modal.msgError('获取申请详情失败');
- }
- }
- /** 重新提交被拒绝的培训申请 */
- function handleResubmitApplication(row: any) {
- resetApplicationForm();
- applicationDialogVisible.value = true;
- applicationDialogTitle.value = '重新提交培训申请';
- // 复制申请数据到表单 - 新字段结构
- applicationForm.userName = row.userName || userStore.nickname || userStore.name;
- applicationForm.department = row.department;
- applicationForm.currentTitle = row.currentTitle || '';
- applicationForm.currentPosition = row.currentPosition || '';
- applicationForm.workStartTime = row.workStartTime || '';
- applicationForm.phone = row.phone || '';
- applicationForm.organizerUnit = row.organizerUnit || row.courseName || '';
- applicationForm.location = row.location;
- applicationForm.startTime = row.startTime;
- applicationForm.endTime = row.endTime;
- // 处理培训类型和其他类型描述
- const trainingType = row.trainingType || '';
- const predefinedTypes = ['教学', '德育', '行政', '管理', '其他'];
- if (predefinedTypes.includes(trainingType)) {
- applicationForm.trainingType = trainingType;
- applicationForm.trainingTypeOther = '';
- } else {
- // 如果是自定义类型,设置为"其他"并将具体内容放到other字段
- applicationForm.trainingType = '其他';
- applicationForm.trainingTypeOther = trainingType;
- }
- applicationForm.trainingFee = row.trainingFee || 0;
- applicationForm.trainingPurpose = row.trainingPurpose || row.reason || '';
- applicationForm.remark = row.remark;
- // 兼容原有字段
- applicationForm.courseName = row.courseName || '';
- applicationForm.duration = row.duration || 0;
- applicationForm.reason = row.reason || '';
- // 保存当前编辑的申请ID
- applicationForm.applicationId = row.applicationId;
- }
- /** 删除培训申请 */
- function handleDeleteApplication(row: any) {
- proxy?.$modal
- .confirm(`确认删除培训申请"${row.courseName || '未命名'}"吗?`)
- .then(() => {
- return deleteTrainingApplication(row.applicationId);
- })
- .then(() => {
- getApplicationList();
- proxy?.$modal.msgSuccess('删除成功');
- })
- .catch((error) => {
- // 如果是用户取消操作,不显示错误提示
- if (error === 'cancel' || error === 'close' || error.message === 'cancel') {
- return;
- }
- console.error('删除失败:', error);
- proxy?.$modal.msgError(`删除失败: ${error.message || '未知错误'}`);
- });
- }
- /** 更新申请状态 */
- function handleUpdateStatus(row: any, status: string) {
- let text = '';
- switch (status) {
- case '1':
- text = '通过';
- break;
- case '2':
- text = '拒绝';
- break;
- case '3':
- text = '取消';
- break;
- case '4':
- text = '完成';
- break;
- default:
- text = '更新';
- }
- proxy?.$modal
- .confirm(`确认要${text}该申请吗?`)
- .then(() => {
- // 调用更新申请状态的API
- return updateApplicationStatus(row.applicationId, status);
- })
- .then(() => {
- // 更新本地数据
- row.status = status;
- if (status === '1' || status === '2') {
- row.approvalTime = new Date().toISOString();
- }
- proxy?.$modal.msgSuccess(`${text}申请成功`);
- // 刷新申请列表
- getApplicationList();
- // 如果当前正在查看该申请的详情,更新详情中的状态
- if (applicationDetailOpen.value && applicationDetail.applicationId === row.applicationId) {
- applicationDetail.status = status;
- if (status === '1' || status === '2') {
- applicationDetail.approvalTime = new Date().toISOString();
- }
- }
- })
- .catch((error) => {
- // 如果是用户取消操作,不显示错误提示
- if (error === 'cancel' || error === 'close' || error.message === 'cancel') {
- return;
- }
- console.error('更新申请状态失败:', error);
- proxy?.$modal.msgError(`${text}申请失败: ${error.message || '未知错误'}`);
- });
- }
- /** 上传申请培训证书 */
- function handleUploadApplicationCertificate(row: any) {
- certificateUploadData.recordId = row.applicationId;
- certificateUploadData.isApplication = true;
- certificateFileList.value = [];
- certificateDialogVisible.value = true;
- }
- /** 查看申请培训证书 */
- function viewApplicationCertificate(row: any) {
- if (row.certificatePath) {
- window.open(row.certificatePath);
- } else {
- proxy?.$modal.msgWarning('该培训申请暂无证书');
- }
- }
- /** 保存管理员备注 */
- function saveAdminRemark() {
- proxy?.$modal.msgSuccess('保存备注功能开发中');
- // 实际应该调用更新申请的API
- applicationDetailOpen.value = false;
- }
- /** 获取审批级别标签 */
- function getApprovalLevelLabel(level: number): string {
- const levels = [
- { value: 1, label: '部门主管' },
- { value: 2, label: '分管领导' },
- { value: 3, label: '校领导' }
- ];
- const levelObj = levels.find((item) => item.value === level);
- return levelObj ? levelObj.label : '未知';
- }
- /** 培训课程搜索按钮操作 */
- function handleTrainingQuery() {
- trainingQueryParams.pageNum = 1;
- getTrainingList();
- }
- /** 重置培训课程查询条件 */
- function resetTrainingQuery() {
- trainingQueryParams.title = '';
- trainingQueryParams.category = '';
- trainingQueryParams.status = '';
- handleTrainingQuery();
- }
- /** 培训课程多选框选中数据 */
- function handleTrainingSelectionChange(selection: CourseVO[]) {
- trainingIds.value = selection.map((item) => item.courseId);
- trainingSingle.value = selection.length !== 1;
- trainingMultiple.value = !selection.length;
- }
- // 培训记录多选框选中数据
- function handleRecordSelectionChange(selection: RecordVO[]) {
- recordIds.value = selection.map((item) => item.recordId);
- recordSingle.value = selection.length !== 1;
- recordMultiple.value = !selection.length;
- }
- /** 培训子选项卡切换 */
- function handleTrainingSubTabClick() {
- if (trainingSubTab.value === 'application') {
- getApplicationList();
- // 确保教师列表已加载(用于同行人显示)
- if (teacherList.value.length === 0) {
- loadTeacherListForDisplay();
- }
- } else if (trainingSubTab.value === 'record') {
- getRecordList();
- // 确保教师列表已加载(用于同行人显示)
- if (teacherList.value.length === 0) {
- loadTeacherListForDisplay();
- }
- } else {
- getTrainingList();
- }
- }
- // 加载课程选项 - 已废弃 (2025-01-03)
- function loadCourseOptions() {
- // 培训课程模块已废弃,现在课程信息通过申请时的自定义课程名称管理
- console.warn('培训课程选项已废弃,现在使用自定义课程名称');
- courseOptions.value = [];
- return Promise.resolve([]);
- }
- // 远程搜索用户
- function remoteSearchUser(query: string) {
- if (query) {
- proxy?.$modal.loading('正在搜索...');
- // 实际使用时应该调用后端搜索接口
- // searchUsers({ username: query }).then(response => {
- // userOptions.value = response.data;
- // proxy?.$modal.closeLoading();
- // });
- // 模拟数据
- setTimeout(() => {
- userOptions.value = [
- { userId: 1, nickName: '张三' },
- { userId: 2, nickName: '李四' },
- { userId: 3, nickName: '王五' },
- { userId: 4, nickName: '赵六' }
- ].filter((item) => item.nickName.includes(query));
- proxy?.$modal.closeLoading();
- }, 500);
- } else {
- userOptions.value = [];
- }
- }
- /** 获取培训记录列表 */
- function getRecordList() {
- recordLoading.value = true;
- // 构建查询参数,包含已通过审批的申请数据
- const queryParams = {
- ...recordQueryParams,
- userId: userStore.userId, // 设置当前用户ID
- // 确保包含已通过状态的培训申请数据
- includeApprovedApplications: true
- };
- // 调用培训记录接口,获取培训记录数据
- listTrainingRecord(queryParams)
- .then((response) => {
- recordList.value = response.rows;
- recordTotal.value = response.total;
- recordLoading.value = false;
- // 确保教师列表已加载(用于同行人显示)
- if (teacherList.value.length === 0) {
- loadTeacherListForDisplay();
- }
- })
- .catch(() => {
- recordLoading.value = false;
- });
- }
- /** 新增培训记录 */
- function handleAddRecord() {
- resetRecordForm();
- recordDialogVisible.value = true;
- recordDialogTitle.value = '添加培训记录';
- // 加载课程选项
- if (!courseOptions.value || courseOptions.value.length === 0) {
- loadCourseOptions();
- }
- }
- /** 重置培训记录表单 */
- function resetRecordForm() {
- console.log('resetRecordForm 开始执行');
- recordForm.recordId = undefined;
- recordForm.courseId = '';
- recordForm.userId = '';
- recordForm.completionStatus = '1';
- recordForm.score = '';
- recordForm.completionTime = '';
- recordForm.certificatePath = '';
- recordForm.certificateNumber = '';
- recordForm.certificateFileName = '';
- recordForm.attachmentIds = '';
- recordForm.feedback = '';
- recordForm.remark = '';
- console.log('开始重置附件列表,当前 recordCertificateFileList.value:', recordCertificateFileList.value);
- if (recordCertificateFileList.value) {
- recordCertificateFileList.value = [];
- } else {
- console.warn('recordCertificateFileList.value 是 undefined,创建新数组');
- recordCertificateFileList.value = [];
- }
- console.log('开始重置表单字段');
- recordFormRef.value?.resetFields();
- console.log('resetRecordForm 执行完成');
- }
- /** 取消培训记录对话框 */
- function cancelRecordDialog() {
- recordDialogVisible.value = false;
- resetRecordForm();
- }
- /** 提交培训记录表单 */
- function submitRecordForm() {
- recordFormRef.value?.validate((valid: boolean) => {
- if (valid) {
- recordDialogLoading.value = true;
- console.log('=== 提交表单数据 ===');
- console.log('recordForm:', recordForm);
- console.log('证书路径:', recordForm.certificatePath);
- console.log('证书文件名:', recordForm.certificateFileName);
- // 在提交前确保attachmentIds字段是最新的
- updateRecordFormAttachmentIds();
- console.log('提交前的attachmentIds:', recordForm.attachmentIds);
- if (recordForm.recordId) {
- // 修改记录时,保存证书信息到本地存储
- if (recordForm.certificatePath) {
- const certificateData = {
- recordId: recordForm.recordId,
- certificatePath: recordForm.certificatePath,
- certificateFileName: recordForm.certificateFileName,
- updateTime: new Date().toISOString()
- };
- // 保存到本地存储
- const existingData = JSON.parse(localStorage.getItem('training_certificates') || '{}');
- existingData[recordForm.recordId] = certificateData;
- localStorage.setItem('training_certificates', JSON.stringify(existingData));
- console.log('证书数据已保存到本地存储:', certificateData);
- }
- updateTrainingRecord(recordForm)
- .then(() => {
- proxy?.$modal.msgSuccess('修改成功');
- recordDialogVisible.value = false;
- getRecordList();
- })
- .catch((error) => {
- console.log('API调用失败,使用模拟更新:', error);
- // 模拟更新成功
- setTimeout(() => {
- proxy?.$modal.msgSuccess('修改成功');
- recordDialogVisible.value = false;
- getRecordList();
- }, 500);
- })
- .finally(() => {
- recordDialogLoading.value = false;
- });
- } else {
- addTrainingRecord(recordForm)
- .then(() => {
- proxy?.$modal.msgSuccess('新增成功');
- recordDialogVisible.value = false;
- getRecordList();
- })
- .catch((error) => {
- console.log('API调用失败,使用模拟新增:', error);
- // 模拟新增成功
- setTimeout(() => {
- proxy?.$modal.msgSuccess('新增成功');
- recordDialogVisible.value = false;
- getRecordList();
- }, 500);
- })
- .finally(() => {
- recordDialogLoading.value = false;
- });
- }
- }
- });
- }
- /** 修改培训记录 */
- async function handleUpdateRecord(row: RecordVO) {
- console.log('=== 修改培训记录开始 ===');
- console.log('row数据:', row);
- try {
- // 立即显示对话框,避免用户等待
- recordDialogVisible.value = true;
- recordDialogTitle.value = '修改培训记录';
- // 重置表单
- try {
- console.log('开始重置表单...');
- resetRecordForm();
- console.log('表单重置完成');
- } catch (error) {
- console.error('重置表单时出错:', error);
- throw error;
- }
- // 先设置基础数据,确保对话框内容可见
- try {
- console.log('开始设置基础数据...');
- Object.assign(recordForm, {
- recordId: row.recordId,
- courseId: row.courseId,
- courseName: row.courseName,
- userId: row.userId,
- userName: row.userName,
- completionStatus: row.completionStatus || '0',
- score: row.score || '',
- duration: row.duration || '',
- completionTime: row.completionTime || '',
- certificatePath: row.certificatePath || '',
- certificateNumber: row.certificateNumber || '',
- certificateFileName: row.certificateFileName || '',
- attachmentIds: (row as any).attachmentIds || '',
- feedback: row.feedback || '',
- remark: row.remark || ''
- });
- console.log('基础数据设置完成,attachmentIds:', recordForm.attachmentIds);
- } catch (error) {
- console.error('设置基础数据时出错:', error);
- throw error;
- }
- console.log('设置基础数据后的recordForm:', recordForm);
- console.log('当前courseOptions.value:', courseOptions.value);
- // 异步加载课程选项(如果还没有加载)
- try {
- console.log('开始处理课程选项,当前courseOptions:', courseOptions.value);
- if (!courseOptions.value || courseOptions.value.length === 0) {
- console.log('需要加载课程选项');
- loadCourseOptions()
- .then(() => {
- console.log('课程选项加载完成,开始匹配课程名称');
- // 确保课程名称正确显示
- if (!recordForm.courseName && recordForm.courseId && courseOptions.value) {
- const course = courseOptions.value.find((c) => c.courseId === recordForm.courseId);
- if (course) {
- recordForm.courseName = course.title;
- }
- }
- })
- .catch((error) => {
- console.warn('加载课程选项失败:', error);
- });
- } else {
- console.log('使用现有课程选项');
- // 确保课程名称正确显示
- if (!recordForm.courseName && recordForm.courseId && courseOptions.value) {
- const course = courseOptions.value.find((c) => c.courseId === recordForm.courseId);
- if (course) {
- recordForm.courseName = course.title;
- }
- }
- }
- console.log('课程选项处理完成');
- } catch (error) {
- console.error('处理课程选项时出错:', error);
- throw error;
- }
- // 异步获取详细数据(可选)
- try {
- const response = await getTrainingRecord(row.recordId);
- if (response.data) {
- // 合并API返回的数据,保留已设置的基础数据
- Object.assign(recordForm, response.data);
- console.log('API返回的详细数据:', response.data);
- }
- } catch (error) {
- console.log('获取详细数据失败,使用基础数据:', error);
- // 使用基础数据继续,不影响对话框显示
- }
- // 异步处理证书附件数据
- handleRecordAttachmentData().catch((error) => {
- console.warn('处理附件数据失败:', error);
- // 不影响对话框显示
- });
- console.log('=== 修改培训记录完成 ===');
- } catch (error) {
- console.error('修改培训记录过程中发生错误:', error);
- // 即使出错也要显示对话框,让用户看到基础数据
- proxy?.$modal.msgError('加载培训记录数据时发生错误,请重试');
- }
- }
- /** 查看培训记录详情 */
- async function handleViewRecord(row: RecordVO) {
- try {
- // 确保教师列表已加载(用于显示同行人姓名)
- if (teacherList.value.length === 0) {
- await loadTeacherListForDisplay();
- }
- // 首先复制记录的基本信息
- Object.assign(recordDetail, row);
- // 如果行数据中没有完整的课程名称,尝试从课程选项获取
- if (!row.courseName && row.courseId) {
- const course = courseOptions.value.find((c) => c.courseId === row.courseId);
- if (course) {
- recordDetail.courseName = course.title;
- }
- }
- // 如果有申请ID,获取完整的申请详情来补充缺失的字段
- if (row.applicationId) {
- try {
- const applicationResponse = await getTrainingApplication(row.applicationId);
- const applicationData = applicationResponse.data;
- // 合并申请数据中的详细信息到记录详情
- Object.assign(recordDetail, {
- // 保留记录的核心信息
- recordId: row.recordId,
- completionStatus: row.completionStatus,
- completionTime: row.completionTime,
- certificatePath: row.certificatePath,
- certificateNumber: row.certificateNumber,
- feedback: row.feedback,
- // 从申请数据中获取详细信息
- department: applicationData.department || row.department,
- currentTitle: applicationData.currentTitle,
- currentPosition: applicationData.currentPosition,
- workStartTime: applicationData.workStartTime,
- phone: applicationData.phone,
- organizerUnit: applicationData.organizerUnit,
- trainingType: applicationData.trainingType,
- trainingFee: applicationData.trainingFee,
- companions: applicationData.companions,
- trainingPurpose: applicationData.trainingPurpose || applicationData.reason,
- location: applicationData.location || row.location,
- startTime: applicationData.startTime,
- endTime: applicationData.endTime,
- applyTime: applicationData.applyTime,
- // 其他基本信息
- courseName: applicationData.courseName || recordDetail.courseName,
- userName: applicationData.userName || row.userName,
- duration: applicationData.duration || row.duration,
- remark: row.remark
- });
- // 加载申请附件数据
- if (applicationData.attachmentIds) {
- await handleApplicationDetailAttachmentData(applicationData.attachmentIds);
- recordDetailAttachmentList.value = [...applicationDetailAttachmentList.value];
- }
- } catch (error) {
- console.warn('获取申请详情失败,使用记录基本信息:', error);
- // 如果获取申请详情失败,继续使用记录的基本信息
- }
- }
- recordDetailDialogVisible.value = true;
- } catch (error) {
- console.error('查看培训记录详情失败:', error);
- proxy?.$modal.msgError('获取培训记录详情失败,请重试');
- }
- }
- /** 删除培训记录 */
- function handleDeleteRecord(row?: RecordVO) {
- const ids = row?.recordId || recordIds.value;
- if (!ids || (Array.isArray(ids) && ids.length === 0)) {
- return;
- }
- let confirmMessage = '';
- if (row) {
- confirmMessage = `是否确认删除学员"${row.userName}"的培训记录?`;
- } else {
- const selectedItems = recordList.value.filter((item) => recordIds.value.includes(item.applicationId as number | string));
- if (selectedItems.length === 1) {
- confirmMessage = `是否确认删除学员"${selectedItems[0].userName}"的培训记录?`;
- } else {
- const names = selectedItems.map((item) => item.userName).join('、');
- confirmMessage = `是否确认删除学员"${names}"的培训记录?`;
- }
- }
- proxy?.$modal
- .confirm(confirmMessage)
- .then(() => {
- return delTrainingRecord(ids);
- })
- .then(() => {
- getRecordList();
- proxy?.$modal.msgSuccess('删除成功');
- })
- .catch((error) => {
- // 如果是用户取消操作,不显示错误提示
- if (error === 'cancel' || error === 'close' || error.message === 'cancel') {
- return;
- }
- // 模拟删除成功,实际应使用API
- setTimeout(() => {
- getRecordList();
- proxy?.$modal.msgSuccess('删除成功');
- }, 300);
- });
- }
- /** 证书上传前检查 */
- function beforeCertificateUpload(file: File) {
- const isValidType = ['application/pdf', 'image/jpeg', 'image/png'].includes(file.type);
- const isLt10M = file.size / 1024 / 1024 < 10;
- if (!isValidType) {
- proxy?.$modal.msgError('上传证书文件只能是PDF/JPG/PNG格式!');
- return false;
- }
- if (!isLt10M) {
- proxy?.$modal.msgError('上传证书文件大小不能超过10MB!');
- return false;
- }
- return true;
- }
- /** 证书上传成功处理 */
- function handleCertificateSuccess(response: any) {
- certificateDialogVisible.value = false;
- if (certificateUploadData.isApplication) {
- // 更新申请列表
- getApplicationList();
- // 如果当前正在查看该申请的详情,更新详情中的证书路径
- if (applicationDetailOpen.value && applicationDetail.applicationId === certificateUploadData.recordId) {
- if (response && response.data) {
- applicationDetail.certificatePath = response.data.path || response.data.url;
- }
- }
- proxy?.$modal.msgSuccess('申请培训证书上传成功!');
- } else {
- // 更新记录列表
- getRecordList();
- proxy?.$modal.msgSuccess('培训记录证书上传成功!');
- }
- // 重置标记
- certificateUploadData.isApplication = false;
- // 清空文件列表
- certificateFileList.value = [];
- }
- /** 证书上传失败处理 */
- function handleCertificateError(error: any) {
- console.error('证书上传失败:', error);
- let errorMessage = '证书上传失败,请重试!';
- // 尝试从错误响应中获取具体错误信息
- if (error && error.response && error.response.data) {
- if (error.response.data.msg) {
- errorMessage = error.response.data.msg;
- } else if (error.response.data.message) {
- errorMessage = error.response.data.message;
- }
- }
- proxy?.$modal.msgError(errorMessage);
- }
- /** 查看证书 */
- function viewCertificate(row: RecordVO) {
- if (row.certificatePath) {
- const ossId = extractOssIdFromPath(row.certificatePath);
- if (ossId) {
- // 如果有OSS ID,使用下载路径打开
- const downloadUrl = `/dev-api/resource/oss/download/${ossId}`;
- window.open(downloadUrl, '_blank');
- } else {
- // 后备方案:直接使用证书路径
- window.open(row.certificatePath, '_blank');
- }
- } else {
- proxy?.$modal.msgWarning('该培训记录暂无证书');
- }
- }
- /** 培训记录修改对话框中的证书上传前检查 */
- function beforeRecordCertificateUpload(file: File) {
- const isValidType = ['application/pdf', 'image/jpeg', 'image/png'].includes(file.type);
- const isLt10M = file.size / 1024 / 1024 < 10;
- if (!isValidType) {
- proxy?.$modal.msgError('上传证书文件只能是PDF/JPG/PNG格式!');
- return false;
- }
- if (!isLt10M) {
- proxy?.$modal.msgError('上传证书文件大小不能超过10MB!');
- return false;
- }
- return true;
- }
- /** 培训记录修改对话框中的证书上传成功处理 */
- function handleRecordCertificateSuccess(response: any, file: any) {
- if (response.code === 200 && response.data) {
- file.url = response.data.url;
- file.ossId = response.data.ossId;
- proxy?.$modal.msgSuccess('证书上传成功!');
- console.log('证书上传成功:', response.data);
- // 更新文件状态为成功
- file.status = 'success';
- file.response = response;
- // 自动保存附件ID到后端
- saveRecordAttachments();
- // 同时更新recordForm中的attachmentIds字段,确保提交时包含附件信息
- updateRecordFormAttachmentIds();
- }
- }
- /** 培训记录修改对话框中的证书上传失败处理 */
- function handleRecordCertificateError(error: any) {
- console.error('证书上传失败:', error);
- let errorMessage = '证书上传失败,请重试!';
- // 尝试从错误响应中获取具体错误信息
- if (error && error.response && error.response.data) {
- if (error.response.data.msg) {
- errorMessage = error.response.data.msg;
- } else if (error.response.data.message) {
- errorMessage = error.response.data.message;
- }
- }
- proxy?.$modal.msgError(errorMessage);
- }
- /** 保存培训记录附件ID到后端 */
- async function saveRecordAttachments() {
- if (!recordForm.recordId) {
- console.warn('记录ID为空,无法保存附件');
- return;
- }
- try {
- // 提取所有成功上传的ossId
- const attachmentOssIds = recordCertificateFileList.value.filter((file) => file.status === 'success' && file.ossId).map((file) => file.ossId);
- // 转换为逗号分隔的字符串
- const attachmentIds = attachmentOssIds.join(',');
- console.log('保存附件ID:', attachmentIds);
- // 调用后端接口保存附件ID
- await updateRecordAttachments(recordForm.recordId, { attachmentIds });
- console.log('附件ID保存成功');
- } catch (error) {
- console.error('保存附件ID失败:', error);
- proxy?.$modal.msgError('保存附件失败,请重试');
- }
- }
- /** 更新recordForm中的attachmentIds字段 */
- function updateRecordFormAttachmentIds() {
- if (!recordForm.recordId) {
- console.warn('记录ID为空,无法更新附件ID');
- return;
- }
- // 提取所有成功上传的ossId
- const attachmentOssIds = recordCertificateFileList.value.filter((file) => file.status === 'success' && file.ossId).map((file) => file.ossId);
- // 转换为逗号分隔的字符串并更新recordForm
- const attachmentIds = attachmentOssIds.join(',');
- recordForm.attachmentIds = attachmentIds;
- console.log('更新recordForm.attachmentIds:', attachmentIds);
- }
- /** 处理培训记录附件数据 */
- async function handleRecordAttachmentData() {
- try {
- if (!recordForm.attachmentIds || !recordForm.attachmentIds.toString().trim()) {
- console.log('没有附件数据,清空附件列表');
- recordCertificateFileList.value = [];
- return;
- }
- console.log('开始处理附件数据:', recordForm.attachmentIds);
- const ossIdsArray = recordForm.attachmentIds
- .toString()
- .split(',')
- .map((id) => id.trim())
- .filter((id) => id);
- if (ossIdsArray.length === 0) {
- recordCertificateFileList.value = [];
- return;
- }
- const response = await request({
- url: `/resource/oss/listByIds/${ossIdsArray.join(',')}`,
- method: 'get'
- });
- if (response.code === 200 && response.data) {
- console.log('获取到的附件信息:', response.data);
- const attachmentFiles = response.data.map((item: any, index: number) => ({
- uid: `existing_${index}_${Date.now()}`,
- name: item.originalName || `证书${index + 1}`,
- url: item.url,
- ossId: item.ossId,
- status: 'success',
- fileSize: item.fileSize || 0,
- response: {
- code: 200,
- data: {
- url: item.url,
- ossId: item.ossId
- }
- }
- }));
- recordCertificateFileList.value = attachmentFiles;
- console.log('设置附件列表成功:', recordCertificateFileList.value);
- } else {
- console.warn('获取附件信息失败:', response.msg);
- recordCertificateFileList.value = [];
- }
- } catch (error) {
- console.error('处理附件数据失败:', error);
- recordCertificateFileList.value = [];
- }
- }
- /** 从文件路径中提取文件名 */
- function getFileNameFromPath(filePath: string): string {
- if (!filePath) return '未知文件';
- const pathParts = filePath.split('/');
- const fileName = pathParts[pathParts.length - 1];
- // 移除时间戳前缀(如果有的话)
- return fileName.replace(/^\d+_/, '') || '证书文件';
- }
- /** 查看当前证书 */
- function viewCurrentCertificate() {
- if (recordForm.certificatePath) {
- window.open(recordForm.certificatePath);
- } else {
- proxy?.$modal.msgWarning('当前记录暂无证书');
- }
- }
- /** 预览证书文件 */
- function handlePreviewCertificate(file: any) {
- console.log('预览证书文件:', file);
- console.log('file.url:', file.url);
- console.log('file.ossId:', file.ossId);
- if (!file) {
- proxy?.$modal.msgError('文件不存在');
- return;
- }
- // 如果是图片,使用图片预览
- if (isImageFile(file)) {
- let imageUrl = '';
- // 优先使用 file.url(已上传的文件)
- if (file.url) {
- imageUrl = file.url;
- }
- // 如果是新上传的文件,使用预览URL
- else if (file.raw) {
- imageUrl = URL.createObjectURL(file.raw);
- }
- if (imageUrl) {
- console.log('预览图片URL:', imageUrl);
- // 创建图片预览弹窗
- const img = new Image();
- img.onload = function () {
- const newWindow = window.open('', '_blank');
- if (newWindow) {
- newWindow.document.write(`
- <!DOCTYPE html>
- <html>
- <head>
- <title>图片预览 - ${file.name}</title>
- <style>
- body {
- margin: 0;
- padding: 20px;
- background: #f5f5f5;
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- }
- img {
- max-width: 90%;
- max-height: 90%;
- border: 1px solid #ddd;
- border-radius: 4px;
- box-shadow: 0 4px 8px rgba(0,0,0,0.1);
- }
- </style>
- </head>
- <body>
- <img src="${imageUrl}" alt="${file.name}" />
- </body>
- </html>
- `);
- newWindow.document.close();
- }
- };
- img.onerror = function () {
- proxy?.$modal.msgError('图片加载失败,可能需要登录权限');
- // 如果图片加载失败,尝试使用OSS下载地址
- const ossId = getFileOssId(file);
- if (ossId) {
- const downloadUrl = `/dev-api/resource/oss/download/${ossId}`;
- window.open(downloadUrl, '_blank');
- }
- };
- img.src = imageUrl;
- } else {
- proxy?.$modal.msgError('无法获取图片预览');
- }
- } else {
- // 非图片文件,尝试在新窗口打开
- const ossId = getFileOssId(file);
- if (ossId) {
- // 如果有OSS ID,使用下载地址
- const downloadUrl = `/dev-api/resource/oss/download/${ossId}`;
- window.open(downloadUrl, '_blank');
- } else if (file.url) {
- window.open(file.url, '_blank');
- } else {
- proxy?.$modal.msgError('无法预览此类型文件');
- }
- }
- }
- /** 判断是否为图片文件 */
- function isImageFile(file: any): boolean {
- const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp'];
- return imageTypes.includes(file.raw?.type) || (file.url && /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.name));
- }
- /** 获取文件扩展名 */
- function getFileExt(fileName: string): string {
- const ext = fileName.split('.').pop()?.toUpperCase();
- return ext || 'FILE';
- }
- /** 格式化文件名(长文件名显示省略号) */
- function formatFileName(fileName: string): string {
- if (!fileName) return '';
- if (fileName.length <= 20) return fileName;
- return fileName.substring(0, 10) + '...' + fileName.substring(fileName.length - 10);
- }
- /** 获取缩写的文件名(保留后缀) */
- function getShortFileName(fileName: string): string {
- if (!fileName) return '';
- const lastDotIndex = fileName.lastIndexOf('.');
- if (lastDotIndex === -1) {
- return fileName.length > 12 ? fileName.substring(0, 12) + '...' : fileName;
- }
- const name = fileName.substring(0, lastDotIndex);
- const ext = fileName.substring(lastDotIndex);
- if (name.length <= 8) {
- return fileName;
- }
- return name.substring(0, 8) + '...' + ext;
- }
- /** 从文件路径中提取OSS ID */
- function extractOssIdFromPath(filePath: string): string {
- if (!filePath) return '';
- // 如果是下载路径 /resource/oss/download/{ossId},提取ossId
- const downloadMatch = filePath.match(/\/resource\/oss\/download\/([^/?]+)/);
- if (downloadMatch) {
- return downloadMatch[1];
- }
- // 如果路径中包含ossId参数
- const ossIdMatch = filePath.match(/[?&]ossId=([^&]+)/);
- if (ossIdMatch) {
- return ossIdMatch[1];
- }
- // 如果是直接的ossId(通常是数字)
- if (/^\d+$/.test(filePath)) {
- return filePath;
- }
- return '';
- }
- /** 获取证书路径(支持两种存储方式:certificatePath 和 attachmentIds) */
- function getCertificatePath(record: any): string {
- // 优先使用 attachmentIds(新方式,存储OSS ID)
- if (record.attachmentIds && record.attachmentIds.toString().trim()) {
- const ossIds = record.attachmentIds.toString().split(',');
- if (ossIds.length > 0 && ossIds[0].trim()) {
- return ossIds[0].trim(); // 返回第一个OSS ID
- }
- }
- // 后备方案:使用 certificatePath(旧方式,存储文件路径)
- return record.certificatePath || '';
- }
- /** 判断证书是否为图片格式 */
- function isCertificateImage(filePath: string): boolean {
- if (!filePath) return false;
- const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
- const lowerPath = filePath.toLowerCase();
- return imageExtensions.some((ext) => lowerPath.includes(ext));
- }
- /** 获取证书预览URL */
- function getCertificatePreviewUrl(filePath: string): string {
- if (!filePath) return '';
- // 如果是纯数字,认为是OSS ID - 但这里不直接返回URL,需要异步获取
- if (/^\d+$/.test(filePath.trim())) {
- // 对于OSS ID,我们需要通过API获取临时URL
- // 这里返回一个占位符,实际预览通过异步方式处理
- return '';
- }
- const ossId = extractOssIdFromPath(filePath);
- if (ossId) {
- // 如果从路径中提取到OSS ID,使用下载路径
- return `/dev-api/resource/oss/download/${ossId}`;
- }
- // 如果是相对路径,添加API前缀
- if (filePath.startsWith('/')) {
- return `/dev-api${filePath}`;
- }
- // 如果是完整的HTTP URL,直接使用
- if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
- return filePath;
- }
- // 后备方案:假设是相对路径,添加API前缀
- return `/dev-api/${filePath}`;
- }
- /** 异步获取证书预览URL(用于OSS资源)*/
- async function getSecureCertificatePreviewUrl(filePath: string): Promise<string> {
- if (!filePath) return '';
- // 如果是纯数字,认为是OSS ID,需要通过API获取临时预览URL
- if (/^\d+$/.test(filePath.trim())) {
- try {
- const response = await getFilePreviewUrl(filePath.trim());
- // API返回的URL在msg字段中,而不是data字段
- return response.msg || response.data || '';
- } catch (error) {
- console.error('获取OSS文件预览URL失败:', error);
- return '';
- }
- }
- // 其他情况使用同步方法
- return getCertificatePreviewUrl(filePath);
- }
- /** 获取证书文件名 */
- function getCertificateFileName(filePath: string): string {
- if (!filePath) return '未知文件';
- // 从路径中提取文件名
- const fileName = filePath.split('/').pop() || filePath;
- // 如果文件名太长,进行截断
- if (fileName.length > 30) {
- const lastDotIndex = fileName.lastIndexOf('.');
- if (lastDotIndex !== -1) {
- const name = fileName.substring(0, lastDotIndex);
- const ext = fileName.substring(lastDotIndex);
- return name.substring(0, 20) + '...' + ext;
- }
- return fileName.substring(0, 25) + '...';
- }
- return fileName;
- }
- /** 下载证书 */
- function downloadCertificate(row: RecordVO) {
- if (!row.certificatePath) {
- proxy?.$modal.msgWarning('该培训记录暂无证书');
- return;
- }
- const ossId = extractOssIdFromPath(row.certificatePath);
- if (ossId) {
- // 如果有OSS ID,使用下载路径
- const downloadUrl = `/dev-api/resource/oss/download/${ossId}`;
- const link = document.createElement('a');
- link.href = downloadUrl;
- link.download = getCertificateFileName(row.certificatePath);
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- } else {
- // 使用预览URL作为下载URL
- const downloadUrl = getCertificatePreviewUrl(row.certificatePath);
- window.open(downloadUrl, '_blank');
- }
- }
- /** 处理培训证书数据 - 参考宿舍卫生检查的附件处理方式 */
- async function handleCertificateData() {
- try {
- console.log('=== 处理证书数据开始 ===');
- console.log('recordForm.recordId:', recordForm.recordId);
- console.log('recordForm.certificatePath:', recordForm.certificatePath);
- console.log('recordForm.certificateFileName:', recordForm.certificateFileName);
- // 优先从本地存储获取证书信息
- if (recordForm.recordId) {
- try {
- const certificatesData = JSON.parse(localStorage.getItem('training_certificates') || '{}');
- const storedCertificate = certificatesData[recordForm.recordId];
- console.log('本地存储的证书数据:', storedCertificate);
- if (storedCertificate && storedCertificate.certificatePath) {
- // 使用本地存储的数据更新表单
- recordForm.certificatePath = storedCertificate.certificatePath;
- recordForm.certificateFileName = storedCertificate.certificateFileName;
- console.log('从本地存储恢复证书信息:', {
- certificatePath: recordForm.certificatePath,
- certificateFileName: recordForm.certificateFileName
- });
- }
- } catch (error) {
- console.warn('读取本地存储失败:', error);
- }
- }
- if (!recordForm.certificatePath || !recordForm.certificatePath.toString().trim()) {
- console.log('没有证书数据,清空证书列表');
- recordCertificateFileList.value = [];
- return;
- }
- console.log('开始处理证书数据:', recordForm.certificatePath);
- // 尝试从路径中提取OSS ID
- const ossId = extractOssIdFromPath(recordForm.certificatePath);
- if (ossId) {
- // 如果有OSS ID,通过API获取详细信息
- try {
- const response = await request({
- url: `/resource/oss/listByIds/${ossId}`,
- method: 'get'
- });
- if (response.code === 200 && response.data && response.data.length > 0) {
- console.log('获取到的证书信息:', response.data);
- const item = response.data[0];
- const certificateFile = {
- uid: `existing_${Date.now()}`,
- name: item.originalName || recordForm.certificateFileName || getFileNameFromPath(recordForm.certificatePath),
- url: item.url || recordForm.certificatePath,
- ossId: item.ossId || ossId,
- status: 'success',
- response: {
- code: 200,
- data: {
- url: item.url || recordForm.certificatePath,
- ossId: item.ossId || ossId
- }
- }
- };
- recordCertificateFileList.value = [certificateFile];
- console.log('设置证书列表成功:', recordCertificateFileList.value);
- return;
- }
- } catch (error) {
- console.warn('通过OSS API获取证书信息失败,使用本地信息:', error);
- }
- }
- // 如果API获取失败或没有OSS ID,使用本地信息
- const fileName = recordForm.certificateFileName || getFileNameFromPath(recordForm.certificatePath);
- recordCertificateFileList.value = [
- {
- uid: Date.now(),
- name: fileName,
- url: recordForm.certificatePath,
- ossId: ossId,
- status: 'success'
- }
- ];
- console.log('使用本地信息设置证书列表:', recordCertificateFileList.value);
- } catch (error) {
- console.error('处理证书数据失败:', error);
- recordCertificateFileList.value = [];
- }
- }
- /** 获取文件的OSS ID用于ImagePreview */
- function getFileOssId(file: any): string {
- return file.ossId || '';
- }
- /** 下载证书文件 */
- async function handleDownloadCertificate(file: any) {
- let downloadUrl = '';
- let fileName = file.name || 'certificate';
- if (file.response && file.response.data && file.response.data.filePath) {
- downloadUrl = file.response.data.filePath;
- fileName = file.response.data.fileName || fileName;
- } else if (file.ossId) {
- downloadUrl = file.ossId;
- } else if (file.raw) {
- const link = document.createElement('a');
- link.href = URL.createObjectURL(file.raw);
- link.download = fileName;
- link.style.display = 'none';
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- URL.revokeObjectURL(link.href);
- console.log('下载本地文件:', fileName);
- return;
- }
- if (!downloadUrl) {
- proxy?.$modal.msgError('无法获取文件下载地址');
- return;
- }
- try {
- const loading = ElLoading.service({
- text: '正在下载文件...',
- target: document.body
- });
- const response = await request({
- url: '/resource/oss/download/' + downloadUrl,
- method: 'get',
- responseType: 'blob',
- timeout: 300000
- });
- if (!response) {
- throw new Error('响应数据为空');
- }
- let blobData: Blob;
- if (response instanceof Blob) {
- blobData = response;
- } else if (response.data instanceof Blob) {
- blobData = response.data;
- } else {
- throw new Error('无效的响应数据格式');
- }
- const url = window.URL.createObjectURL(blobData);
- const link = document.createElement('a');
- link.href = url;
- link.download = fileName;
- link.style.display = 'none';
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- window.URL.revokeObjectURL(url);
- loading?.close();
- console.log('下载服务器文件成功:', fileName);
- } catch (error) {
- console.error('下载文件失败:', error);
- proxy?.$modal.msgError('下载文件失败,请稍后重试');
- }
- }
- /** 确认删除证书文件 */
- function confirmRemoveCertificate(file: any) {
- proxy?.$modal
- .confirm('确认要删除该证书文件吗?')
- .then(() => {
- handleRemoveCertificate(file);
- })
- .catch(() => {});
- }
- /** 移除证书文件 */
- function handleRemoveCertificate(file: any) {
- // 从文件列表中移除
- const index = recordCertificateFileList.value.findIndex((item) => item.uid === file.uid);
- if (index > -1) {
- recordCertificateFileList.value.splice(index, 1);
- }
- // 自动保存附件ID到后端
- saveRecordAttachments();
- // 同时更新recordForm中的attachmentIds字段
- updateRecordFormAttachmentIds();
- proxy?.$modal.msgSuccess('证书文件已删除');
- }
- /** 搜索培训记录 */
- function handleRecordQuery() {
- recordQueryParams.pageNum = 1;
- getRecordList();
- }
- /** 重置培训记录查询参数 */
- function resetRecordQuery() {
- recordQueryForm.value?.resetFields();
- recordQueryParams.courseName = '';
- recordQueryParams.location = '';
- recordQueryParams.department = '';
- handleRecordQuery();
- }
- /** 导出培训记录 */
- function handleExportRecord() {
- proxy?.download(
- 'rs/training/record/export',
- {
- ...recordQueryParams
- },
- `training_record_${new Date().getTime()}.xlsx`
- );
- }
- /** 搜索培训申请 */
- function handleApplicationQuery() {
- applicationQueryParams.pageNum = 1;
- getApplicationList();
- }
- /** 重置培训申请查询参数 */
- function resetApplicationQuery() {
- applicationQueryForm.value?.resetFields();
- applicationQueryParams.courseId = undefined;
- applicationQueryParams.courseName = '';
- applicationQueryParams.location = '';
- applicationQueryParams.duration = undefined;
- applicationQueryParams.status = '';
- // 保持排除已通过状态的设置
- applicationQueryParams.excludeStatus = '1';
- handleApplicationQuery();
- }
- /** 查看审批历史 */
- async function handleViewHistory(row: any) {
- try {
- proxy?.$modal.loading('正在加载审批历史...');
- const response = await getTrainingApprovalHistory(row.applicationId);
- approvalHistoryList.value = response.data || [];
- approvalHistoryOpen.value = true;
- proxy?.$modal.closeLoading();
- } catch (error) {
- console.error('获取审批历史失败:', error);
- proxy?.$modal.msgError('获取审批历史失败');
- proxy?.$modal.closeLoading();
- }
- }
- // 新增:强制重新初始化函数
- function forceReinitialize() {
- console.log('强制重新初始化组件状态');
- isInitialized.value = false;
- isDataLoaded.value = false;
- // 重置关键状态
- courseOptions.value = [];
- recordList.value = [];
- applicationList.value = [];
- // 重新加载当前选项卡数据
- if (trainingSubTab.value === 'application') {
- getApplicationList();
- } else if (trainingSubTab.value === 'record') {
- getRecordList();
- } else {
- getTrainingList();
- }
- }
- // 删除复杂的初始化函数,采用培训审批页面的简单模式
- // 新增:activated生命周期钩子 - 处理Keep-Alive缓存激活
- onActivated(() => {
- console.log('组件被激活,重新加载当前选项卡数据');
- // 直接重新加载当前选项卡的数据,不依赖复杂的状态判断
- if (trainingSubTab.value === 'application') {
- console.log('重新加载培训申请数据');
- getApplicationList();
- } else if (trainingSubTab.value === 'record') {
- console.log('重新加载培训记录数据');
- getRecordList();
- } else {
- console.log('重新加载培训课程数据');
- getTrainingList();
- }
- // 同时加载课程选项,确保下拉选项可用
- loadCourseOptions();
- // 确保教师列表已加载(用于同行人显示)
- if (teacherList.value.length === 0) {
- loadTeacherListForDisplay();
- }
- });
- onMounted(() => {
- console.log('组件挂载,开始初始化');
- // 采用培训审批页面的成功模式:同时加载所有数据
- getTrainingList();
- getApplicationList();
- getRecordList();
- loadCourseOptions();
- getDepartmentList();
- // 加载教师列表(用于同行人显示)
- loadTeacherListForDisplay();
- });
- // ===== 申请培训附件上传相关方法 =====
- /** 申请培训附件上传前检查 */
- function beforeApplicationAttachmentUpload(file: File) {
- console.log('[申请培训附件上传前检查] 开始检查');
- console.log('[申请培训附件上传前检查] 文件信息:', {
- name: file.name,
- type: file.type,
- size: `${(file.size / 1024 / 1024).toFixed(2)}MB`
- });
- const isValidType = [
- 'application/pdf',
- 'application/msword',
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- 'application/vnd.ms-excel',
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
- 'image/jpeg',
- 'image/png'
- ].includes(file.type);
- const isLt20M = file.size / 1024 / 1024 < 20;
- if (!isValidType) {
- console.error('[申请培训附件上传前检查] 文件类型不符合要求:', file.type);
- proxy?.$modal.msgError('上传附件文件只能是PDF/Word/Excel/图片格式!');
- return false;
- }
- if (!isLt20M) {
- console.error('[申请培训附件上传前检查] 文件大小超出限制:', `${(file.size / 1024 / 1024).toFixed(2)}MB`);
- proxy?.$modal.msgError('上传附件文件大小不能超过20MB!');
- return false;
- }
- console.log('[申请培训附件上传前检查] 检查通过');
- return true;
- }
- /** 申请培训附件上传成功处理 */
- function handleApplicationAttachmentSuccess(response: any, file: any) {
- console.log('[申请培训附件上传成功] 响应数据:', response);
- console.log('[申请培训附件上传成功] 文件对象:', file);
- if (response.code === 200 && response.data) {
- file.url = response.data.url;
- file.ossId = response.data.ossId;
- proxy?.$modal.msgSuccess('附件上传成功!');
- console.log('[申请培训附件上传成功] OSS ID:', response.data.ossId);
- // 更新文件状态为成功
- file.status = 'success';
- file.response = response;
- // 更新applicationForm中的attachmentIds字段
- updateApplicationFormAttachmentIds();
- }
- }
- /** 申请培训附件上传失败处理 */
- function handleApplicationAttachmentError(error: any, file: any) {
- console.error('[申请培训附件上传失败] 错误对象:', error);
- console.error('[申请培训附件上传失败] 文件对象:', file);
- let errorMessage = '附件上传失败,请重试!';
- let errorDetails = '';
- // 尝试从错误响应中获取具体错误信息
- if (error && error.response) {
- console.error('[申请培训附件上传失败] HTTP响应:', {
- status: error.response.status,
- statusText: error.response.statusText,
- data: error.response.data
- });
- if (error.response.data) {
- if (error.response.data.msg) {
- errorMessage = error.response.data.msg;
- } else if (error.response.data.message) {
- errorMessage = error.response.data.message;
- }
- }
- // 常见错误状态码处理
- if (error.response.status === 401) {
- errorDetails = '认证失败,请重新登录';
- } else if (error.response.status === 403) {
- errorDetails = '没有上传权限';
- } else if (error.response.status === 404) {
- errorDetails = '上传接口不存在';
- } else if (error.response.status === 413) {
- errorDetails = '文件太大,服务器拒绝接收';
- } else if (error.response.status >= 500) {
- errorDetails = '服务器内部错误';
- }
- } else if (error.message) {
- console.error('[申请培训附件上传失败] 错误信息:', error.message);
- errorDetails = error.message;
- }
- const finalMessage = errorDetails ? `${errorMessage} (${errorDetails})` : errorMessage;
- proxy?.$modal.msgError(finalMessage);
- }
- /** 更新applicationForm中的attachmentIds字段 */
- function updateApplicationFormAttachmentIds() {
- console.log('[更新附件IDs] 开始更新');
- console.log('[更新附件IDs] 文件列表:', applicationAttachmentFileList.value);
- // 提取所有成功上传的ossId
- const attachmentOssIds = applicationAttachmentFileList.value.filter((file) => file.status === 'success' && file.ossId).map((file) => file.ossId);
- // 更新applicationForm
- applicationForm.attachmentIds = attachmentOssIds;
- console.log('[更新附件IDs] 提取的OSS IDs:', attachmentOssIds);
- console.log('[更新附件IDs] applicationForm.attachmentIds:', applicationForm.attachmentIds);
- }
- /** 移除申请培训附件 */
- function handleRemoveApplicationAttachment(file: any) {
- // 更新attachmentIds
- updateApplicationFormAttachmentIds();
- }
- /** 确认移除申请培训附件 */
- function confirmRemoveApplicationAttachment(file: any) {
- proxy?.$modal
- .confirm('确定要删除这个附件吗?')
- .then(() => {
- const index = applicationAttachmentFileList.value.findIndex((f) => f.uid === file.uid);
- if (index > -1) {
- applicationAttachmentFileList.value.splice(index, 1);
- updateApplicationFormAttachmentIds();
- proxy?.$modal.msgSuccess('附件删除成功');
- }
- })
- .catch(() => {
- console.log('取消删除附件');
- });
- }
- /** 预览申请培训附件 */
- function handlePreviewApplicationAttachment(file: any) {
- if (file.url) {
- window.open(file.url, '_blank');
- }
- }
- /** 下载申请培训附件 */
- async function handleDownloadApplicationAttachment(file: any) {
- console.log('[下载申请培训附件] 开始下载');
- console.log('[下载申请培训附件] 文件信息:', file);
- if (!file.ossId) {
- console.error('[下载申请培训附件] ossId为空,无法下载');
- proxy?.$modal.msgError('文件信息不完整,无法下载');
- return;
- }
- try {
- console.log('[下载申请培训附件] 请求下载接口:', `/resource/oss/download/${file.ossId}`);
- // 使用后端安全下载接口,通过request工具发送请求以包含认证头
- const response = await request({
- url: `/resource/oss/download/${file.ossId}`,
- method: 'get',
- responseType: 'blob' // 指定响应类型为blob
- });
- console.log('[下载申请培训附件] 接口响应:', response);
- // 创建blob URL并触发下载
- const blob = response instanceof Blob ? response : new Blob([response.data]);
- const url = window.URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = file.name || '申请培训附件';
- document.body.appendChild(link);
- link.click();
- // 清理资源
- document.body.removeChild(link);
- window.URL.revokeObjectURL(url);
- console.log('[下载申请培训附件] 下载成功');
- proxy?.$modal.msgSuccess('文件下载成功');
- } catch (error: any) {
- console.error('[下载申请培训附件] 下载失败:', error);
- let errorMessage = '下载附件失败,请稍后重试';
- if (error.response) {
- console.error('[下载申请培训附件] HTTP响应错误:', {
- status: error.response.status,
- statusText: error.response.statusText
- });
- if (error.response.status === 401) {
- errorMessage = '认证失败,请重新登录';
- } else if (error.response.status === 403) {
- errorMessage = '没有下载权限';
- } else if (error.response.status === 404) {
- errorMessage = '文件不存在或已被删除';
- }
- }
- proxy?.$modal.msgError(errorMessage);
- }
- }
- </script>
- <style scoped>
- .certificate-upload-container {
- border: 1px dashed #d9d9d9;
- border-radius: 6px;
- padding: 15px;
- background-color: #fafafa;
- }
- .certificate-upload-container:hover {
- border-color: #409eff;
- }
- .attachment-upload-container {
- border: 1px dashed #d9d9d9;
- border-radius: 6px;
- padding: 15px;
- background-color: #fafafa;
- box-sizing: border-box;
- }
- .attachment-upload-container .attachment-upload-demo {
- width: 100%;
- }
- .attachment-upload-container .attachment-upload-demo .el-upload {
- width: 100%;
- height: auto;
- background-color: transparent !important;
- border: none !important;
- }
- .attachment-upload-container .attachment-upload-demo .el-upload:hover {
- background-color: transparent !important;
- }
- .attachment-upload-container .el-upload-dragger {
- background-color: transparent !important;
- border: none !important;
- width: 100% !important;
- height: auto !important;
- padding: 0 !important;
- margin: 0 !important;
- }
- .attachment-upload-container .el-upload-dragger:hover {
- background-color: transparent !important;
- border: none !important;
- }
- .attachment-upload-container:hover {
- border-color: #409eff;
- }
- .el-upload-dragger-attachment {
- padding: 0 !important;
- margin: 0 !important;
- text-align: center;
- background-color: transparent;
- border: none;
- min-height: 80px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 4px;
- box-sizing: border-box;
- }
- .el-upload-dragger-attachment .el-icon--upload {
- font-size: 24px;
- color: #c0c4cc;
- margin: 0;
- }
- .el-upload-dragger-attachment .el-upload__text {
- color: #666666;
- font-size: 12px;
- line-height: 1.3;
- margin: 0;
- }
- .el-upload-dragger-attachment .el-upload__text em {
- color: #409eff;
- font-style: normal;
- font-weight: 500;
- }
- .el-upload-dragger-attachment .el-upload__tip {
- font-size: 11px !important;
- color: #909399 !important;
- margin: 0 !important;
- line-height: 1.2;
- }
- .record-certificate-upload {
- margin-bottom: 10px;
- }
- .current-certificate {
- margin-top: 10px;
- padding: 8px;
- background-color: #f0f9ff;
- border: 1px solid #b3d8ff;
- border-radius: 4px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .current-certificate .el-tag {
- margin-right: 10px;
- }
- .detail-text {
- white-space: pre-wrap;
- word-break: break-word;
- }
- .pagination-container {
- padding: 20px 0;
- text-align: center;
- }
- .search-btn-container {
- text-align: center;
- }
- .button-card {
- margin-top: 10px;
- }
- .table-card {
- margin-bottom: 10px;
- }
- /* 证书上传拖拽区域样式 - 紧凑设计 */
- .el-upload-dragger-certificate {
- text-align: center;
- padding: 10px 15px;
- min-height: 40px;
- border: 2px dashed #d9d9d9;
- border-radius: 6px;
- background-color: #fafafa;
- transition: border-color 0.3s;
- }
- .el-upload-dragger-certificate:hover {
- border-color: #409eff;
- }
- .el-upload-dragger-certificate .el-icon--upload {
- font-size: 18px;
- color: #c0c4cc;
- margin-bottom: 4px;
- }
- .el-upload-dragger-certificate .el-upload__text {
- color: #606266;
- font-size: 12px;
- line-height: 1.2;
- }
- .el-upload-dragger-certificate .el-upload__text em {
- color: #409eff;
- font-style: normal;
- }
- /* 证书文件卡片样式 */
- .certificate-file-card {
- position: relative;
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- background: #fff;
- border-radius: 6px;
- overflow: hidden;
- }
- .certificate-file-thumb {
- position: relative;
- width: 100%;
- height: 120px;
- background: #f5f7fa;
- display: flex;
- align-items: center;
- justify-content: center;
- overflow: hidden;
- }
- .certificate-thumb-image {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- .certificate-file-placeholder {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 100%;
- color: #909399;
- }
- .certificate-file-placeholder .el-icon {
- font-size: 36px;
- margin-bottom: 8px;
- }
- .certificate-file-placeholder .file-ext {
- font-size: 12px;
- font-weight: bold;
- color: #f56c6c;
- }
- .certificate-file-loading {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(255, 255, 255, 0.9);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- z-index: 10;
- }
- .certificate-file-loading .el-icon {
- font-size: 24px;
- color: #409eff;
- margin-bottom: 8px;
- }
- .certificate-file-loading .loading-text {
- font-size: 12px;
- color: #606266;
- }
- .certificate-file-name {
- padding: 8px;
- font-size: 12px;
- color: #606266;
- text-align: center;
- background: #fff;
- border-top: 1px solid #ebeef5;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .certificate-file-card-actions {
- position: absolute;
- top: 4px;
- right: 4px;
- background: rgba(255, 255, 255, 0.9);
- border-radius: 4px;
- padding: 2px;
- }
- .certificate-file-card-actions .el-button {
- padding: 4px;
- min-height: 24px;
- }
- .certificate-file-card-actions .el-button .el-icon {
- font-size: 12px;
- }
- /* 证书上传整体容器优化 */
- .certificate-upload-demo {
- width: 100%;
- }
- /* 强制覆盖Element Plus默认高度 */
- .certificate-upload-demo :deep(.el-upload) {
- height: auto !important;
- min-height: 40px !important;
- }
- .certificate-upload-demo :deep(.el-upload-dragger) {
- height: auto !important;
- min-height: 40px !important;
- padding: 10px 15px !important;
- }
- .certificate-upload-demo :deep(.el-upload-list--picture-card) {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- }
- .certificate-upload-demo :deep(.el-upload--picture-card) {
- width: auto;
- height: auto;
- min-height: 40px;
- border: none;
- background: transparent;
- display: flex;
- align-items: center;
- }
- .certificate-upload-demo :deep(.el-upload-list__item) {
- width: 120px;
- height: auto;
- margin: 0;
- border: 1px solid #ebeef5;
- border-radius: 6px;
- overflow: hidden;
- }
- /* 提示文字样式优化 */
- .el-upload__tip {
- margin-top: 8px !important;
- font-size: 12px !important;
- color: #909399 !important;
- line-height: 1.4 !important;
- }
- /* 只读输入框样式 - 确保灰色背景被白色边框完全包裹 */
- .readonly-input {
- width: 100% !important;
- }
- /* 通用样式,适配不同版本的Element Plus */
- .readonly-input :deep(.el-input),
- .readonly-input :deep(.el-input__wrapper) {
- background-color: #f5f7fa !important;
- border: 1px solid #dcdfe6 !important;
- border-radius: 4px !important;
- box-shadow: none !important;
- }
- .readonly-input :deep(.el-input__wrapper) {
- padding: 1px 11px !important;
- min-height: 32px !important;
- display: flex !important;
- align-items: center !important;
- }
- .readonly-input :deep(.el-input__inner) {
- background-color: transparent !important;
- color: #606266 !important;
- border: none !important;
- cursor: not-allowed !important;
- padding: 0 !important;
- height: 30px !important;
- line-height: 30px !important;
- outline: none !important;
- box-shadow: none !important;
- }
- /* 悬停和聚焦状态 */
- .readonly-input :deep(.el-input:hover),
- .readonly-input :deep(.el-input__wrapper:hover) {
- border-color: #dcdfe6 !important;
- box-shadow: none !important;
- }
- .readonly-input :deep(.el-input.is-focus),
- .readonly-input :deep(.el-input__wrapper.is-focus) {
- border-color: #dcdfe6 !important;
- box-shadow: none !important;
- }
- .readonly-input :deep(.el-input:hover .el-input__inner),
- .readonly-input :deep(.el-input.is-focus .el-input__inner) {
- border-color: transparent !important;
- box-shadow: none !important;
- }
- </style>
- <style scoped>
- .card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .el-tag {
- margin-right: 5px;
- }
- /* 页面容器样式 */
- .app-container {
- padding: 8px;
- }
- /* 表格卡片布局 */
- .table-card {
- margin-top: 0;
- }
- .table-card :deep(.el-card__body) {
- padding: 0px 20px 20px 20px !important;
- }
- /* 移除按钮栏底部边框 */
- .button-card :deep(.el-card__header) {
- border-bottom: none !important;
- }
- /* 分页容器样式 */
- .pagination-container {
- display: flex;
- justify-content: flex-end;
- padding: 16px 20px 0 0 !important;
- background-color: #fff;
- margin-top: 0px !important;
- }
- /* 搜索按钮容器样式 */
- .search-btn-container {
- display: flex;
- justify-content: flex-end;
- margin-top: 10px;
- }
- .search-btn-container :deep(.el-form-item) {
- margin-bottom: 0 !important;
- }
- /* 表单提示信息 */
- .el-form-item-msg {
- font-size: 12px;
- color: #909399;
- margin-top: 5px;
- }
- /* 详情文本 */
- .detail-text {
- padding: 8px;
- border: 1px solid #e4e7ed;
- border-radius: 4px;
- min-height: 60px;
- max-height: 200px;
- overflow-y: auto;
- }
- /* 审批历史项样式 */
- .approval-history-item {
- padding: 10px 0;
- border-bottom: 1px solid #ebeef5;
- }
- .approval-history-item:last-child {
- border-bottom: none;
- }
- .approval-header {
- display: flex;
- align-items: center;
- margin-bottom: 5px;
- }
- .approver-name {
- font-weight: bold;
- margin-right: 10px;
- }
- .approval-level {
- margin-right: 10px;
- color: #909399;
- }
- .approval-opinion {
- font-size: 14px;
- color: #606266;
- line-height: 1.6;
- }
- .no-data {
- text-align: center;
- padding: 20px;
- color: #909399;
- }
- /* 文件卡片样式(参考宿舍卫生检查) */
- .upload-file-card {
- position: relative;
- width: 100%;
- height: 100%;
- border-radius: 6px;
- overflow: hidden;
- background: #fff;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- transition: all 0.3s;
- }
- .upload-file-card:hover {
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
- }
- .upload-file-thumb {
- position: relative;
- width: 100%;
- height: 110px;
- background: #f5f5f5;
- display: flex;
- align-items: center;
- justify-content: center;
- overflow: hidden;
- }
- .upload-thumb-image {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- .upload-file-image {
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #f5f5f5;
- }
- .upload-file-loading {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- z-index: 10;
- }
- .upload-file-placeholder {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 100%;
- color: #909399;
- }
- .upload-file-placeholder .file-ext {
- font-size: 12px;
- margin-top: 5px;
- }
- .loading-text {
- margin-top: 8px;
- font-size: 12px;
- color: #606266;
- }
- .upload-file-name {
- padding: 6px 8px;
- font-size: 11px;
- color: #303133;
- text-align: center;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- background: #fafafa;
- border-top: 1px solid #f0f0f0;
- line-height: 1.2;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- word-break: break-all;
- max-width: 100%;
- }
- .upload-file-card-actions {
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 6px 8px;
- background: #fafafa;
- border-top: 1px solid #f0f0f0;
- transition: all 0.3s;
- height: 38px;
- }
- .upload-file-card-actions .el-button-group {
- background: transparent;
- border-radius: 4px;
- padding: 0;
- }
- .upload-file-card-actions .el-button {
- padding: 4px 8px;
- border: 1px solid #e0e0e0;
- background: #fff;
- font-size: 12px;
- margin: 0 2px;
- border-radius: 3px;
- transition: all 0.3s;
- }
- .upload-file-card-actions .el-button:hover {
- background: #f5f5f5;
- border-color: #409eff;
- }
- .upload-file-card-actions .el-button.is-disabled {
- opacity: 0.5;
- cursor: not-allowed;
- background: #f5f5f5;
- }
- </style>
- <style lang="scss" scoped>
- .app-container {
- // ... 现有样式
- // 拒绝原因样式
- .reject-reason {
- :deep(.el-descriptions-item__content) {
- padding: 12px;
- }
- .reject-info {
- width: 100%;
- .reject-title {
- font-weight: bold;
- font-size: 14px;
- }
- .reject-content {
- margin-top: 8px;
- padding: 8px;
- background-color: #fef0f0;
- border-radius: 4px;
- color: #f56c6c;
- white-space: pre-wrap;
- word-break: break-word;
- line-height: 1.5;
- }
- .reject-action {
- margin-top: 10px;
- text-align: right;
- }
- }
- }
- // 审批历史样式
- .approval-history-item {
- padding: 0 0 8px 0;
- .approval-header {
- display: flex;
- align-items: center;
- margin-bottom: 8px;
- .approver-name {
- font-weight: bold;
- margin-right: 8px;
- }
- .approval-level {
- color: #909399;
- margin-right: 8px;
- }
- }
- .approval-opinion {
- background-color: #f5f7fa;
- padding: 8px 12px;
- border-radius: 4px;
- color: #606266;
- white-space: pre-wrap;
- word-break: break-word;
- line-height: 1.5;
- }
- }
- .no-data {
- text-align: center;
- color: #909399;
- padding: 30px 0;
- }
- // 证书预览样式
- .certificate-preview {
- .certificate-container {
- display: flex;
- align-items: center;
- margin-bottom: 10px;
- .image-preview {
- .image-slot {
- display: flex;
- justify-content: center;
- align-items: center;
- flex-direction: column;
- width: 100%;
- height: 100%;
- background: #f5f7fa;
- color: #909399;
- font-size: 14px;
- .el-icon {
- font-size: 24px;
- margin-bottom: 5px;
- }
- }
- }
- .image-preview {
- .el-image {
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- transition: transform 0.2s;
- &:hover {
- transform: scale(1.02);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- }
- }
- .loading-preview {
- background: #f9f9f9;
- color: #909399;
- font-size: 14px;
- .el-icon {
- margin-right: 8px;
- font-size: 18px;
- }
- }
- .image-slot {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 100%;
- color: #909399;
- .el-icon {
- font-size: 32px;
- margin-bottom: 8px;
- }
- div {
- font-size: 14px;
- }
- }
- }
- .file-info {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 20px;
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- background: #fafafa;
- min-width: 200px;
- .el-icon {
- color: #909399;
- margin-bottom: 10px;
- }
- .file-name {
- font-size: 14px;
- color: #606266;
- margin-bottom: 5px;
- word-break: break-all;
- text-align: center;
- }
- .file-type {
- font-size: 12px;
- color: #909399;
- }
- }
- }
- .certificate-actions {
- display: flex;
- gap: 10px;
- .el-button {
- display: flex;
- align-items: center;
- gap: 5px;
- }
- }
- }
- .no-certificate {
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 120px;
- }
- }
- </style>
|