index.vue 272 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103
  1. <template>
  2. <div class="position-detail-container">
  3. <div class="header-container">
  4. <page-header title="测试" @back="goBack" />
  5. <div class="header-actions">
  6. <el-button type="primary" @click="handleEdit">保存</el-button>
  7. <!-- <el-button @click="handleShare">分享职位</el-button> -->
  8. </div>
  9. </div>
  10. <div class="detail-content">
  11. <!-- 左侧详情信息 -->
  12. <el-card class="detail-card">
  13. <div class="section-title">
  14. <div class="section-line"></div>
  15. <span>职位详情</span>
  16. </div>
  17. <div class="detail-item">
  18. <div class="detail-label">职位名称</div>
  19. <div class="detail-value" v-if="!isEditingTitle">
  20. {{ positionData.title }}
  21. <el-button type="text" class="edit-title-btn" @click="startEditTitle">
  22. <el-icon>
  23. <Edit />
  24. </el-icon>
  25. </el-button>
  26. </div>
  27. <div class="title-edit-container" v-else>
  28. <el-input v-model="editingTitle" placeholder="请输入职位名称" maxlength="20" show-word-limit />
  29. <div class="title-edit-actions">
  30. <el-button size="small" @click="cancelEditTitle">取消</el-button>
  31. <el-button size="small" type="primary" @click="saveTitle">保存</el-button>
  32. </div>
  33. </div>
  34. </div>
  35. <!-- 工作地点 -->
  36. <div class="detail-item">
  37. <div class="detail-label">工作地点</div>
  38. <div class="detail-value" v-if="!isEditingLocation">
  39. {{
  40. (() => {
  41. if (!positionData.location) return '暂无';
  42. if (Array.isArray(positionData.location)) return positionData.location.join(',');
  43. if (typeof positionData.location === 'string') {
  44. try {
  45. // 移除方括号并分割字符串
  46. const cleanStr = positionData.location.replace(/[\[\]']/g, '');
  47. return cleanStr.split(',').map(item => item.trim()).join(',');
  48. } catch (e) {
  49. return positionData.location;
  50. }
  51. }
  52. return positionData.location;
  53. })()
  54. }}
  55. <el-button type="text" class="edit-title-btn" @click="startEditLocation">
  56. <el-icon>
  57. <Edit />
  58. </el-icon>
  59. </el-button>
  60. </div>
  61. <div class="title-edit-container" v-else>
  62. <!-- <el-input
  63. v-model="editingLocation"
  64. placeholder="请输入工作地点"
  65. maxlength="50"
  66. show-word-limit
  67. /> -->
  68. <el-cascader v-model="editingLocation" :options="locationOptions" :props="{
  69. expandTrigger: 'hover',
  70. checkStrictly: false,
  71. value: 'code',
  72. label: 'name'
  73. }" style="width: 100%" placeholder="请选择工作地点" clearable @change="handleLocationChange" />
  74. <div class="title-edit-actions">
  75. <el-button size="small" @click="cancelEditLocation">取消</el-button>
  76. <el-button size="small" type="primary" @click="saveLocation">保存</el-button>
  77. </div>
  78. </div>
  79. </div>
  80. <!-- 详细地址 -->
  81. <div class="detail-item">
  82. <div class="detail-label">详细地址</div>
  83. <div class="detail-value" v-if="!isDetailedAddress">
  84. {{ positionData.detailed_address || '暂无' }}
  85. <el-button type="text" class="edit-title-btn" @click="startDetailedAddress">
  86. <el-icon>
  87. <Edit />
  88. </el-icon>
  89. </el-button>
  90. </div>
  91. <div class="title-edit-container" v-else>
  92. <el-input v-model="detailed_address" placeholder="请输入详细地址" maxlength="30" show-word-limit />
  93. <div class="title-edit-actions">
  94. <el-button size="small" @click="cancelDetailedAddress">取消</el-button>
  95. <el-button size="small" type="primary" @click="saveDetailedAddress">保存</el-button>
  96. </div>
  97. </div>
  98. </div>
  99. <!-- 职位薪资 -->
  100. <div class="detail-item">
  101. <div class="detail-label">职位薪资</div>
  102. <div class="detail-value" v-if="!isEditingSalary">
  103. {{ positionData.salary_range || '暂无' }}
  104. <el-button type="text" class="edit-title-btn" @click="startEditSalary">
  105. <el-icon>
  106. <Edit />
  107. </el-icon>
  108. </el-button>
  109. </div>
  110. <div class="title-edit-container" v-else>
  111. <div class="salary-range-container">
  112. <el-select v-model="editingSalaryType" placeholder="薪资类型" class="salary-type-select"
  113. @change="updateSalaryRange">
  114. <el-option label="月薪" value="月薪" />
  115. <el-option label="年薪" value="年薪" />
  116. <el-option label="日薪" value="日薪" />
  117. <el-option label="时薪" value="时薪" />
  118. </el-select>
  119. <el-input-number v-model="editingStartSalary" placeholder="起始薪资" class="salary-input" type="number"
  120. controls-position="right" min="0" @input="updateSalaryRange" @blur="handleSalaryBlur" />
  121. <span class="salary-separator">-</span>
  122. <el-input-number v-model="editingEndSalary" style="margin-left: 20px;" placeholder="最高薪资" class="salary-input"
  123. controls-position="right" type="number" min="0" @input="updateSalaryRange" @blur="handleSalaryBlur" />
  124. </div>
  125. <div class="title-edit-actions">
  126. <el-button size="small" @click="cancelEditSalary">取消</el-button>
  127. <el-button size="small" type="primary" @click="saveSalary">保存</el-button>
  128. </div>
  129. </div>
  130. </div>
  131. <!-- 工作经验 -->
  132. <div class="detail-item">
  133. <div class="detail-label">工作经验</div>
  134. <div class="detail-value" v-if="!isEditingExperience">
  135. {{ positionData.work_experience_required || '暂无' }}
  136. <el-button type="text" class="edit-title-btn" @click="startEditExperience">
  137. <el-icon>
  138. <Edit />
  139. </el-icon>
  140. </el-button>
  141. </div>
  142. <div class="title-edit-container" v-else>
  143. <el-select v-model="editingExperience" placeholder="请选择工作经验" style="width: 100%">
  144. <el-option label="不限" value="不限" />
  145. <el-option label="应届毕业生" value="应届毕业生" />
  146. <el-option label="1年以下" value="1年以下" />
  147. <el-option label="1-3年" value="1-3年" />
  148. <el-option label="3-5年" value="3-5年" />
  149. <el-option label="5-10年" value="5-10年" />
  150. <el-option label="10年以上" value="10年以上" />
  151. </el-select>
  152. <div class="title-edit-actions">
  153. <el-button size="small" @click="cancelEditExperience">取消</el-button>
  154. <el-button size="small" type="primary" @click="saveExperience">保存</el-button>
  155. </div>
  156. </div>
  157. </div>
  158. <!-- 学历要求 -->
  159. <div class="detail-item">
  160. <div class="detail-label">学历要求</div>
  161. <div class="detail-value" v-if="!isEditingEducation">
  162. {{ positionData.education_required || '暂无' }}
  163. <el-button type="text" class="edit-title-btn" @click="startEditEducation">
  164. <el-icon>
  165. <Edit />
  166. </el-icon>
  167. </el-button>
  168. </div>
  169. <div class="title-edit-container" v-else>
  170. <el-select v-model="editingEducation" placeholder="请选择学历要求" style="width: 100%">
  171. <el-option label="不限" value="不限" />
  172. <el-option label="高中" value="高中" />
  173. <el-option label="大专" value="大专" />
  174. <el-option label="本科" value="本科" />
  175. <el-option label="硕士" value="硕士" />
  176. <el-option label="博士" value="博士" />
  177. </el-select>
  178. <div class="title-edit-actions">
  179. <el-button size="small" @click="cancelEditEducation">取消</el-button>
  180. <el-button size="small" type="primary" @click="saveEducation">保存</el-button>
  181. </div>
  182. </div>
  183. </div>
  184. <!-- 所属部门 -->
  185. <div class="detail-item">
  186. <div class="detail-label">所属部门</div>
  187. <div class="detail-value" v-if="!isEditingDepartment">
  188. {{ positionData.department || '暂无' }}
  189. <el-button type="text" class="edit-title-btn" @click="startEditDepartment">
  190. <el-icon>
  191. <Edit />
  192. </el-icon>
  193. </el-button>
  194. </div>
  195. <div class="title-edit-container" v-else>
  196. <el-input v-model="editingDepartment" placeholder="请输入所属部门" maxlength="30" show-word-limit />
  197. <div class="title-edit-actions">
  198. <el-button size="small" @click="cancelEditDepartment">取消</el-button>
  199. <el-button size="small" type="primary" @click="saveDepartment">保存</el-button>
  200. </div>
  201. </div>
  202. </div>
  203. <div class="detail-item">
  204. <div class="detail-label">职位状态</div>
  205. <div class="detail-value">
  206. <el-tag :type="getStatusType(positionData.status)">
  207. {{ getStatusText(positionData.status) }}
  208. </el-tag>
  209. </div>
  210. </div>
  211. <div class="detail-item">
  212. <div class="detail-label">截止日期</div>
  213. <div class="detail-value" v-if="!isEditingEndDate">
  214. {{ positionData.end_date || '暂无' }}
  215. <el-button type="text" class="edit-title-btn" @click="startEditEndDate">
  216. <el-icon>
  217. <Edit />
  218. </el-icon>
  219. </el-button>
  220. </div>
  221. <div class="title-edit-container" v-else>
  222. <el-date-picker v-model="editingEndDate" type="datetime" placeholder="请选择截止日期"
  223. value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
  224. <div class="title-edit-actions">
  225. <el-button size="small" @click="cancelEditEndDate">取消</el-button>
  226. <el-button size="small" type="primary" @click="saveEndDate">保存</el-button>
  227. </div>
  228. </div>
  229. </div>
  230. <div class="detail-item">
  231. <div class="detail-label">职位要求</div>
  232. <div class="detail-value html-content" v-if="!isEditingRequirements" v-html="positionData.requirements"></div>
  233. <el-button type="text" class="edit-title-btn" @click="startEditRequirements" v-if="!isEditingRequirements">
  234. <el-icon>
  235. <Edit />
  236. </el-icon>
  237. </el-button>
  238. <div class="title-edit-container" v-else>
  239. <el-input v-model="editingRequirements" type="textarea" rows="6" placeholder="请输入职位要求" maxlength="1000"
  240. show-word-limit />
  241. <div class="title-edit-actions">
  242. <el-button size="small" @click="cancelEditRequirements">取消</el-button>
  243. <el-button size="small" type="primary" @click="saveRequirements">保存</el-button>
  244. </div>
  245. </div>
  246. </div>
  247. <div style="width: 100%; border-bottom: 1px solid #e5e5e5;"></div>
  248. <div class="detail-item">
  249. <div class="detail-label">职位描述</div>
  250. <div class="detail-value html-content" v-if="!isEditingDescription" v-html="positionData.description"></div>
  251. <el-button type="text" class="edit-title-btn" @click="startEditDescription" v-if="!isEditingDescription">
  252. <el-icon>
  253. <Edit />
  254. </el-icon>
  255. </el-button>
  256. <div class="title-edit-container" v-else>
  257. <el-input v-model="editingDescription" type="textarea" rows="6" placeholder="请输入职位描述" maxlength="1000"
  258. show-word-limit />
  259. <div class="title-edit-actions">
  260. <el-button size="small" @click="cancelEditDescription">取消</el-button>
  261. <el-button size="small" type="primary" @click="saveDescription">保存</el-button>
  262. </div>
  263. </div>
  264. </div>
  265. <!-- 在 detail-item 列表中添加能力标签部分 -->
  266. <div class="detail-item">
  267. <div class="detail-label">能力标签</div>
  268. <div class="detail-value" v-if="!isEditingCompetency">
  269. <el-tag v-for="tag in positionData.competency_tags" :key="tag.id" effect="plain" class="competency-tag">
  270. {{ tag.name }}
  271. </el-tag>
  272. <el-button type="text" class="edit-title-btn" @click="startEditCompetency">
  273. <el-icon>
  274. <Edit />
  275. </el-icon>
  276. </el-button>
  277. </div>
  278. <div class="title-edit-container" v-else>
  279. <el-select v-model="selectedCompetencies" multiple filterable placeholder="请选择能力标签" style="width: 100%">
  280. <el-option v-for="tag in competencyTagOptions" :key="tag.id" :label="tag.name" :value="tag.id">
  281. </el-option>
  282. </el-select>
  283. <div class="title-edit-actions">
  284. <el-button size="small" @click="cancelEditCompetency">取消</el-button>
  285. <el-button size="small" type="primary" @click="saveCompetency">保存</el-button>
  286. </div>
  287. </div>
  288. </div>
  289. </el-card>
  290. <!-- 右侧状态信息 -->
  291. <div class="status-cards">
  292. <el-card class="status-card">
  293. <div class="section-title">
  294. <div class="section-line"></div>
  295. <span>职位状态</span>
  296. </div>
  297. <div class="status-switch">
  298. <el-switch v-model="positionStatus" inactive-text="" @change="handleStatusChange" />
  299. <span class="status-text">{{ positionStatus ? '已启用' : '待启用' }}</span>
  300. </div>
  301. </el-card>
  302. <el-card class="status-card">
  303. <div class="section-title">
  304. <div class="section-line"></div>
  305. <span>职位性质</span>
  306. <el-button type="text" class="edit-btn" @click="startEditJobType" v-if="!isEditingJobType">编辑</el-button>
  307. </div>
  308. <div class="usage-content">
  309. <!-- <div class="usage-label">职位用途</div> -->
  310. <div class="usage-value" v-if="!isEditingJobType">{{ getJobTypeText(positionData.job_type) }}</div>
  311. <!-- 编辑模式 -->
  312. <div v-if="isEditingJobType" class="title-edit-container">
  313. <el-select v-model="editingJobType" placeholder="请选择职位性质" style="width: 100%;">
  314. <el-option label="全职" :value="0"></el-option>
  315. <el-option label="兼职" :value="1"></el-option>
  316. <el-option label="实习" :value="2"></el-option>
  317. <el-option label="其他" :value="3"></el-option>
  318. </el-select>
  319. <div class="title-edit-actions">
  320. <el-button size="small" @click="cancelEditJobType">取消</el-button>
  321. <el-button size="small" type="primary" @click="saveEditJobType">保存</el-button>
  322. </div>
  323. </div>
  324. </div>
  325. </el-card>
  326. <el-card class="status-card">
  327. <div class="section-title">
  328. <div class="section-line"></div>
  329. <span>招聘流程</span>
  330. <el-button type="text" class="edit-btn" @click="handleEditProcess">设置流程</el-button>
  331. </div>
  332. <div class="process-steps">
  333. <div v-for="(step, index) in recruitmentProcess" :key="step.id" class="process-step"
  334. :class="{ 'disabled': !step.active }" v-show="step.visible">
  335. <div class="step-number" v-if="step.name != '已通过' && step.name != '已淘汰'">{{ getVisibleStepNumber(index) }}
  336. </div>
  337. <div class="step-content">
  338. <div class="step-title">{{ step.name }}</div>
  339. <!-- <div class="step-desc">{{ step.description }}</div> -->
  340. </div>
  341. </div>
  342. </div>
  343. </el-card>
  344. <el-card class="status-card">
  345. <div class="section-title">
  346. <div class="section-line"></div>
  347. <span>面试设置</span>
  348. <el-button type="text" class="edit-btn" @click="handleEditInterview">编辑</el-button>
  349. </div>
  350. </el-card>
  351. </div>
  352. </div>
  353. <!-- 添加招聘流程编辑对话框 -->
  354. <el-dialog title="设置招聘流程" v-model="showProcessDialog" width="500px" :close-on-click-modal="false">
  355. <div class="process-dialog-content">
  356. <div v-for="(step, index) in recruitmentProcess" :key="step.id" v-show="step.visible">
  357. <!-- 添加在每个步骤上方的加号按钮,最后两个步骤不显示加号 -->
  358. <div class="add-process-btn-top" v-if="index < recruitmentProcess.length - 1">
  359. <el-button type="text" @click="showStepOptions(index, $event)">
  360. <el-icon>
  361. <Plus />
  362. </el-icon>
  363. </el-button>
  364. </div>
  365. <div class="process-edit-item" :class="{ 'highlight': step.active }">
  366. <div class="process-number" v-if="step.name != '已通过' && step.name != '已淘汰'">{{ getVisibleStepNumber(index) }}
  367. </div>
  368. <div class="process-edit-content">
  369. <div>{{ step.name }}</div>
  370. <!-- <div class="process-edit-desc">{{ step.description }}</div> -->
  371. </div><!-- v-if="step.name=='AI考察'" -->
  372. <div class="process-edit-btn" v-if="step.name != '已通过' && step.name != '已淘汰'">
  373. <el-button type="info" size="small" plain v-if="step.name != '简历收集' && step.name != '肢体检测'"
  374. @click="editProcessStep(step, index)">
  375. 修改
  376. </el-button>
  377. <el-button type="danger" size="small" plain style="margin-top: 10px;margin-left: 0px;"
  378. v-if="step.name != '资料收集' && step.name != 'AI考察'" @click="deleteProcessStep(step, index)">
  379. 删除
  380. </el-button>
  381. </div>
  382. </div>
  383. </div>
  384. <!-- 最后一个步骤下方的加号按钮 -->
  385. <div class="add-process-btn">
  386. <!-- <el-button type="text" @click="showStepOptions(recruitmentProcess.length, $event)">
  387. <el-icon><Plus /></el-icon>
  388. </el-button> -->
  389. </div>
  390. </div>
  391. <!-- 添加选项菜单 -->
  392. <div v-if="showOptionsMenu" class="process-options-menu"
  393. :style="{ top: menuPosition.top, left: menuPosition.left }" @click.stop>
  394. <div class="close-btn" @click="showOptionsMenu = false">
  395. <el-icon>
  396. <Close />
  397. </el-icon>
  398. </div>
  399. <div v-for="option in processStepOptions" :key="option.value" class="process-option-item"
  400. @click="addSelectedStepType(option.value, option.label)">
  401. {{ option.label }}
  402. </div>
  403. </div>
  404. <template #footer>
  405. <span class="dialog-footer">
  406. <el-button @click="cancelEditProcess">取消</el-button>
  407. <el-button type="primary" @click="saveProcess">确定</el-button>
  408. </span>
  409. </template>
  410. </el-dialog>
  411. <!-- AI考察配置对话框 -->
  412. <el-drawer title="AI考察" v-model="showAIVideoDialog" direction="rtl" size="60%" :close-on-click-modal="false"
  413. class="ai-video-drawer" :destroy-on-close="false">
  414. <!-- 步骤指示器 -->
  415. <div class="ai-video-steps">
  416. <div class="step-item" :class="{ 'active': currentStep === 1 }" @click="jumpToStep(1)">
  417. <div class="step-number">1</div>
  418. <div class="step-title" :class="{ 'active-title': currentStep === 1 }">考察能力</div>
  419. </div>
  420. <div class="step-line" :class="{ 'active-line': currentStep >= 2 }"></div>
  421. <div class="step-item" :class="{ 'active': currentStep === 2 }" @click="jumpToStep(2)">
  422. <div class="step-number">2</div>
  423. <div class="step-title" :class="{ 'active-title': currentStep === 2 }">面试题目</div>
  424. </div>
  425. <div class="step-line" :class="{ 'active-line': currentStep >= 3 }"></div>
  426. <div class="step-item" :class="{ 'active': currentStep === 3 }" @click="jumpToStep(3)">
  427. <div class="step-number">3</div>
  428. <div class="step-title" :class="{ 'active-title': currentStep === 3 }">系统设置</div>
  429. </div>
  430. </div>
  431. <!-- 步骤1:考察能力 -->
  432. <div v-if="currentStep === 1" class="ai-video-content" v-loading="competencyLoading"
  433. element-loading-text="正在生成能力标签...">
  434. <div class="ai-video-options">
  435. <div class="ai-video-options-header">
  436. <div class="ai-video-options-header-title">
  437. <span>AI根据岗位信息生成的能力,创建完步骤后,会自动将选择的胜任力考核维度加入到该职位的考察能力列表中。</span>
  438. </div>
  439. <!-- <div class="right_btn">
  440. <el-button type="primary" class="custom-btn" plain size="small" @click="handleAddCompetency">重新生成</el-button>
  441. </div> -->
  442. </div>
  443. <!-- <p class="option-tip">AI根据岗位信息生成的能力,创建完步骤后,会自动将选择的新智能能力加入到该职位的考察能力列表中。</p> -->
  444. <!-- <div class="new-feature-tip">
  445. 下面是奇才智能算法推荐的胜任力考核维度,请结合您的需求增删,确认后点击「下一步」出题
  446. <span class="new-tag">NEW</span>
  447. AI根据岗位信息生成的能力,创建完步骤后,会自动将选择的新智能能力加入到该职位的考察能力列表中。
  448. </div> -->
  449. <!-- 推荐的考核维度 -->
  450. <div class="recommended-dimensions">
  451. <!-- <h3>推荐考核维度</h3> -->
  452. <div class="dimension-grid">
  453. <el-checkbox v-for="item in competencyTags.slice(0, 10)" :key="item.id" v-model="item.selected"
  454. @change="(val: boolean) => handleCompetencyChange(item, val)" class="custom-checkbox">
  455. <div class="ability-label">
  456. <span>{{ item.name }}</span>
  457. <img src="../../../assets/sheng.png" alt="" class="sheng">
  458. </div>
  459. </el-checkbox>
  460. </div>
  461. </div>
  462. <!-- 备选考核维度 -->
  463. <div class="optional-dimensions">
  464. <div class="header-row">
  465. <h3 style="color: #808080;">备选考核维度</h3>
  466. <div class="right_btn">
  467. <el-button type="primary" class="custom-btn" plain size="small"
  468. @click="handleAddCompetency">重新生成</el-button>
  469. </div>
  470. </div>
  471. <div class="dimension-grid">
  472. <el-checkbox v-for="item in competencyTags.slice(10, 50)" :key="item.id" v-model="item.selected"
  473. @change="(val: boolean) => handleCompetencyChange(item, val)" class="custom-checkbox">
  474. <div class="ability-label">
  475. <span>{{ item.name }}</span>
  476. <!-- <span v-if="item.isCustom" class="custom-tag">自定义</span> -->
  477. <!-- <el-tooltip content="考察候选人的XXXX能力" placement="top">
  478. <el-icon><QuestionFilled /></el-icon>
  479. </el-tooltip> -->
  480. </div>
  481. </el-checkbox>
  482. <!-- 显示自定义维度 -->
  483. <!-- <div
  484. v-for="item in competencyTags.filter(tag => tag.isCustom)"
  485. :key="'custom-' + item.id"
  486. class="custom-dimension-item">
  487. <el-checkbox
  488. v-model="item.selected"
  489. @change="(val: boolean) => handleCompetencyChange(item, val)"
  490. class="custom-checkbox">
  491. <div class="ability-label">
  492. <span>{{ item.name }}</span>
  493. <span class="custom-tag">自定义</span>
  494. <el-tooltip :content="item.description" placement="top" v-if="item.description">
  495. <el-icon><QuestionFilled /></el-icon>
  496. </el-tooltip>
  497. </div>
  498. </el-checkbox>
  499. <el-button
  500. type="text"
  501. size="small"
  502. class="delete-custom-btn"
  503. @click="deleteCustomDimension(item)"
  504. title="删除自定义维度">
  505. <el-icon><Close /></el-icon>
  506. </el-button>
  507. </div> -->
  508. </div>
  509. </div>
  510. <!-- 自定义维度 -->
  511. <div class="custom-dimensions">
  512. <h3 style="color: #808080;">自定义维度</h3>
  513. <div class="dimension-form">
  514. <div class="dimension-row">
  515. <div class="dimension-input">
  516. <!-- <div class="input-label">维度名称:</div> -->
  517. <el-input style="width: 150px;" v-model="customDimension" placeholder="请输入维度名称" />
  518. </div>
  519. <div class="dimension-input">
  520. <div class="input-label">特征描述:</div>
  521. <el-input v-model="customDescription" style="width: 500px;" type="textarea" placeholder="请输入特征描述" />
  522. </div>
  523. </div>
  524. <div class="dimension-actions">
  525. <!-- <el-checkbox v-model="continueAdd" class="custom-checkbox">
  526. <div class="ability-label">
  527. <span>继续新增</span>
  528. </div>
  529. </el-checkbox> -->
  530. <el-button type="primary" @click="addDimension">添加</el-button>
  531. </div>
  532. </div>
  533. </div>
  534. </div>
  535. </div>
  536. <!-- 步骤2:面试题目 -->
  537. <div v-if="currentStep === 2" class="ai-video-content">
  538. <div class="interview-settings">
  539. <div class="interview-header">
  540. <div class="interview-actions">
  541. <el-button plain class="custom-btn" @click="handleImportQuestion"><!-- @click="handleAddQuestion" -->
  542. <el-icon>
  543. <Plus />
  544. </el-icon>
  545. 自定义题目
  546. </el-button>
  547. <el-button plain class="custom-btn">
  548. <el-icon>
  549. <Document />
  550. </el-icon>
  551. 导入题目
  552. </el-button>
  553. <el-button v-if="chatQuestions.length > 0" type="primary" plain class="custom-btn"
  554. :loading="autoGenerateLoading" @click="handleAutoGenerate">
  555. <el-icon>
  556. <Refresh />
  557. </el-icon>
  558. 重新生成
  559. </el-button>
  560. <!-- 添加得分题和识图题开关 -->
  561. <div class="question-switches">
  562. <!-- 添加题目数量控制 -->
  563. <div class="switch-item">
  564. <span class="switch-label">开放题数量</span>
  565. <el-input-number v-model="questionCountSettings.openQuestionsCount" :min="1" :max="10" size="small"
  566. @change="handleQuestionCountChange" />
  567. </div>
  568. <div class="switch-item">
  569. <span class="switch-label">选择题数量</span>
  570. <el-input-number v-model="questionCountSettings.choiceQuestionsCount" :min="1" :max="20" size="small"
  571. @change="handleQuestionCountChange" />
  572. </div>
  573. <!-- <div class="switch-item">
  574. <span class="switch-label">得分题</span>
  575. <el-switch
  576. style="margin-top:0px;"
  577. v-model="questionSettings.enableScoreQuestions"
  578. @change="handleScoreQuestionsChange"
  579. />
  580. </div>
  581. <div class="switch-item">
  582. <span class="switch-label">识图题</span>
  583. <el-switch
  584. style="margin-top:0px;"
  585. v-model="questionSettings.enableColorBlindQuestions"
  586. @change="handleColorBlindQuestionsChange"
  587. />
  588. </div> -->
  589. </div>
  590. </div>
  591. <div class="interview-duration">
  592. <span>预计面试时长: </span>
  593. <span class="duration-value">{{ formatDuration }}</span>
  594. <span class="duration-tip">拖动试题卡片可改变试题顺序</span>
  595. </div>
  596. </div>
  597. <!-- <div class="interview-mode">
  598. <span>面试问答方式: </span>
  599. <span class="mode-value">传统读题模式</span>
  600. <el-button type="primary" plain size="small" class="customize-btn">定制面试间</el-button>
  601. </div> -->
  602. <div class="interview-content">
  603. <div class="interview-questions-container" v-loading="autoGenerateLoading"
  604. element-loading-text="AI正在生成题目...">
  605. <div class="no-questions" v-if="chatQuestions.length === 0">
  606. <p>暂无题目</p>
  607. <p class="ai-tip">点击"AI自动组题"按钮</p>
  608. <p class="ai-tip">可根据您选择的能力类型一键组题</p>
  609. <el-button type="success" class="auto-generate-btn" :loading="autoGenerateLoading"
  610. @click="handleAutoGenerate">
  611. AI自动组题
  612. </el-button>
  613. <el-button v-if="autoGenerateLoading && pollingController" type="warning" plain size="small"
  614. @click="handleCancelGenerate" style="margin-left: 10px;">
  615. 取消生成
  616. </el-button>
  617. </div>
  618. <!-- :component-data="{
  619. tag: 'transition-group',
  620. name: !drag ? 'flip-list' : null
  621. }" -->
  622. <div class="question-list" v-else>
  623. <draggable v-model="chatQuestions" item-key="id" handle=".drag-handle" @start="handleDragStart"
  624. @end="handleDragEnd" animation="200" ghost-class="sortable-ghost" chosen-class="sortable-chosen"
  625. drag-class="sortable-drag">
  626. <template #item="{ element }">
  627. <div class="question-card">
  628. <div class="drag-handle">
  629. <img src="../../../assets/dorp.png" alt="" class="dorp">
  630. </div>
  631. <div class="question-header">
  632. <div class="question-title">
  633. <div class="title-content">
  634. <div class="question-content">
  635. <span class="question-number">题目{{ chatQuestions.indexOf(element) + 1 }}</span>
  636. <span><span style="color: #0017ab;">{{ '「' + element.question_form_name + '」' }}</span>{{
  637. element.title }}</span>
  638. </div>
  639. <div class="question-tags">
  640. <el-tag
  641. v-for="(tag, index) in (element.competency_tag_info || element.competency_tags || []).slice(0, 2)"
  642. :key="tag.id" effect="plain" class="competency-tag" size="small">
  643. {{ getTagNames(tag) }}
  644. </el-tag>
  645. <span v-if="(element.competency_tag_info || element.competency_tags || []).length > 2"
  646. class="more-tags-indicator">
  647. +{{ (element.competency_tag_info || element.competency_tags || []).length - 2 }}
  648. </span>
  649. </div>
  650. </div>
  651. </div>
  652. <div class="question-actions" style="margin-top: 0px;">
  653. <el-button type="text" @click="editQuestion(chatQuestions.indexOf(element))">
  654. <el-icon>
  655. <Edit />
  656. </el-icon>
  657. </el-button>
  658. <el-button type="text" @click="deleteQuestion(chatQuestions.indexOf(element))">
  659. <el-icon>
  660. <Delete />
  661. </el-icon>
  662. </el-button>
  663. </div>
  664. </div>
  665. <div class="question-info">
  666. <div class="info-item">
  667. <!-- <span class="info-label">考察能力:</span> -->
  668. <span>{{ element.scoring_reference }}</span>
  669. </div>
  670. <!-- <div class="info-item">
  671. <span class="info-label">面试内容:</span>
  672. <span>{{ element.content }}</span>
  673. </div>
  674. <div class="info-item">
  675. <span class="info-label">对话目标:</span>
  676. <span>{{ element.target }}</span>
  677. </div> -->
  678. </div>
  679. <div class="question-actions" v-if="element.question_form == 0">
  680. <!-- <div class="weight-control">
  681. <span class="weight-label">权重占比:</span>
  682. <el-input-number
  683. v-model="element.weight"
  684. :min="0"
  685. :max="100"
  686. :step="1"
  687. size="small"
  688. controls-position="right"
  689. >
  690. <template #suffix>%</template>
  691. </el-input-number>
  692. </div> -->
  693. <div class="answer-limit">
  694. <el-switch style="margin-top:0px; " v-model="element.enable_follow_up"
  695. @change="handleFollowUpChange" />
  696. <span class="answer-label">回答不充分追问</span>
  697. </div>
  698. <div class="answer-limit">
  699. <span class="answer-label">追问轮数</span>
  700. <el-select v-model="element.maxAnswers" size="small" placeholder="请选择">
  701. <el-option label="1次" :value="1" />
  702. <!-- <el-option label="2次" :value="2" />
  703. <el-option label="3次" :value="3" /> -->
  704. </el-select>
  705. </div>
  706. </div>
  707. <div class="question-actions" v-if="element.question_form == 1">
  708. <div class="answer-limit">
  709. <span class="answer-label">是否为红线题:</span>
  710. <el-select v-model="element.is_required_correct" :disabled="true"
  711. @change="handleIsRequiredCorrectChange(element.id, element.is_required_correct)" size="small"
  712. placeholder="请选择">
  713. <el-option label="是" :value="true" />
  714. <el-option label="否" :value="false" />
  715. </el-select>
  716. </div>
  717. </div>
  718. </div>
  719. </template>
  720. </draggable>
  721. </div>
  722. </div>
  723. <div class="multi-dimension-questions">
  724. <div class="dimension-title">已选胜任力考核维度</div>
  725. <div class="dimension-list">
  726. <div v-for="(ability, index) in selectedAbilities" :key="ability.value" class="dimension-item">
  727. <div class="dimension-header">
  728. <div class="dimension-info">
  729. <span class="ability-name">{{ ability.label }}</span>
  730. <img src="../../../assets/sheng.png" alt="" class="sheng">
  731. </div>
  732. </div>
  733. <div class="dimension-content" @click="removeSelectedAbility(index)" title="点击删除此胜任力标签">
  734. <div class="ability-description">{{ "—" }}</div>
  735. <!-- <el-icon class="delete-icon"><Delete /></el-icon> -->
  736. </div>
  737. </div>
  738. </div>
  739. </div>
  740. </div>
  741. </div>
  742. </div>
  743. <!-- 步骤3:系统设置 -->
  744. <div v-if="currentStep === 3" class="ai-video-content">
  745. <div class="system-settings">
  746. <!-- 面试官形象设置 -->
  747. <div class="setting-section">
  748. <div class="section-header">请选择面试官形象</div>
  749. <div style=" display: flex;align-items: flex-start;">
  750. <div
  751. style="width: 100px;height: 170px;background-color: #f2f2f2; margin-right: 30px;border-radius: 10px;overflow: hidden;">
  752. <img v-if="interviewerAvatar" :src="interviewerAvatar" alt=""
  753. style="width: 100%;height: 100%;object-fit: cover;transform: translateZ(0);backface-visibility: hidden;">
  754. </div>
  755. <div>
  756. <div class="interviewer-avatars">
  757. <div v-for="avatar in interviewerAvatars" :key="avatar.id" class="avatar-item"
  758. :class="{ active: selectedInterviewer.id === avatar.id }" @click="handleInterviewerSelect(avatar)">
  759. <div class="avatar-wrapper">
  760. <div class="avatar-circle" :class="{ selected: selectedInterviewer.id === avatar.id }">
  761. <img :src="avatar.avatar_url || avatar.image" :alt="avatar.name" />
  762. </div>
  763. <div class="avatar-name" :title="avatar.name">{{ avatar.name }}</div>
  764. </div>
  765. </div>
  766. </div>
  767. <div class="setting-item" style="border: none;">
  768. <div class="setting-label">「追问风格」</div>
  769. <div class="setting-options">
  770. <el-select v-model="settings.followUpStyle" placeholder="请选择"
  771. @change="handleSettingChange('followUpStyle', settings.followUpStyle)">
  772. <el-option label="温和" value="1" />
  773. <el-option label="严厉" value="2" />
  774. <el-option label="严谨" value="3" />
  775. </el-select>
  776. </div>
  777. <div class="setting-desc"></div>
  778. </div>
  779. </div>
  780. </div>
  781. <!-- 面试设置选项 -->
  782. <div class="interview-settings">
  783. <div class="setting-item">
  784. <div class="setting-label">
  785. <p>「回答视频保留」</p>
  786. </div>
  787. <div class="setting-desc">
  788. <p>选取后,将在报告中展示候选人的回答视频</p>
  789. </div>
  790. <el-switch v-model="settings.keepVideo"
  791. @change="(value: boolean) => handleSettingChange('keepVideo', value)" :disabled="true" />
  792. </div>
  793. <div class="setting-item">
  794. <div class="setting-label">
  795. <p>「中断后作答限制」</p>
  796. </div>
  797. <div class="setting-options">
  798. <el-radio v-model="settings.interruptionMode" label="current"
  799. @change="handleSettingChange('interruptionMode', settings.interruptionMode)">
  800. 从当前问题重新作答
  801. </el-radio>
  802. <el-radio v-model="settings.interruptionMode" label="previous"
  803. @change="handleSettingChange('interruptionMode', settings.interruptionMode)">
  804. 从头作答
  805. </el-radio>
  806. </div>
  807. <div class="setting-desc" style="width: 40px;"></div>
  808. </div>
  809. <div class="setting-item">
  810. <div class="setting-label">
  811. <p>「智慧追问」</p>
  812. </div>
  813. <div class="setting-desc">
  814. <p>追问将结合候选人资料情况,综合分析发问</p>
  815. </div>
  816. <el-switch v-model="settings.smartFollowUp"
  817. @change="(value: boolean) => handleSettingChange('smartFollowUp', value)" />
  818. </div>
  819. </div>
  820. </div>
  821. </div>
  822. </div>
  823. <template #footer>
  824. <div class="drawer-footer">
  825. <el-button v-if="currentStep > 1" size="small" @click="prevStep">上一步</el-button>
  826. <el-button @click="cancelAIVideoConfig" size="small">取消</el-button>
  827. <el-button v-if="currentStep < 3" type="primary" :loading="currentStep==1? competencyLoading : autoGenerateLoading" size="small" @click="nextStep"
  828. style="background-color: #0419a3;border-color: #0419a3;">下一步</el-button>
  829. <el-button v-else type="success" @click="finishConfig" size="small"
  830. style="background-color: #0419a3;border-color: #0419a3;">新增</el-button>
  831. </div>
  832. </template>
  833. </el-drawer>
  834. <!-- 在 template 中添加视频宣讲弹窗 -->
  835. <el-dialog :title="videoLectureDialogTitle" v-model="showVideoLectureDialog" width="35%" :close-on-click-modal="false"
  836. class="video-lecture-dialog">
  837. <div class="video-lecture-form">
  838. <!-- <div class="form-item">
  839. <div class="form-label">步骤名称</div>
  840. <el-input
  841. v-model="videoLectureForm.title"
  842. placeholder="请输入步骤名称"
  843. maxlength="30"
  844. show-word-limit
  845. />
  846. </div> -->
  847. <div class="form-item">
  848. <div class="form-label">上传视频</div>
  849. <el-upload
  850. class="video-uploader"
  851. :action="uploadUrl"
  852. :auto-upload="true"
  853. :show-file-list="false"
  854. accept="video/*"
  855. :data="{ tenant_id: Session.get('tenant_id') }"
  856. :headers="{ Authorization: `JWT ${Session.get('token')}` }"
  857. :before-upload="beforeVideoUpload"
  858. :on-success="handleVideoUploadSuccess"
  859. :on-error="handleVideoUploadError"
  860. >
  861. <div class="upload-area">
  862. <template v-if="videoLectureForm.video_url">
  863. <video :src="videoLectureForm.video_url" style="width:100%;height:180px; border-radius: 6px; " controls />
  864. </template>
  865. <template v-else>
  866. <el-icon>
  867. <Plus />
  868. </el-icon>
  869. <div class="upload-text">上传文件大小不超过500M</div>
  870. </template>
  871. </div>
  872. </el-upload>
  873. </div>
  874. <div class="form-item">
  875. <div class="form-label">视频简介</div>
  876. <el-input v-model="videoLectureForm.description" type="textarea" rows="4" placeholder="视频简介会显示在视频下方的标题下方"
  877. maxlength="200" show-word-limit />
  878. </div>
  879. <!-- <div class="form-item">
  880. <div class="form-label">播放设置</div>
  881. <el-radio-group v-model="videoLectureForm.broadcastType">
  882. <el-radio label="required">必须看完</el-radio>
  883. <el-radio label="optional">看过即可</el-radio>
  884. </el-radio-group>
  885. </div> -->
  886. </div>
  887. <template #footer>
  888. <span class="dialog-footer">
  889. <el-button @click="cancelVideoLecture">取消</el-button>
  890. <el-button type="primary" @click="confirmVideoLecture">确认</el-button>
  891. </span>
  892. </template>
  893. </el-dialog>
  894. <!-- AI实时对话抽屉 -->
  895. <el-drawer title="AI实时对话" v-model="showAIChatDialog" direction="rtl" size="60%" :close-on-click-modal="false"
  896. class="ai-chat-drawer" :destroy-on-close="false">
  897. <div class="ai-chat-content">
  898. <!-- AI实时对话的配置内容 -->
  899. <div class="chat-settings">
  900. <div class="section-title">
  901. <div class="section-line"></div>
  902. <span>对话设置</span>
  903. </div>
  904. <!-- 添加按钮组 -->
  905. <div class="chat-actions">
  906. <el-button type="primary" plain @click="handleAddQuestion">
  907. <el-icon>
  908. <Plus />
  909. </el-icon>
  910. 添加问题
  911. </el-button>
  912. <el-button type="success" plain @click="handleImportQuestion">
  913. <el-icon>
  914. <Document />
  915. </el-icon>
  916. 导入问题
  917. </el-button>
  918. </div>
  919. <!-- 添加问题列表容器 -->
  920. <div class="chat-questions">
  921. <div v-if="chatQuestions.length === 0" class="empty-questions">
  922. <p>暂无问题</p>
  923. <p class="tip-text">点击"添加问题"或"导入问题"添加面试问题</p>
  924. </div>
  925. <div v-else class="question-list">
  926. <draggable v-model="chatQuestions" item-key="id" handle=".drag-handle">
  927. <template #item="{ element, index }">
  928. {{ element }}
  929. <div class="question-card">
  930. <div class="question-header">
  931. <div class="question-title">
  932. <span class="question-number">题目{{ index + 1 }}</span>
  933. <span>{{ element.question }}</span>
  934. </div>
  935. <div class="question-actions">
  936. <div class="weight-control">
  937. <span class="weight-label">权重占比:</span>
  938. <el-input-number v-model="element.weight" :min="0" :max="100" :step="1" size="small"
  939. controls-position="right">
  940. <template #suffix>%</template>
  941. </el-input-number>
  942. </div>
  943. <div class="answer-limit">
  944. <span class="answer-label">最多回答次数</span>
  945. <el-select v-model="element.maxAnswers" size="small" placeholder="请选择">
  946. <el-option label="1次" :value="1" />
  947. <el-option label="2次" :value="2" />
  948. <el-option label="3次" :value="3" />
  949. </el-select>
  950. </div>
  951. <el-button type="text" @click="editQuestion(index)">
  952. <el-icon>
  953. <Edit />
  954. </el-icon>
  955. </el-button>
  956. <el-button type="text" @click="deleteQuestion(index)">
  957. <el-icon>
  958. <Delete />
  959. </el-icon>
  960. </el-button>
  961. </div>
  962. </div>
  963. <div class="question-info">
  964. <div class="info-item">
  965. <span class="info-label">考察能力:</span>
  966. <span>{{ element.ability }}</span>
  967. </div>
  968. <div class="info-item">
  969. <span class="info-label">面试内容:</span>
  970. <span>{{ element.content }}</span>
  971. </div>
  972. <div class="info-item">
  973. <span class="info-label">对话目标:</span>
  974. <span>{{ element.target }}</span>
  975. </div>
  976. </div>
  977. </div>
  978. </template>
  979. </draggable>
  980. </div>
  981. </div>
  982. </div>
  983. </div>
  984. <template #footer>
  985. <div class="drawer-footer">
  986. <el-button @click="cancelAIChatConfig">取消</el-button>
  987. <el-button type="primary" @click="handleAIChatConfig">确认</el-button>
  988. </div>
  989. </template>
  990. </el-drawer>
  991. <!-- 在 template 中添加问题编辑弹框 -->
  992. <el-dialog title="查看问题" v-model="showQuestionDialog" width="500px" :close-on-click-modal="false"
  993. class="question-dialog">
  994. <div class="question-form">
  995. <div class="form-item">
  996. <div class="form-label">
  997. 题目内容<span class="required">*</span>
  998. </div>
  999. <el-input v-model="questionForm.content" type="textarea" rows="4"
  1000. placeholder="请描述一次你如何在紧迫的时间内完成一个复杂的后端开发任务的经历" maxlength="300" show-word-limit :disabled="true" />
  1001. </div>
  1002. <div class="form-item">
  1003. <div class="form-label">胜任力标签</div>
  1004. <el-select v-model="competencyTagsValue" placeholder="请选择胜任力标签" class="full-width" multiple :disabled="true">
  1005. <el-option v-for="tag in questionForm.competency_tags" :key="tag.id" :label="tag.name" :value="tag.id" />
  1006. </el-select>
  1007. <div class="form-tip">选择胜任力标签和胜任力标签,可多选</div>
  1008. </div>
  1009. <div class="form-item">
  1010. <div class="form-label">
  1011. 胜任力配置
  1012. </div>
  1013. <div class="competency-config">
  1014. <div class="config-row">
  1015. <span class="config-label">标签</span>
  1016. <span class="config-label">权重</span>
  1017. <!-- <el-button
  1018. type="text"
  1019. size="small"
  1020. @click="distributeWeightsEvenly"
  1021. style="color: #409eff; font-size: 12px;"
  1022. >
  1023. 平均分配
  1024. </el-button> -->
  1025. <!-- <span class="config-label">级别</span> -->
  1026. </div>
  1027. <div class="config-row" v-for="(tag, index) in questionForm.competency_tags" :key="tag.id">
  1028. <span class="tag-name">{{ tag.name }}</span>
  1029. <el-input-number v-model="tag.weight" :min="0" :max="100" :step="1" :disabled="true"
  1030. controls-position="right" @change="(value) => handleWeightChange(index, value)" /> %
  1031. <!-- <el-input-number v-model="tag.importance" :min="1" :max="5" :step="1" controls-position="right" /> -->
  1032. </div>
  1033. </div>
  1034. <div class="form-tip">
  1035. 配置各个胜任力标签的权重,总和需要100%
  1036. <span class="weight-sum" :class="{ 'weight-error': getTotalWeight() !== 100 }">
  1037. (当前总计: {{ getTotalWeight() }}%)
  1038. </span>
  1039. <br />
  1040. <el-icon style="color: #409eff; margin-right: 4px; margin-top: 4px;">
  1041. <InfoFilled />
  1042. </el-icon>
  1043. <span style="color: #909399; font-size: 11px;">
  1044. 修改任意权重时,其他权重会自动按比例调整以保持总和为100%。点击"平均分配"可快速平均分配所有权重。
  1045. </span>
  1046. </div>
  1047. </div>
  1048. <div class="form-item">
  1049. <div class="form-label">题目形式</div>
  1050. <el-select v-model="questionForm.question_form" placeholder="开放问题" class="full-width" :disabled="true"
  1051. @change="handleQuestionTypeChange">
  1052. <el-option label="开放问题" :value="0" />
  1053. <el-option label="单选题" :value="1" />
  1054. <el-option label="识图题" :value="3" />
  1055. <el-option label="多选题" :value="2" />
  1056. <el-option label="填空题" :value="6" />
  1057. <el-option label="得分题" :value="4" />
  1058. <el-option label="追加型开放问题" :value="5" />
  1059. </el-select>
  1060. <div class="form-tip">选择题目的形式:开放问题、单选题、多选题、填空题、自言语</div>
  1061. </div>
  1062. <!-- :disabled="true" -->
  1063. <div class="form-item">
  1064. <div class="form-label">适用职位</div>
  1065. <el-select v-model="positionId" placeholder="请选择适用职位类型" class="full-width" :disabled="true">
  1066. <el-option v-for="pos in positionOptions" :key="pos.value" :label="pos.label" :value="pos.value" />
  1067. </el-select>
  1068. <div class="form-tip">选择题目适用的职位类型,可多选</div>
  1069. </div>
  1070. <div class="form-item" v-if="questionForm.question_form === 1 ||
  1071. questionForm.question_form === 2 || questionForm.question_form === 3">
  1072. <div class="form-label">选项</div>
  1073. <div>
  1074. <div class="option-header" style="display: flex; margin-bottom: 10px; font-weight: bold;">
  1075. <span style="flex: 1;">选项内容</span>
  1076. <span style="width: 80px; text-align: center;">是否正确</span>
  1077. </div>
  1078. <div v-for="(option, index) in questionForm.options" :key="index" class="option-item"
  1079. style="display: flex; align-items: center; margin-bottom: 10px;">
  1080. <el-input v-model="option.text" placeholder="请输入选项内容" :disabled="true"
  1081. style="flex: 1; margin-right: 10px;" />
  1082. <el-tooltip content="设置为正确答案" placement="top" effect="light" :disabled="true">
  1083. <div @click="() => {
  1084. if (questionForm.question_form === 1 || questionForm.question_form === 3) {
  1085. // 单选题:只能有一个正确答案
  1086. questionForm.options.forEach((item, idx) => {
  1087. item.is_correct = (idx === index);
  1088. });
  1089. } else {
  1090. // 多选题:可以有多个正确答案
  1091. option.is_correct = !option.is_correct;
  1092. }
  1093. }" style="cursor: pointer; width: 80px; text-align: center;">
  1094. <template v-if="questionForm.question_form === 1 || questionForm.question_form === 3">
  1095. <!-- 单选题使用单选按钮 -->
  1096. <el-radio :disabled="true" v-model="option.is_correct" :label="true" />
  1097. </template>
  1098. <template v-else>
  1099. <!-- 多选题使用复选框 -->
  1100. <el-checkbox :disabled="true" v-model="option.is_correct" />
  1101. </template>
  1102. </div>
  1103. </el-tooltip>
  1104. </div>
  1105. <!-- <div style="display: flex; justify-content: space-between; margin-top: 10px;">
  1106. <el-button
  1107. type="primary"
  1108. :disabled="true"
  1109. @click="() => {
  1110. // 确保options数组已初始化
  1111. if (!questionForm.options) {
  1112. questionForm.options = [];
  1113. }
  1114. // 添加新选项时自动设置排序值
  1115. const sort = questionForm.options.length > 0 ?
  1116. Math.max(...questionForm.options.map(o => o.sort || 0)) + 1 : 1;
  1117. questionForm.options.push({ option_text: '', is_correct: false, sort });
  1118. }"
  1119. >
  1120. 添加选项
  1121. </el-button>
  1122. <el-button
  1123. v-if="questionForm.options && questionForm.options.length > 2"
  1124. type="danger"
  1125. :disabled="true"
  1126. @click="() => {
  1127. questionForm.options.pop();
  1128. }"
  1129. >
  1130. 删除最后一项
  1131. </el-button>
  1132. </div> -->
  1133. </div>
  1134. <div class="form-tip">添加选项并设置正确答案,单选题只能有一个正确答案,多选题可以有多个正确答案</div>
  1135. </div>
  1136. <div class="form-item" v-if="questionForm.question_form == 6">
  1137. <div class="form-label">填空答案模板</div>
  1138. <BlankAnswerEditor :model-value="questionForm.blank_answer_template || []" :question="questionForm.content"
  1139. :readonly="true" />
  1140. </div>
  1141. <div v-if="questionForm.question_form === 3">
  1142. <div class="form-label">题目图片</div>
  1143. <img :src="questionForm.question_image_url" alt="">
  1144. <!-- <el-upload
  1145. class="avatar-uploader"
  1146. :action="uploadUrl"
  1147. :show-file-list="false"
  1148. :on-success="handleAvatarSuccess"
  1149. :auto-upload="false"
  1150. :file-list="questionForm.question_image_url"
  1151. :disabled="true"
  1152. >
  1153. <el-button size="small" type="primary">点击上传</el-button>
  1154. </el-upload> -->
  1155. </div>
  1156. <div class="form-item">
  1157. <div class="form-label">分类</div>
  1158. <el-input v-model="questionForm.category_name" :disabled="true" placeholder="暂无分类" />
  1159. <!-- <el-select
  1160. v-model="questionForm.category"
  1161. placeholder="请选择题目分类"
  1162. class="full-width"
  1163. :disabled="true"
  1164. >
  1165. <el-option
  1166. v-for="cat in categoryOptions"
  1167. :key="cat.value"
  1168. :label="cat.label"
  1169. :value="cat.value"
  1170. />
  1171. </el-select> -->
  1172. <div class="form-tip">选择题目所属分类</div>
  1173. </div>
  1174. <div class="form-item">
  1175. <div class="form-label">标签</div>
  1176. <el-select v-model="questionForm.tag_ids" placeholder="暂无标签" class="full-width" multiple :disabled="true">
  1177. <el-option v-for="tag in questionForm.tags" :key="tag.value" :label="tag.name" :value="tag.id" />
  1178. </el-select>
  1179. <div class="form-tip">选择题目相关标签,可多选</div>
  1180. </div>
  1181. <div class="form-item">
  1182. <div class="form-label">建议时长(秒)</div>
  1183. <el-input-number v-model="questionForm.suggestedDuration" :min="0" :step="1" controls-position="right"
  1184. placeholder="请输入建议回答时长" class="full-width" :disabled="true" />
  1185. <div class="form-tip">建议回答题目的时长,单位为秒</div>
  1186. </div>
  1187. <div class="form-item">
  1188. <div class="form-label">评价标准</div>
  1189. <el-input v-model="questionForm.scoring_reference" type="textarea" :disabled="true" rows="4"
  1190. placeholder="评分标准说明,例如:【团队协作能力】1.优秀:xxxxxx。2.xxxx。3.一般:xxxxx。" class="full-width" />
  1191. <div class="form-tip">评分标准说明,例如:【团队协作能力】1.优秀:xxxxxx。2.xxxx。3.一般:xxxxx。</div>
  1192. </div>
  1193. </div>
  1194. <template #footer>
  1195. <span class="dialog-footer">
  1196. <el-button @click="cancelAddQuestion">取消</el-button>
  1197. <!-- <el-button type="primary" @click="confirmAddQuestion">确认</el-button> -->
  1198. </span>
  1199. </template>
  1200. </el-dialog>
  1201. <!-- 添加心理题选择对话框 -->
  1202. <el-dialog title="选择题目" v-model="showQuestionSelectDialog" width="30%" :close-on-click-modal="false"
  1203. class="question-select-dialog">
  1204. <div class="question-select-content">
  1205. <div>
  1206. <el-tree ref="treeRef" :data="treeData" node-key="id" :props="defaultProps" default-expand-all
  1207. highlight-current @node-click="handleNodeClick" v-loading="treeLoading">
  1208. <template #default="{ node, data }">
  1209. <div class="custom-tree-node">
  1210. <div class="node-label" style="display: flex;align-items: center;">
  1211. <el-icon>
  1212. <Folder />
  1213. </el-icon>
  1214. <span class="ml-2">{{ data.title }}</span>
  1215. </div>
  1216. </div>
  1217. </template>
  1218. </el-tree>
  1219. </div>
  1220. </div>
  1221. <template #footer>
  1222. <span class="dialog-footer">
  1223. <el-button @click="handleQuestionSelectPreview('4')">预览已选</el-button>
  1224. <el-button @click="cancelQuestionSelect">取消</el-button>
  1225. <el-button type="primary" @click="confirmQuestionSelect" :disabled='selectedQuestions.length == 0'
  1226. style="background-color: #ed7d31;border-color: #ed7d31;">确定</el-button>
  1227. </span>
  1228. </template>
  1229. </el-dialog>
  1230. <!-- 自定义题目创建对话框 -->
  1231. <el-dialog title="创建自定义题目" v-model="showQuestionBankFormDialog" width="40%" :close-on-click-modal="false"
  1232. class="question-bank-form-dialog" :before-close="handleQuestionBankFormCancel">
  1233. <QuestionBankForm v-model="questionBankFormData" :is-edit="isEditingQuestion"
  1234. @submit="handleQuestionBankFormSubmit" @cancel="handleQuestionBankFormCancel" />
  1235. </el-dialog>
  1236. <!-- 修改资料收集对话框模板 -->
  1237. <el-dialog title="资料收集" v-model="showDataCollectionDialog" width="900px" :close-on-click-modal="false"
  1238. class="data-collection-dialog">
  1239. <div class="data-collection-form">
  1240. <!-- 字段配置面板 -->
  1241. <div class="field-config-panel">
  1242. <div class="panel-header">
  1243. <h3>字段显示配置</h3>
  1244. <!-- <div class="panel-actions">
  1245. <el-button type="primary" size="small" @click="handleSaveFieldConfigToCache">保存到本地</el-button>
  1246. <el-button size="small" @click="clearFieldConfigCache">清除缓存</el-button>
  1247. </div> -->
  1248. </div>
  1249. <div class="field-config-list">
  1250. <!-- 基本信息部分 -->
  1251. <div class="section-title">基本信息</div>
  1252. <el-row :gutter="20">
  1253. <el-col :span="8" v-for="(field, key) in getBasicFields()" :key="key">
  1254. <div class="field-config-item">
  1255. <span class="field-label">
  1256. {{ field.label }}
  1257. <!-- <el-tag v-if="field.required" size="small" type="danger" class="required-tag">必填</el-tag> -->
  1258. </span>
  1259. <el-tooltip content="切换显示/隐藏" placement="top">
  1260. <el-switch v-model="field.visible"
  1261. :disabled="field.label == '姓名' || field.label == '手机号' || field.label == '身份证号'" class="custom-switch" />
  1262. </el-tooltip>
  1263. </div>
  1264. </el-col>
  1265. </el-row>
  1266. <!-- 家庭成员信息部分 -->
  1267. <div class="section-title">家庭成员信息</div>
  1268. <el-row :gutter="20">
  1269. <el-col :span="24">
  1270. <div class="field-config-item">
  1271. <span class="field-label">
  1272. {{ fieldConfig.family_member.label }}
  1273. <el-tag v-if="fieldConfig.family_member.visible" size="small" type="success">已启用</el-tag>
  1274. </span>
  1275. <!-- <el-switch v-model="fieldConfig.family_member.visible" class="custom-switch" /> -->
  1276. </div>
  1277. </el-col>
  1278. </el-row>
  1279. <el-row :gutter="20" v-if="fieldConfig.family_member.visible">
  1280. <el-col :span="8" v-for="(field, key) in fieldConfig.family_member.fields" :key="key">
  1281. <div class="field-config-item sub-field">
  1282. <span class="field-label">
  1283. {{ field.label }}
  1284. <!-- <el-tag v-if="field.required" size="small" type="danger" class="required-tag">必填</el-tag> -->
  1285. </span>
  1286. <el-tooltip content="切换显示/隐藏" placement="top">
  1287. <el-switch v-model="field.visible" class="custom-switch" />
  1288. </el-tooltip>
  1289. </div>
  1290. </el-col>
  1291. </el-row>
  1292. <!-- 教育经历部分 -->
  1293. <div class="section-title">教育经历</div>
  1294. <el-row :gutter="20">
  1295. <el-col :span="24">
  1296. <div class="field-config-item">
  1297. <span class="field-label">
  1298. {{ fieldConfig.education.label }}
  1299. <el-tag v-if="fieldConfig.education.visible" size="small" type="success">已启用</el-tag>
  1300. </span>
  1301. <!-- <el-switch v-model="fieldConfig.education.visible" class="custom-switch" /> -->
  1302. </div>
  1303. </el-col>
  1304. </el-row>
  1305. <el-row :gutter="20" v-if="fieldConfig.education.visible">
  1306. <el-col :span="8" v-for="(field, key) in fieldConfig.education.fields" :key="key">
  1307. <div class="field-config-item sub-field">
  1308. <span class="field-label">
  1309. {{ field.label }}
  1310. <!-- <el-tag v-if="field.required" size="small" type="danger" class="required-tag">必填</el-tag> -->
  1311. </span>
  1312. <el-tooltip content="切换显示/隐藏" placement="top">
  1313. <el-switch v-model="field.visible" class="custom-switch" />
  1314. </el-tooltip>
  1315. </div>
  1316. </el-col>
  1317. </el-row>
  1318. <!-- 专业技能部分 -->
  1319. <div class="section-title">专业技能</div>
  1320. <el-row :gutter="20">
  1321. <el-col :span="8" v-for="(field, key) in getSkillFields()" :key="key">
  1322. <div class="field-config-item">
  1323. <span class="field-label">
  1324. {{ field.label }}
  1325. <!-- <el-tag v-if="field.required" size="small" type="danger" class="required-tag">必填</el-tag> -->
  1326. </span>
  1327. <el-tooltip content="切换显示/隐藏" placement="top">
  1328. <el-switch v-model="field.visible" class="custom-switch" />
  1329. </el-tooltip>
  1330. </div>
  1331. </el-col>
  1332. </el-row>
  1333. <!-- 工作经历部分 -->
  1334. <div class="section-title">工作经历</div>
  1335. <el-row :gutter="20">
  1336. <el-col :span="24">
  1337. <div class="field-config-item">
  1338. <span class="field-label">
  1339. {{ fieldConfig.work_experience.label }}
  1340. <el-tag v-if="fieldConfig.work_experience.visible" size="small" type="success">已启用</el-tag>
  1341. </span>
  1342. <!-- <el-switch v-model="fieldConfig.work_experience.visible" class="custom-switch" /> -->
  1343. </div>
  1344. </el-col>
  1345. </el-row>
  1346. <el-row :gutter="20" v-if="fieldConfig.work_experience.visible">
  1347. <el-col :span="8" v-for="(field, key) in fieldConfig.work_experience.fields" :key="key">
  1348. <div class="field-config-item sub-field">
  1349. <span class="field-label">
  1350. {{ field.label }}
  1351. <!-- <el-tag v-if="field.required" size="small" type="danger" class="required-tag">必填</el-tag> -->
  1352. </span>
  1353. <el-tooltip content="切换显示/隐藏" placement="top">
  1354. <el-switch v-model="field.visible" class="custom-switch" />
  1355. </el-tooltip>
  1356. </div>
  1357. </el-col>
  1358. </el-row>
  1359. </div>
  1360. </div>
  1361. </div>
  1362. <template #footer>
  1363. <span class="dialog-footer">
  1364. <el-button @click="cancelDataCollection">取消</el-button>
  1365. <el-button type="primary" @click="saveFieldConfig">确认</el-button><!-- confirmDataCollection -->
  1366. </span>
  1367. </template>
  1368. </el-dialog>
  1369. <!-- 添加简历收集对话框 -->
  1370. <el-dialog title="简历收集" v-model="showResumeCollectionDialog" width="500px" :close-on-click-modal="false"
  1371. class="resume-collection-dialog">
  1372. <div class="resume-collection-form">
  1373. <div class="form-item">
  1374. <div class="form-label">步骤名称</div>
  1375. <el-input v-model="resumeCollectionForm.title" placeholder="请输入步骤名称" maxlength="30" show-word-limit
  1376. :disabled="true" />
  1377. </div>
  1378. <!-- <div class="form-item">
  1379. <div class="form-label">必填项</div>
  1380. <el-switch v-model="resumeCollectionForm.isRequired" />
  1381. <span class="switch-label">{{ resumeCollectionForm.isRequired ? '开启' : '关闭' }}</span>
  1382. </div>
  1383. <div class="resume-preview">
  1384. <div class="preview-title">简历收集预览</div>
  1385. <div class="preview-content">
  1386. <div class="preview-item">
  1387. <div class="item-label">姓名</div>
  1388. <div class="item-input"></div>
  1389. <div class="item-required" v-if="resumeCollectionForm.isRequired">*</div>
  1390. </div>
  1391. <div class="preview-item">
  1392. <div class="item-label">手机号</div>
  1393. <div class="item-input"></div>
  1394. <div class="item-required" v-if="resumeCollectionForm.isRequired">*</div>
  1395. </div>
  1396. <div class="preview-item">
  1397. <div class="item-label">邮箱</div>
  1398. <div class="item-input"></div>
  1399. <div class="item-required" v-if="resumeCollectionForm.isRequired">*</div>
  1400. </div>
  1401. <div class="preview-item">
  1402. <div class="item-label">上传简历</div>
  1403. <div class="item-upload">
  1404. <el-icon><Plus /></el-icon>
  1405. </div>
  1406. <div class="item-required" v-if="resumeCollectionForm.isRequired">*</div>
  1407. </div>
  1408. </div>
  1409. </div> -->
  1410. </div>
  1411. <template #footer>
  1412. <span class="dialog-footer">
  1413. <el-button @click="cancelResumeCollection">取消</el-button>
  1414. <el-button type="primary" @click="confirmResumeCollection">确认</el-button>
  1415. </span>
  1416. </template>
  1417. </el-dialog>
  1418. <!-- 添加代码测试对话框 -->
  1419. <el-dialog title="添加代码笔试步骤" v-model="showCodeTestDialog" width="500px" :close-on-click-modal="false"
  1420. class="code-test-dialog">
  1421. <div class="code-test-form">
  1422. <div class="form-item">
  1423. <div class="form-label">步骤名称</div>
  1424. <el-input v-model="codeTestForm.title" placeholder="请输入步骤名称" maxlength="30" show-word-limit />
  1425. </div>
  1426. <div class="form-item">
  1427. <div class="form-label">自动评判试卷</div>
  1428. <el-switch v-model="codeTestForm.autoJudge" />
  1429. <span class="switch-label">{{ codeTestForm.autoJudge ? '开启' : '关闭' }}</span>
  1430. </div>
  1431. <div class="form-item">
  1432. <div class="form-label">添加考试试卷</div>
  1433. <div class="test-paper-selector">
  1434. <div class="empty-paper" v-if="!codeTestForm.selectedPaper">
  1435. <el-button type="primary" plain @click="handleSelectPaper">
  1436. <el-icon>
  1437. <Plus />
  1438. </el-icon>
  1439. 添加考卷
  1440. </el-button>
  1441. <p class="tip-text">多选时,考生会随机分配到一套试卷</p>
  1442. </div>
  1443. <div class="selected-paper" v-else>
  1444. <div class="paper-info">
  1445. <div class="paper-title">{{ codeTestForm.selectedPaper.title }}</div>
  1446. <div class="paper-meta">
  1447. <span>{{ codeTestForm.selectedPaper.questionCount }}道题</span>
  1448. <span>{{ codeTestForm.selectedPaper.duration }}分钟</span>
  1449. <span>{{ codeTestForm.selectedPaper.difficulty }}</span>
  1450. </div>
  1451. </div>
  1452. <div class="paper-actions">
  1453. <el-button type="text" @click="handleViewPaper">查看</el-button>
  1454. <el-button type="text" @click="handleChangePaper">更换</el-button>
  1455. <el-button type="text" @click="handleRemovePaper">删除</el-button>
  1456. </div>
  1457. </div>
  1458. </div>
  1459. </div>
  1460. </div>
  1461. <template #footer>
  1462. <span class="dialog-footer">
  1463. <el-button @click="cancelCodeTest">取消</el-button>
  1464. <el-button type="primary" @click="confirmCodeTest">确认</el-button>
  1465. </span>
  1466. </template>
  1467. </el-dialog>
  1468. <!-- 添加打字测试对话框 -->
  1469. <el-drawer title="添加打字测试步骤" v-model="showTypingTestDialog" size="60%" :close-on-click-modal="false"
  1470. class="typing-test-dialog" :destroy-on-close="false">
  1471. <div class="typing-test-form">
  1472. <div class="form-item">
  1473. <div class="form-label">选择文章</div>
  1474. <div class="article-selection">
  1475. <el-button type="primary" size="small" class="add-article-btn">
  1476. <el-icon>
  1477. <Plus />
  1478. </el-icon>
  1479. 添加文章
  1480. </el-button>
  1481. </div>
  1482. </div>
  1483. <div class="form-item">
  1484. <div class="form-label">候选人选择文章方式</div>
  1485. <el-radio-group v-model="typingTestForm.selectionMode">
  1486. <el-radio label="random">随机抽取</el-radio>
  1487. <el-radio label="self">候选人自主选择</el-radio>
  1488. </el-radio-group>
  1489. </div>
  1490. <div class="form-item">
  1491. <div class="form-label">测试时间</div>
  1492. <div class="time-input-container">
  1493. <el-input v-model="typingTestForm.duration" class="time-input" />
  1494. <span class="time-unit">分钟</span>
  1495. </div>
  1496. </div>
  1497. <div class="form-item">
  1498. <div class="form-label">登录界面预览图(建议尺寸1024*768像素,最大不要超过5M)</div>
  1499. <el-upload class="preview-uploader" action="#" :auto-upload="false" :show-file-list="false" accept="image/*">
  1500. <div class="upload-area">
  1501. <el-icon>
  1502. <Plus />
  1503. </el-icon>
  1504. <div class="upload-text">添加图片</div>
  1505. </div>
  1506. </el-upload>
  1507. </div>
  1508. <div class="form-item">
  1509. <div class="form-label">注意事项内容(在登录界面显示)</div>
  1510. <el-input v-model="typingTestForm.notes" type="textarea" rows="4" placeholder="请输入注意事项内容" maxlength="300"
  1511. show-word-limit />
  1512. </div>
  1513. <div class="form-item">
  1514. <div class="form-label">测试页面标题</div>
  1515. <el-input v-model="typingTestForm.pageTitle" placeholder="测试页面标题" />
  1516. </div>
  1517. <div class="form-item">
  1518. <div class="form-label">视频监控</div>
  1519. <el-checkbox v-model="typingTestForm.videoMonitoring">视频监控</el-checkbox>
  1520. </div>
  1521. <div class="form-item">
  1522. <div class="form-label">通过标准</div>
  1523. <div class="criteria-options">
  1524. <el-checkbox v-model="typingTestForm.passCriteria.speed">速度</el-checkbox>
  1525. <el-checkbox v-model="typingTestForm.passCriteria.accuracy">正确率</el-checkbox>
  1526. </div>
  1527. </div>
  1528. </div>
  1529. <template #footer>
  1530. <span class="dialog-footer">
  1531. <el-button @click="cancelTypingTest">取消</el-button>
  1532. <el-button type="primary" @click="confirmTypingTest">保存</el-button>
  1533. </span>
  1534. </template>
  1535. </el-drawer>
  1536. <!-- 面试设置弹窗 -->
  1537. <el-dialog title="面试设置" v-model="showInterviewSettingsDialog" width="60%" :close-on-click-modal="false"
  1538. class="interview-settings-dialog">
  1539. <div class="interview-settings-content">
  1540. <!-- 防作弊设置 -->
  1541. <div class="settings-section">
  1542. <div class="section-header">
  1543. <span class="section-title">防作弊设置</span>
  1544. </div>
  1545. <div class="setting-item">
  1546. <div class="setting-info">
  1547. <span class="setting-name">「人脸一致性检测」</span>
  1548. <p class="setting-desc">选取后,将会在候选人离开前置摄像头时进行提示,默认开启</p>
  1549. </div>
  1550. <el-switch v-model="interviewSettings.faceConsistency" :disabled="true" />
  1551. </div>
  1552. <div class="setting-item">
  1553. <div class="setting-info">
  1554. <span class="setting-name">「 防截屏 / 录屏 」</span>
  1555. <p class="setting-desc">选取后,将会在候选人截屏或录屏时进行提示,为防止泄题,默认开启</p>
  1556. </div>
  1557. <el-switch v-model="interviewSettings.preventScreenshot" :disabled="true" />
  1558. </div>
  1559. <div class="setting-item">
  1560. <div class="setting-info">
  1561. <span class="setting-name">「 切 屏 预 制 」</span>
  1562. <p class="setting-desc">选取后,将会在候选人切屏时进行提示,为防止作弊,默认开启</p>
  1563. </div>
  1564. <el-switch v-model="interviewSettings.screenSwitchAlert" :disabled="true">
  1565. <span class="switch-desc">切屏超过 2 次关闭,作废本关面试</span>
  1566. </el-switch>
  1567. </div>
  1568. <div class="setting-item">
  1569. <div class="setting-info">
  1570. <span class="setting-name">「情绪/眼动检测」</span>
  1571. <p class="setting-desc">选取后,将对候选人整场面试的情绪与压力表现进行分析,建议开启</p>
  1572. </div>
  1573. <el-switch v-model="interviewSettings.positionDetection" />
  1574. </div>
  1575. <!-- <div class="setting-item">
  1576. <div class="setting-info">@change="handlePositionDetectionChange"
  1577. <span class="setting-name">「 眼 动 检 测 」</span>
  1578. <p class="setting-desc">视实检测候选人的眼动频率进行分析,页面等较等</p>
  1579. </div>
  1580. <el-switch v-model="interviewSettings.eyeMovementDetection" :disabled="true"/>
  1581. </div> -->
  1582. </div>
  1583. <!-- 其他设置 -->
  1584. <div class="settings-section">
  1585. <div class="section-header">
  1586. <span class="section-title">其他设置</span>
  1587. </div>
  1588. <div class="setting-item">
  1589. <div class="setting-info">
  1590. <span class="setting-name">重复面试</span>
  1591. <div class="setting-desc-container">
  1592. <p class="setting-desc" style="">选取后,将允许面试者进行重复面试</p>
  1593. <el-switch v-model="interviewSettings.reviewInterview" />
  1594. </div>
  1595. </div>
  1596. </div>
  1597. <div class="setting-item">
  1598. <div class="setting-info">
  1599. <span class="setting-name">开场词</span>
  1600. <div v-if="!isEditingBeforeStart" class="setting-desc-container">
  1601. <p class="setting-desc">{{ interviewSettings.beforeStartText }}</p>
  1602. <el-button type="text" class="edit-desc-btn" @click="startEditBeforeStart">
  1603. <el-icon>
  1604. <Edit />
  1605. </el-icon>
  1606. </el-button>
  1607. </div>
  1608. <div v-else class="desc-edit-container">
  1609. <el-input v-model="editingBeforeStartText" type="textarea" rows="5" maxlength="150"
  1610. placeholder="请输入开始前提示内容" show-word-limit /><!-- maxlength="100" -->
  1611. <div class="desc-edit-actions">
  1612. <el-button size="small" @click="cancelEditBeforeStart">取消</el-button>
  1613. <el-button size="small" type="primary" @click="saveBeforeStart">保存</el-button>
  1614. </div>
  1615. </div>
  1616. </div>
  1617. <!-- <el-switch v-model="interviewSettings.beforeStart" /> -->
  1618. </div>
  1619. <!-- <div class="setting-item">
  1620. <div class="setting-info">
  1621. <span class="setting-name">开始后提醒</span>
  1622. <div v-if="!isEditingAfterStart" class="setting-desc-container">
  1623. <p class="setting-desc">{{ interviewSettings.afterStartText }}</p>
  1624. <el-button type="text" class="edit-desc-btn" @click="startEditAfterStart">
  1625. <el-icon><Edit /></el-icon>
  1626. </el-button>
  1627. </div>
  1628. <div v-else class="desc-edit-container">
  1629. <el-input
  1630. v-model="editingAfterStartText"
  1631. type="textarea"
  1632. rows="5"
  1633. placeholder="请输入开始后提醒内容"
  1634. maxlength="100"
  1635. show-word-limit
  1636. />
  1637. <div class="desc-edit-actions">
  1638. <el-button size="small" @click="cancelEditAfterStart">取消</el-button>
  1639. <el-button size="small" type="primary" @click="saveAfterStart">保存</el-button>
  1640. </div>
  1641. </div>
  1642. </div>
  1643. </div> -->
  1644. <div class="setting-item">
  1645. <div class="setting-info">
  1646. <span class="setting-name">结束语</span>
  1647. <div v-if="!isEditingDisclaimer" class="setting-desc-container">
  1648. <p class="setting-desc">{{ interviewSettings.disclaimerText }}</p>
  1649. <el-button type="text" class="edit-desc-btn" @click="startEditDisclaimer">
  1650. <el-icon>
  1651. <Edit />
  1652. </el-icon>
  1653. </el-button>
  1654. </div>
  1655. <div v-else class="desc-edit-container">
  1656. <el-input v-model="editingDisclaimerText" type="textarea" rows="5" placeholder="请输入免责声明内容"
  1657. maxlength="200" show-word-limit />
  1658. <div class="desc-edit-actions">
  1659. <el-button size="small" @click="cancelEditDisclaimer">取消</el-button>
  1660. <el-button size="small" type="primary" @click="saveDisclaimer">保存</el-button>
  1661. </div>
  1662. </div>
  1663. </div>
  1664. <!-- <el-switch v-model="interviewSettings.disclaimer" /> -->
  1665. </div>
  1666. </div>
  1667. </div>
  1668. <template #footer>
  1669. <span class="dialog-footer">
  1670. <el-button @click="cancelInterviewSettings">取消</el-button>
  1671. <el-button type="primary" @click="confirmInterviewSettings">完成</el-button>
  1672. </span>
  1673. </template>
  1674. </el-dialog>
  1675. <!-- 绑定综合素质问题 -->
  1676. <el-dialog title="选择题目" v-model="QuestionSelectDialog" width="70%" :close-on-click-modal="false"
  1677. class="question-select-dialog">
  1678. <div class="question-select-content">
  1679. <el-row class="document-el-row">
  1680. <el-col :span="3">
  1681. <div class="document-box document-left-box" v-loading="documentTreeLoading">
  1682. <TreeList ref="treeListRef" :treeData="documentTreeData" @treeClick="handleTreeClick"
  1683. @updateDocument="handleUpdateDocument" @deleteDocument="handleDeleteDocument" />
  1684. </div>
  1685. </el-col>
  1686. <el-col :span="21">
  1687. <!-- 搜索栏 -->
  1688. <div class="search-bar">
  1689. <div class="search-inputs">
  1690. <el-input v-model="questionSearchKeyword" placeholder="请输入关键词搜索题目" clearable
  1691. @clear="handleQuestionSearch" @keyup.enter="handleQuestionSearch"
  1692. style="width: 300px; margin-right: 10px;">
  1693. <template #append>
  1694. <el-button @click="handleQuestionSearch">
  1695. <el-icon>
  1696. <Search />
  1697. </el-icon>
  1698. </el-button>
  1699. </template>
  1700. </el-input>
  1701. <el-select v-model="questionListQuery.question_form" placeholder="题目类型" clearable
  1702. @change="handleQuestionSearch" style="width: 120px;">
  1703. <el-option label="开放问题" :value="0" />
  1704. <el-option label="单选题" :value="1" />
  1705. <el-option label="多选题" :value="2" />
  1706. <el-option label="识图题" :value="3" />
  1707. <el-option label="得分题" :value="4" />
  1708. <el-option label="追加型开放问题" :value="5" />
  1709. <el-option label="填空题" :value="6" />
  1710. </el-select>
  1711. </div>
  1712. </div>
  1713. <!-- 题目列表 -->
  1714. <div class="question-lists">
  1715. <el-table :data="questionList" style="width: 100%" @selection-change="handleQuestionSelectionChange">
  1716. <el-table-column type="selection" width="55" />
  1717. <el-table-column prop="question" label="题目标题" />
  1718. <el-table-column prop="question_form_name" label="题目类型" width="100" />
  1719. <!-- <el-table-column prop="content" label="面试内容" show-overflow-tooltip />
  1720. <el-table-column prop="target" label="对话目标" show-overflow-tooltip /> -->
  1721. </el-table>
  1722. <!-- 分页 -->
  1723. <div class="pagination-container">
  1724. <el-pagination v-model:current-page="questionListQuery.page"
  1725. v-model:page-size="questionListQuery.pageSize" :total="totalQuestions2"
  1726. :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next"
  1727. @size-change="handleQSizeChange" @current-change="handleQPageChange" />
  1728. </div>
  1729. </div>
  1730. </el-col>
  1731. </el-row>
  1732. </div>
  1733. <template #footer>
  1734. <!-- <div class="selected-questions-preview" v-if="selectedQuestions.length > 0">
  1735. <div class="preview-title">已选择的题目({{ selectedQuestions.length }}):</div>
  1736. <div class="preview-list">
  1737. <el-tag
  1738. v-for="(question, index) in selectedQuestions"
  1739. :key="index"
  1740. class="preview-item"
  1741. closable
  1742. @close="removeSelectedQuestion(question)"
  1743. >
  1744. {{ question.question }}
  1745. </el-tag>
  1746. </div>
  1747. </div> -->
  1748. <span class="dialog-footer">
  1749. <el-button @click="handleQuestionSelectPreview('1,2,3,6')">预览已选</el-button>
  1750. <!-- <el-button @click="cancelQuestionSelect">取消</el-button> -->
  1751. <el-button type="primary" @click="confirmSelect"
  1752. style="background-color: #ed7d31;border-color: #ed7d31;">确定</el-button>
  1753. </span>
  1754. </template>
  1755. </el-dialog>
  1756. <!-- 候选人提问环节配置弹窗 -->
  1757. <el-dialog :title="editingCandidateQuestionsIndex >= 0 ? '编辑候选人提问环节' : '配置候选人提问环节'"
  1758. v-model="showCandidateQuestionsDialog" width="600px" :close-on-click-modal="false"
  1759. class="candidate-questions-dialog">
  1760. <div class="candidate-questions-form">
  1761. <div class="form-item">
  1762. <div class="form-label">
  1763. 环节名称<span class="required">*</span>
  1764. </div>
  1765. <el-input v-model="candidateQuestionsForm.title" placeholder="请输入环节名称" maxlength="30" show-word-limit />
  1766. </div>
  1767. <div class="form-item">
  1768. <div class="form-label">
  1769. 提问时长<span class="required">*</span>
  1770. </div>
  1771. <el-input-number v-model="candidateQuestionsForm.duration" :min="1" :max="30" :step="1"
  1772. controls-position="right" style="width: 200px" />
  1773. <span class="form-unit">分钟</span>
  1774. <div class="form-tip">候选人可用于提问的总时长</div>
  1775. </div>
  1776. <!-- <div class="form-item">
  1777. <div class="form-label">
  1778. 最多提问数量<span class="required">*</span>
  1779. </div>
  1780. <el-input-number
  1781. v-model="candidateQuestionsForm.maxQuestions"
  1782. :min="1"
  1783. :max="10"
  1784. :step="1"
  1785. controls-position="right"
  1786. style="width: 200px"
  1787. />
  1788. <span class="form-unit">个</span>
  1789. <div class="form-tip">候选人最多可以提出的问题数量</div>
  1790. </div> -->
  1791. <div class="form-item">
  1792. <div class="form-label">环节描述</div>
  1793. <el-input v-model="candidateQuestionsForm.description" type="textarea" rows="3" placeholder="请输入环节描述"
  1794. maxlength="200" />
  1795. <div class="form-tip">向候选人说明此环节的目的和意义</div>
  1796. </div>
  1797. <!-- <div class="form-item">
  1798. <div class="form-label">环节设置</div>
  1799. <div class="form-checkboxes">
  1800. <el-checkbox v-model="candidateQuestionsForm.isRequired">
  1801. 必须环节(候选人必须完成此环节)
  1802. </el-checkbox>
  1803. <el-checkbox v-model="candidateQuestionsForm.allowSkip">
  1804. 允许跳过(候选人可选择跳过此环节)
  1805. </el-checkbox>
  1806. </div>
  1807. </div>
  1808. <div class="form-item">
  1809. <div class="form-label">提问指引</div>
  1810. <div class="guidelines-list">
  1811. <div
  1812. v-for="(guideline, index) in candidateQuestionsForm.guidelines"
  1813. :key="index"
  1814. class="guideline-item"
  1815. >
  1816. <el-input
  1817. v-model="candidateQuestionsForm.guidelines[index]"
  1818. placeholder="请输入提问指引"
  1819. maxlength="100"
  1820. >
  1821. <template #append>
  1822. <el-button
  1823. type="danger"
  1824. @click="removeGuideline(index)"
  1825. v-if="candidateQuestionsForm.guidelines.length > 1"
  1826. >
  1827. 删除
  1828. </el-button>
  1829. </template>
  1830. </el-input>
  1831. </div>
  1832. <el-button
  1833. type="primary"
  1834. plain
  1835. @click="addGuideline"
  1836. v-if="candidateQuestionsForm.guidelines.length < 5"
  1837. >
  1838. <el-icon><Plus /></el-icon>
  1839. 添加指引
  1840. </el-button>
  1841. </div>
  1842. <div class="form-tip">为候选人提供提问的指导建议,最多5条</div>
  1843. </div> -->
  1844. </div>
  1845. <template #footer>
  1846. <span class="dialog-footer">
  1847. <el-button @click="cancelCandidateQuestions">取消</el-button>
  1848. <el-button type="primary" @click="confirmCandidateQuestions">确认</el-button>
  1849. </span>
  1850. </template>
  1851. </el-dialog>
  1852. <!-- 肢体检测配置对话框 -->
  1853. <el-dialog v-model="bodyDetectionConfigVisible" title="肢体检测配置" width="600px" :close-on-click-modal="false">
  1854. <el-form :model="bodyDetectionConfig" label-width="120px">
  1855. <el-form-item label="检测时长">
  1856. <el-input-number v-model="bodyDetectionConfig.duration" :min="1" :max="30" :step="1"
  1857. controls-position="right" />
  1858. 分钟
  1859. </el-form-item>
  1860. <el-form-item label="检测项目">
  1861. <el-checkbox-group v-model="bodyDetectionConfig.detectionItems">
  1862. <el-checkbox label="face">面部表情</el-checkbox>
  1863. <el-checkbox label="pose">肢体姿态</el-checkbox>
  1864. <el-checkbox label="gesture">手势动作</el-checkbox>
  1865. </el-checkbox-group>
  1866. </el-form-item>
  1867. <el-form-item label="提示文本">
  1868. <el-input v-model="bodyDetectionConfig.promptText" type="textarea" :rows="3" placeholder="请输入提示文本" />
  1869. </el-form-item>
  1870. </el-form>
  1871. <template #footer>
  1872. <span class="dialog-footer">
  1873. <el-button @click="cancelBodyDetectionConfig" size="small">取消</el-button>
  1874. <el-button type="primary" @click="saveBodyDetectionConfig" size="small">确定</el-button>
  1875. </span>
  1876. </template>
  1877. </el-dialog>
  1878. <!-- 查看已选 -->
  1879. <el-dialog v-model="previewDialog" title="职位问题预览" width="60%" :close-on-click-modal="false">
  1880. <div class="selected-applications-preview">
  1881. <div class="preview-title">已绑定的问题({{ selectedApplications.length }}):</div>
  1882. <div class="tags-summary" v-if="uniqueSelectedTags.length">
  1883. <span style="margin-right: 8px; color: #606266;">标签:</span>
  1884. <el-tag v-for="tag in uniqueSelectedTags" :key="tag.id ?? tag.name" effect="plain" size="small">
  1885. {{ tag.name }}
  1886. </el-tag>
  1887. </div>
  1888. <div v-else style="color:#909399; margin-bottom: 8px;">暂无标签</div>
  1889. <el-table :data="selectedApplications" style="width: 100%">
  1890. <el-table-column prop="question" label="问题内容" min-width="300" show-overflow-tooltip />
  1891. <el-table-column label="标签" min-width="220">
  1892. <template #default="scope">
  1893. <el-tag
  1894. v-for="tag in getQuestionTags(scope.row)"
  1895. :key="tag.id ?? tag.name"
  1896. size="small"
  1897. effect="plain"
  1898. style="margin: 0 6px 6px 0;"
  1899. >
  1900. {{ tag.name ?? tag.label ?? tag.text ?? tag.title }}
  1901. </el-tag>
  1902. <span v-if="!getQuestionTags(scope.row).length" style="color:#909399">无</span>
  1903. </template>
  1904. </el-table-column>
  1905. </el-table>
  1906. </div>
  1907. <div class="preview-footer">
  1908. <el-button type="primary" @click="previewDialog = false">关闭</el-button>
  1909. </div>
  1910. </el-dialog>
  1911. </div>
  1912. </template>
  1913. <script setup lang="ts">
  1914. import { ref, reactive, onMounted, onUnmounted, computed, watch, nextTick } from 'vue';
  1915. import { useRouter, useRoute } from 'vue-router';
  1916. import { ElMessage, ElMessageBox, ElDrawer } from 'element-plus';
  1917. import * as api from '../list/api';
  1918. import { Plus, Document, ArrowDown, Edit, Delete, InfoFilled, QuestionFilled, Search, Rank, Close } from '@element-plus/icons-vue';
  1919. import { GetList } from '../../questionBank/classList/api';
  1920. import {
  1921. GetCompetencyList, GetQuestionList, GenerateQuestions,
  1922. GetDraftList, SaveDraft, GetDigitalList,
  1923. CreateConfig, GetConfig, UpdateConfig, AddDocument,
  1924. GetOpeningSpeech, GetVideo, BatchBind, BatchUnbind, UpdateObj,
  1925. GenerateCompetency, SaveCompetency, GetPositionTags,
  1926. UpdateQuestion, GetPositionQuestions, CreateCompetency,
  1927. BatchUpdateTags, GetQuestionDetail, GetSelectedApplications, DeleteSelectedApplications
  1928. } from './api';
  1929. import draggable from 'vuedraggable';
  1930. import { updateFieldConfig } from './utils';
  1931. import type { ProfileFieldsConfig } from './types';
  1932. // 本地缓存工具函数
  1933. const FIELD_CONFIG_CACHE_KEY = 'position_field_config_cache';
  1934. // 保存字段配置到本地缓存
  1935. const saveFieldConfigToCache = (config: any, positionId: string) => {
  1936. try {
  1937. const cacheData = {
  1938. positionId,
  1939. config: JSON.parse(JSON.stringify(config)),
  1940. timestamp: Date.now()
  1941. };
  1942. localStorage.setItem(FIELD_CONFIG_CACHE_KEY, JSON.stringify(cacheData));
  1943. } catch (error) {
  1944. console.error('保存字段配置到缓存失败:', error);
  1945. }
  1946. };
  1947. // 从本地缓存读取字段配置
  1948. const loadFieldConfigFromCache = (positionId: string) => {
  1949. try {
  1950. const cachedData = localStorage.getItem(FIELD_CONFIG_CACHE_KEY);
  1951. if (cachedData) {
  1952. const parsed = JSON.parse(cachedData);
  1953. // 检查缓存是否匹配当前职位ID且未过期(7天有效期)
  1954. if (parsed.positionId === positionId &&
  1955. Date.now() - parsed.timestamp < 7 * 24 * 60 * 60 * 1000) {
  1956. return parsed.config;
  1957. }
  1958. }
  1959. return null;
  1960. } catch (error) {
  1961. console.error('读取字段配置缓存失败:', error);
  1962. return null;
  1963. }
  1964. };
  1965. // 清除本地缓存
  1966. const clearFieldConfigCache = () => {
  1967. try {
  1968. localStorage.removeItem(FIELD_CONFIG_CACHE_KEY);
  1969. ElMessage.success('缓存已清除');
  1970. } catch (error) {
  1971. console.error('清除字段配置缓存失败:', error);
  1972. ElMessage.error('清除缓存失败');
  1973. }
  1974. };
  1975. // 防抖定时器
  1976. const saveConfigTimer = ref<NodeJS.Timeout | null>(null);
  1977. // 手动保存字段配置到本地缓存
  1978. const handleSaveFieldConfigToCache = () => {
  1979. try {
  1980. saveFieldConfigToCache(fieldConfig, route.query.id as string);
  1981. ElMessage.success('字段配置已保存到本地缓存');
  1982. } catch (error) {
  1983. console.error('保存字段配置到缓存失败:', error);
  1984. ElMessage.error('保存缓存失败');
  1985. }
  1986. };
  1987. import { getPCAData } from '../../../utils/pcaData';
  1988. import QuestionBankForm from '../../questionBank/list/components/QuestionBankForm.vue';
  1989. import BlankAnswerEditor from '../../questionBank/list/components/BlankAnswerEditor.vue';
  1990. import * as questionBankApi from '../../questionBank/list/api';
  1991. import TreeList from './treeList.vue';
  1992. import { GetDocumentTree, GetCategoryList, GetTagList, DeleteDocumentCategory } from '../../questionBank/positionList/api';
  1993. import { Session } from '/@/utils/storage';
  1994. // 添加 CompetencyTag 接口定义
  1995. interface CompetencyTag {
  1996. id: number;
  1997. name: string;
  1998. selected?: boolean;
  1999. weight?: number;
  2000. }
  2001. // 添加 InterviewerAvatar 接口定义
  2002. interface InterviewerAvatar {
  2003. id: number;
  2004. name: string;
  2005. image?: string;
  2006. avatar_url?: string;
  2007. }
  2008. // 添加选项接口定义
  2009. interface QuestionOption {
  2010. id?: number;
  2011. text: string;
  2012. is_correct: boolean;
  2013. sort?: number;
  2014. }
  2015. // 添加 ChatQuestion 接口定义
  2016. interface ChatQuestion {
  2017. id?: number;
  2018. title: string;
  2019. ability: string;
  2020. content: string;
  2021. target: string;
  2022. weight: number;
  2023. maxAnswers: number;
  2024. source: 'ai_generated' | 'custom_selected'; // 添加题目来源标识
  2025. question_form?: number;
  2026. question_form_name?: string;
  2027. scoring_reference?: string;
  2028. competency_tags?: CompetencyTag[];
  2029. enable_follow_up?: boolean;
  2030. options?: QuestionOption[];
  2031. }
  2032. // 添加 Paper 接口定义
  2033. interface Paper {
  2034. id: number;
  2035. title: string;
  2036. questionCount: number;
  2037. duration: number;
  2038. difficulty: string;
  2039. }
  2040. interface AIVideoStep {
  2041. name: string;
  2042. id: string | number;
  2043. aiConfig?: {
  2044. abilities: string[];
  2045. };
  2046. }
  2047. const router = useRouter();
  2048. const route = useRoute();
  2049. const positionId = ref(route.query.id as string);
  2050. const positionStatus = ref(false);
  2051. const drag = ref(false); // 添加拖拽状态变量
  2052. // 计算可见步骤的序号
  2053. const getVisibleStepNumber = (index: number) => {
  2054. // 计算在当前索引之前有多少个可见的步骤
  2055. return recruitmentProcess
  2056. .slice(0, index + 1)
  2057. .filter(step => step.visible)
  2058. .length;
  2059. };
  2060. // 职位数据
  2061. const positionData = reactive({
  2062. title: '',
  2063. job_category: '',
  2064. job_type_display: '',
  2065. city: '',
  2066. location: [] as string[] | string, // 修改类型定义
  2067. location_str: '',
  2068. address_detail: '',
  2069. benefits: [],
  2070. requirements: '',
  2071. job_type: 0,
  2072. salary_range: '',
  2073. department: '',
  2074. end_date: '',
  2075. description: '',
  2076. work_experience_required: '',
  2077. education_required: '',
  2078. competency_tags: [],
  2079. options: [],
  2080. status: 0,
  2081. detailed_address: ''
  2082. });
  2083. // 招聘流程数据 - 修改为普通数组而非 ref 对象,添加type字段用于重复检查
  2084. const recruitmentProcess = reactive([
  2085. { id: 1, name: '视频宣讲', description: '视频宣讲', active: true, type: 'video_presentation', visible: true },
  2086. { id: 6, name: '简历收集', description: '简历收集', active: true, type: 'resume_collection', visible: true },
  2087. { id: 5, name: '资料收集', description: '资料收集', active: true, type: 'data_collection', visible: true },
  2088. { id: 2, name: 'AI考察', description: 'AI考察', active: true, type: 'ai_video', visible: true },
  2089. { id: 8, name: '综合素质考察', description: '综合素质考察', active: true, type: 'common_sense', visible: true },
  2090. { id: 9, name: '心理测试', description: '心理测试', active: true, type: 'psychological_problem', visible: true },
  2091. { id: 10, name: '肢体检测', description: '肢体检测', active: true, type: 'posture_check', visible: true }, //, enable_posture_check: true
  2092. {
  2093. id: 7, name: '候选人提问', description: '时长5分钟,最多3个问题', active: true, type: 'candidate_questions', visible: true,
  2094. config: {
  2095. title: '候选人提问',
  2096. duration: 5,
  2097. maxQuestions: 3,
  2098. description: '候选人可以向面试官提问关于公司、职位或团队的问题',
  2099. isRequired: true,
  2100. allowSkip: false,
  2101. guidelines: [
  2102. '请提出与职位、公司文化或团队相关的问题',
  2103. '避免询问已在职位描述中明确说明的信息',
  2104. '可以询问职业发展机会和团队工作方式'
  2105. ]
  2106. }
  2107. },
  2108. /* { id: 1, name: '待核验', description: '待核验', active: true, type: 'pending_verification' }, */
  2109. { id: 3, name: '已通过', description: '已通过', active: true, type: 'passed', visible: true },
  2110. { id: 4, name: '已淘汰', description: '已淘汰', active: false, type: 'rejected', visible: true },
  2111. ]);
  2112. // 是否显示流程编辑对话框
  2113. const showProcessDialog = ref(false);
  2114. // 定义可选的流程步骤类型
  2115. const processStepOptions = [
  2116. { label: '视频宣讲', value: 'video_presentation' },
  2117. { label: '简历收集', value: 'resume_collection' },
  2118. { label: '资料收集', value: 'data_collection' },
  2119. { label: 'AI考察', value: 'ai_video' },
  2120. { label: '综合素质考察', value: 'common_sense' },
  2121. { label: '心理测试', value: 'psychological_problem' },
  2122. { label: '候选人提问', value: 'candidate_questions' },
  2123. { label: '肢体检测', value: 'posture_check' }, // 新增肢体检测选项
  2124. /* { label: 'AI实时对话', value: 'ai_chat' },*/
  2125. /* { label: '代码测试', value: 'code_test' },
  2126. { label: '打字测试', value: 'typing_test' } */
  2127. ];
  2128. /* 获取标签名称 */
  2129. const getTagNames = (tags: any) => {
  2130. competencyTags.value.forEach((item: any) => {
  2131. if (tags.id == item.id) {
  2132. tags.name = item.name;
  2133. }
  2134. })
  2135. return tags.name;
  2136. }
  2137. /* 选择问题 */
  2138. const QuestionSelectDialog = ref(false);
  2139. const handleQSizeChange = (size: number) => {
  2140. questionList.pageSize = size;
  2141. GetQuestionList({ page: questionList.page, limit: questionList.pageSize, question_form: questionListQuery.question_form }).then((res: any) => {
  2142. if (res.data) {
  2143. questionList.value = res.data.items;
  2144. questionListpage.page = res.data.page;
  2145. questionListpage.pageSize = res.data.limit;
  2146. totalQuestions2.value = res.data.total;
  2147. }
  2148. })
  2149. };
  2150. const handleQPageChange = (page: number) => {
  2151. questionList.page = page;
  2152. GetQuestionList({ page: questionList.page, limit: questionList.pageSize, question_form: questionListQuery.question_form }).then((res: any) => {
  2153. if (res.data) {
  2154. questionList.value = res.data.items;
  2155. questionListpage.page = res.data.page;
  2156. questionListpage.pageSize = res.data.limit;
  2157. totalQuestions2.value = res.data.total;
  2158. }
  2159. })
  2160. };
  2161. const confirmSelect = async () => {
  2162. const selectedQuestionIds = selectedQuestions.value.map((item: any) => item.id);
  2163. console.log('selectedQuestionIds', selectedQuestionIds);
  2164. if (selectedQuestionIds.length === 0) {
  2165. ElMessage.warning('请至少选择一个问题');
  2166. return;
  2167. }
  2168. try {
  2169. await AddDocument({
  2170. position_id: route.query.id,
  2171. question_id: selectedQuestionIds,
  2172. duration: 60, // 默认时长,您可以根据需求调整
  2173. tenant_id: Session.get('tenant_id')
  2174. });
  2175. // 更新已选择的问题列表
  2176. const newSelectedQuestions = questionList.value.filter((item: any) => item.selected);
  2177. selectedQuestions.value.push(...newSelectedQuestions);
  2178. ElMessage.success('问题添加成功');
  2179. QuestionSelectDialog.value = false;
  2180. } catch (error: any) {
  2181. console.error('添加问题失败:', error);
  2182. if (error.code === 4000) {
  2183. ElMessage.warning('该题目已绑定,请选择其他题目');
  2184. } else {
  2185. ElMessage.error('添加问题失败,请重试');
  2186. }
  2187. }
  2188. }
  2189. const handleAddCompetency = () => {
  2190. competencyLoading.value = true;
  2191. GenerateCompetency({
  2192. position_id: route.query.id,
  2193. }).then((res: any) => {
  2194. res.data.forEach((item: any) => {
  2195. item.selected = false;
  2196. });
  2197. competencyTags.value = res.data;
  2198. console.log('res', res);
  2199. competencyLoading.value = false;
  2200. }).catch((err: any) => {
  2201. console.log('err', err);
  2202. competencyLoading.value = false;
  2203. })
  2204. };
  2205. /* 工作地点 */
  2206. // 工作地点选项
  2207. const locationOptions = getPCAData();
  2208. // 修改处理地点选择变化的函数
  2209. const handleLocationChange = (value: any) => {
  2210. console.log('地址选择变化:', value);
  2211. if (value && Array.isArray(value) && value.length > 0) {
  2212. const [provinceCode, cityCode, districtCode] = value;
  2213. const province = locationOptions.find(p => p.code === provinceCode);
  2214. let city, district;
  2215. if (province && province.children) {
  2216. city = province.children.find(c => c.code === cityCode);
  2217. if (city && city.children) {
  2218. district = city.children.find(d => d.code === districtCode);
  2219. }
  2220. }
  2221. /* // 保存名称
  2222. formData.location_names = [
  2223. province?.name || '',
  2224. city?.name || '',
  2225. district?.name || ''
  2226. ].filter(Boolean);
  2227. // 更新各个字段
  2228. formData.province = province?.name || '';
  2229. formData.city = city?.name || '';
  2230. formData.district = district?.name || '';
  2231. formData.location_str = formData.location_names.join('/'); */
  2232. } else {
  2233. /* formData.location_names = [];
  2234. formData.province = '';
  2235. formData.city = '';
  2236. formData.district = '';
  2237. formData.location_str = ''; */
  2238. }
  2239. };
  2240. const previewDialog = ref(false)
  2241. const selectedApplications = ref([])
  2242. // 从 selectedApplications 收敛唯一标签(兼容多字段)
  2243. const uniqueSelectedTags = computed(() => {
  2244. const map = new Map<string | number, any>();
  2245. const pushTag = (tag: any) => {
  2246. if (!tag) return;
  2247. const idOrName = tag.id ?? tag.name;
  2248. if (idOrName == null) return;
  2249. const key = String(idOrName);
  2250. if (!map.has(key)) {
  2251. map.set(key, { id: tag.id, name: tag.name ?? tag.label ?? tag.text ?? tag.title ?? '' });
  2252. }
  2253. };
  2254. (selectedApplications.value as any[]).forEach((item: any) => {
  2255. // 处理心理问题标签(question_form: 4)
  2256. if (item.question_form === 4) {
  2257. // 心理问题可能有不同的标签字段结构
  2258. const psychologicalTags = item.psychological_tags || item.mental_tags || item.tags || [];
  2259. if (Array.isArray(psychologicalTags) && psychologicalTags.length > 0) {
  2260. psychologicalTags.forEach(pushTag);
  2261. } else {
  2262. // 如果心理问题没有标签,添加默认的"心理测试"标签
  2263. pushTag({ id: 'psychological_default', name: '心理测试', title: '心理测试' });
  2264. }
  2265. } else {
  2266. // 其他问题类型的标签处理
  2267. const tags = item.tags || item.competency_tags || [];
  2268. if (Array.isArray(tags)) {
  2269. tags.forEach(pushTag);
  2270. }
  2271. }
  2272. });
  2273. return Array.from(map.values());
  2274. });
  2275. // 获取问题标签的辅助函数
  2276. const getQuestionTags = (question: any) => {
  2277. if (!question) return [];
  2278. // 处理心理问题标签(question_form: 4)
  2279. if (question.question_form === 4) {
  2280. const psychologicalTags = question.psychological_tags || question.mental_tags || question.tags || [];
  2281. // 如果心理问题没有标签,返回默认的"心理测试"标签
  2282. if (psychologicalTags.length === 0) {
  2283. return [{ id: 'psychological_default', name: '心理测试', title: '心理测试' }];
  2284. }
  2285. return psychologicalTags;
  2286. } else {
  2287. // 其他问题类型的标签处理
  2288. return question.tags || question.competency_tags || [];
  2289. }
  2290. };
  2291. /* 预览已选 */
  2292. const handleQuestionSelectPreview = async (type: string) => {
  2293. previewDialog.value = true;
  2294. try {
  2295. const response = await GetSelectedApplications({
  2296. page: 1,
  2297. limit: 100,
  2298. job_id: route.query.id,
  2299. question_form: type
  2300. });
  2301. if (response.data && response.data.items) {
  2302. selectedApplications.value = response.data.items;
  2303. // 调试信息:打印心理问题的数据结构
  2304. if (type === '4') {
  2305. console.log('心理问题数据结构:', response.data.items);
  2306. response.data.items.forEach((item: any, index: number) => {
  2307. console.log(`心理问题 ${index + 1}:`, {
  2308. question: item.question,
  2309. question_form: item.question_form,
  2310. tags: item.tags,
  2311. psychological_tags: item.psychological_tags,
  2312. mental_tags: item.mental_tags,
  2313. competency_tags: item.competency_tags
  2314. });
  2315. });
  2316. }
  2317. }
  2318. } catch (error) {
  2319. console.error('获取职位申请数据失败:', error);
  2320. ElMessage.error('获取职位申请数据失败');
  2321. }
  2322. }
  2323. const handleIsRequiredCorrectChange = (id: number, value: boolean) => {
  2324. // UpdateObj({
  2325. // id: id,
  2326. // is_required_correct: value
  2327. // }).then((res:any) => {
  2328. // console.log('res', res);
  2329. // })
  2330. };
  2331. // 当前选择的添加位置
  2332. const currentAddIndex = ref(-1);
  2333. // 是否显示选项菜单
  2334. const showOptionsMenu = ref(false);
  2335. // 选项菜单的位置
  2336. const menuPosition = reactive({ top: '0px', left: '0px' });
  2337. // AI考察对话框显示状态
  2338. const showAIVideoDialog = ref(false);
  2339. // AI考察配置选项
  2340. const aiVideoOptions = reactive({
  2341. qualityControl: false,
  2342. teamwork: false,
  2343. problemSolving: false,
  2344. dataAnalysis: false,
  2345. positionMatch: false,
  2346. proactiveness: false,
  2347. basicAbility: false,
  2348. communication: false,
  2349. serviceAwareness: false,
  2350. xxxx: false
  2351. });
  2352. // 当前步骤
  2353. const currentStep = ref(1);
  2354. // 选中的能力列表
  2355. const selectedAbilities = ref([
  2356. {
  2357. label: '沟通能力',
  2358. value: 'communication',
  2359. level: 'CC',
  2360. description: '能够清晰、准确地表达想法,善于倾听和理解他人的观点,具备良好的书面和口头表达能力。',
  2361. expanded: false,
  2362. isNew: true
  2363. },
  2364. {
  2365. label: '团队协作',
  2366. value: 'teamwork',
  2367. level: 'CC',
  2368. description: '能够与团队成员有效合作,共同完成目标,具备良好的团队精神和协作意识。',
  2369. expanded: false,
  2370. isNew: false
  2371. }
  2372. ]);
  2373. const toggleAbilityExpand = (ability: any) => {
  2374. ability.expanded = !ability.expanded;
  2375. };
  2376. // 获取能力标签
  2377. const getAbilityLabel = (key: string) => {
  2378. const abilityMap: Record<string, string> = {
  2379. qualityControl: '质量控制',
  2380. teamwork: '团队协作',
  2381. problemSolving: '问题解决',
  2382. dataAnalysis: '问题分析',
  2383. taskManagement: '分析问题',
  2384. productivity: '生产效率',
  2385. manufacturingSkills: '制造制作技能',
  2386. customerService: '客户服务技能',
  2387. resistanceToPressure: '抗压能力-客户服务',
  2388. workDefinition: '工作稳定-宣誓',
  2389. selfIntroduction: '自我介绍-宣誓',
  2390. detailOrientation: '细节关注',
  2391. logicalThinking: '清晰逻辑思维',
  2392. safetyAwareness: '安全意识',
  2393. customerServiceAwareness: '客户服务意识',
  2394. timeManagement: '时间管理',
  2395. teamCooperation: '团队合作',
  2396. timeManagementSkills: '时间管理能力',
  2397. productEvaluation: '品鉴评估能力',
  2398. basicQualifications: '基本素质',
  2399. networkDeployment: '网位布署',
  2400. remoteCooperation: '泡通协作',
  2401. executionAbility: '执行能力',
  2402. selfDiscipline: '自驱力',
  2403. managementAbility: '管理能力',
  2404. customerOrientation: '客户导向',
  2405. stability: '稳定性',
  2406. salesSkills: '销售技巧',
  2407. responseAbility: '应变能力'
  2408. };
  2409. return abilityMap[key] || key;
  2410. };
  2411. // 排名设置
  2412. const rankSettings = reactive({
  2413. min: 0,
  2414. max: 100,
  2415. minScore: 0.00,
  2416. maxScore: 0.00
  2417. });
  2418. /* 回答不充分追问 */
  2419. const handleFollowUpChange = (value: boolean) => {
  2420. console.log(value);
  2421. };
  2422. // AI模型版本
  2423. const aiModelVersion = ref('standard');
  2424. // 是否启用有效期
  2425. const validityEnabled = ref(false);
  2426. // 在 script setup 部分添加以下响应式变量
  2427. const showVideoLectureDialog = ref(false); // 控制视频宣讲弹窗显示
  2428. const uploadUrl = `${import.meta.env.VITE_API_URL}/api/system/admin_upload/`;
  2429. const videoLectureForm = reactive({
  2430. title: '', // 步骤名称
  2431. videoFile: null, // 视频文件(仅本地选择时临时占位,不入库)
  2432. video_url: '', // 上传成功后的地址
  2433. description: '', // 视频简介
  2434. broadcastType: 'required', // 播放设置:必须看完/看过即可
  2435. });
  2436. // 在 script setup 部分添加新的响应式变量
  2437. const showAIChatDialog = ref(false); // 控制AI实时对话抽屉显示
  2438. // 添加 AI 实时对话配置相关的响应式变量
  2439. const chatDuration = ref(15); // 默认15分钟
  2440. const maxQuestions = ref(5); // 默认5个问题
  2441. // 在 script setup 部分添加以下响应式变量
  2442. const showQuestionDialog = ref(false); // 控制添加问题弹框显示
  2443. const questionForm = reactive({
  2444. id: '',
  2445. content: '', // 题目内容
  2446. competency_tags: [], // 胜任力标签
  2447. weight: 100, // 权重占比,默认100%
  2448. level: 1, // 胜任力级别,默认1级
  2449. type: 'open', // 题目形式:开放问题/单选题/多选题/填空题/自言语
  2450. question_form: 0, // 题目类型:0-开放问题,1-单选题,2-多选题
  2451. options: [], // 选项列表
  2452. position: [], // 适用职位
  2453. category_name: '', // 分类
  2454. tags: [], // 标签
  2455. suggestedDuration: 60, // 建议时长,默认60秒
  2456. scoring_reference: '', // 评分标准
  2457. question_image_url: '', // 题目图片
  2458. blank_answer_template: [],
  2459. tag_ids: []
  2460. });
  2461. // 定义胜任力标签类型
  2462. interface CompetencyTagItem {
  2463. id: string;
  2464. name: string;
  2465. weight: number;
  2466. }
  2467. const getcompetency_tags = (value: any) => {
  2468. return value.map((item: any) => {
  2469. return item.id
  2470. })
  2471. }
  2472. // 胜任力标签选项
  2473. const competencyTagOptions = ref<CompetencyTag[]>([]);
  2474. // 职位类型选项
  2475. const positionOptions = ref([]);
  2476. // 题目分类选项
  2477. const categoryOptions = ref([
  2478. { label: '技术能力', value: 'tech' },
  2479. { label: '管理能力', value: 'management' },
  2480. { label: '沟通能力', value: 'communication' },
  2481. { label: '专业知识', value: 'professional' },
  2482. { label: '个人素质', value: 'personal' }
  2483. ]);
  2484. // 标签选项
  2485. const tagOptions = ref([]);
  2486. // 重置问题表单
  2487. const resetQuestionForm = () => {
  2488. Object.assign(questionForm, {
  2489. id: '',
  2490. content: '',
  2491. competency_tags: [],
  2492. weight: 100,
  2493. level: 1,
  2494. type: 'open',
  2495. question_form: 0,
  2496. options: [],
  2497. position: [],
  2498. category_name: '',
  2499. tags: [],
  2500. suggestedDuration: 60,
  2501. scoring_reference: '',
  2502. question_image_url: '',
  2503. blank_answer_template: [],
  2504. tag_ids: []
  2505. });
  2506. };
  2507. /* 获取职位列表 */
  2508. const getPosList = async () => {
  2509. const response = await api.GetList({
  2510. page: 1,
  2511. page_size: 50
  2512. });
  2513. console.log(response);
  2514. positionOptions.value = response.data.map((item: any) => {
  2515. return {
  2516. label: item.title,
  2517. value: String(item.id)
  2518. }
  2519. })
  2520. }
  2521. // 取消添加问题
  2522. const cancelAddQuestion = () => {
  2523. showQuestionDialog.value = false;
  2524. resetQuestionForm();
  2525. };
  2526. // 确认修改问题
  2527. const confirmAddQuestion = async () => {
  2528. // 表单验证
  2529. if (!questionForm.content) {
  2530. ElMessage.error('请输入题目内容');
  2531. return;
  2532. }
  2533. // 验证胜任力配置
  2534. if (!validateCompetencyConfig()) {
  2535. return;
  2536. }
  2537. try {
  2538. // 创建新的问题对象
  2539. const newQuestion = {
  2540. id: questionForm.id,
  2541. content: questionForm.content,
  2542. competency_tags: questionForm.competency_tags,
  2543. options: questionForm.options,
  2544. weight: questionForm.weight,
  2545. level: questionForm.level,
  2546. question_form: questionForm.question_form,
  2547. position: questionForm.position,
  2548. category: questionForm.category,
  2549. suggestedDuration: questionForm.suggestedDuration,
  2550. scoring_reference: questionForm.scoring_reference,
  2551. question_image_url: questionForm.question_image_url
  2552. };
  2553. console.log(newQuestion);
  2554. // TODO: 调用API保存问题
  2555. /* const response = await UpdateQuestion(newQuestion);
  2556. console.log(response);
  2557. if(response.code === 200){
  2558. ElMessage.success('问题修改成功');
  2559. }else{
  2560. ElMessage.error('问题修改失败');
  2561. } */
  2562. // 关闭弹窗
  2563. showQuestionDialog.value = false;
  2564. // 重置表单
  2565. resetQuestionForm();
  2566. // 提示成功
  2567. /* ElMessage.success('问题修改成功'); */
  2568. // 刷新问题列表
  2569. // await loadQuestions();
  2570. } catch (error) {
  2571. console.error('添加问题失败:', error);
  2572. ElMessage.error('添加问题失败,请重试');
  2573. }
  2574. };
  2575. // 在 script setup 中添加以下响应式变量
  2576. const showDataCollectionDialog = ref(false); // 控制资料收集对话框显示
  2577. const dataCollectionForm = reactive({
  2578. isRequired: false, // 开启评分开关
  2579. questions: [] // 问题列表
  2580. });
  2581. // 修改问题类型定义
  2582. interface QuestionType {
  2583. label: string;
  2584. value: string;
  2585. isVisible?: boolean;
  2586. enableScoring?: boolean;
  2587. isRequired?: boolean;
  2588. }
  2589. // 定义问题列表
  2590. const questionTypes = ref<QuestionType[]>([
  2591. {
  2592. label: 'Q1',
  2593. value: 'q1',
  2594. isVisible: true,
  2595. enableScoring: false,
  2596. isRequired: false
  2597. },
  2598. {
  2599. label: '请问你的毕业院校是哪里?',
  2600. value: 'school',
  2601. isVisible: true,
  2602. enableScoring: false,
  2603. isRequired: true
  2604. }
  2605. ]);
  2606. // 添加简历收集相关的状态和方法
  2607. const showResumeCollectionDialog = ref(false);
  2608. const resumeCollectionForm = reactive({
  2609. title: '简历收集',
  2610. isRequired: true,
  2611. steps: 4, // 默认步骤数
  2612. });
  2613. // 显示简历收集弹窗
  2614. const showResumeCollectionConfig = () => {
  2615. showResumeCollectionDialog.value = true;
  2616. };
  2617. // 取消简历收集配置
  2618. const cancelResumeCollection = () => {
  2619. showResumeCollectionDialog.value = false;
  2620. // 重置表单
  2621. Object.assign(resumeCollectionForm, {
  2622. title: '简历收集',
  2623. isRequired: true,
  2624. steps: 4
  2625. });
  2626. };
  2627. // 确认添加简历收集
  2628. const confirmResumeCollection = () => {
  2629. // 检查是否已存在简历收集步骤
  2630. const existingResumeStep = recruitmentProcess.find(step => step.type === 'resume_collection');
  2631. if (existingResumeStep) {
  2632. // 如果已存在,只需要更新配置并显示该步骤
  2633. existingResumeStep.visible = true;
  2634. existingResumeStep.name = resumeCollectionForm.title;
  2635. existingResumeStep.config = {
  2636. ...resumeCollectionForm
  2637. };
  2638. } else {
  2639. // 如果不存在,创建新的简历收集步骤
  2640. const newStep = {
  2641. id: Date.now(),
  2642. name: resumeCollectionForm.title,
  2643. description: '简历收集',
  2644. active: true,
  2645. type: 'resume_collection',
  2646. config: {
  2647. ...resumeCollectionForm
  2648. },
  2649. visible: true
  2650. };
  2651. // 添加到流程中
  2652. recruitmentProcess.splice(currentAddIndex.value, 0, newStep);
  2653. }
  2654. UpdateConfig({
  2655. id: getConfigId.value,
  2656. require_resume_upload: true
  2657. }).then((res: any) => {
  2658. if (res.code == 2000) {
  2659. // ElMessage.success('新增成功');
  2660. } else {
  2661. ElMessage.error('添加简历收集失败');
  2662. }
  2663. }).catch((error: any) => {
  2664. console.error('添加简历收集失败:', error);
  2665. ElMessage.error('添加简历收集失败');
  2666. })
  2667. // 关闭弹窗
  2668. showResumeCollectionDialog.value = false;
  2669. // 提示成功
  2670. ElMessage.success('简历收集步骤已添加');
  2671. // 重置表单
  2672. cancelResumeCollection();
  2673. };
  2674. // AI自动组题loading状态
  2675. const autoGenerateLoading = ref(false);
  2676. // 题目设置选项
  2677. const questionSettings = reactive({
  2678. enableScoreQuestions: false, // 得分题开关
  2679. enableColorBlindQuestions: false, // 识图题开关
  2680. enableOpenQuestions: false // 开放问题开关
  2681. });
  2682. // 获取职位详情
  2683. const getPositionDetail = async () => {
  2684. try {
  2685. const id = route.query.id;
  2686. if (!id) {
  2687. ElMessage.error('职位ID不存在');
  2688. return;
  2689. }
  2690. // 修复类型错误,创建符合InfoReq类型的对象
  2691. const idValue = Array.isArray(id) ? id[0] : id;
  2692. // 创建一个符合API要求的请求对象
  2693. const infoReq = id
  2694. const response = await api.GetObj(id); // 使用 infoReq 而不是 id
  2695. if (response && response.data) {
  2696. Object.assign(positionData, response.data);
  2697. console.log(positionData);
  2698. competencyTagOptions.value = response.data.competency_tags
  2699. const config = await GetConfig(id);
  2700. if (config && config.data) {
  2701. getConfigId.value = config.data.id;
  2702. // 根据配置状态初始化得分题和识图题开关
  2703. if (config.data.enable_psychological_test !== undefined) {
  2704. questionSettings.enableScoreQuestions = config.data.enable_psychological_test;
  2705. }
  2706. if (config.data.enable_color_blind_test !== undefined) {
  2707. questionSettings.enableColorBlindQuestions = config.data.enable_color_blind_test;
  2708. }
  2709. } else if (config && config.code === 2000 && config.message === "该职位暂无配置") {
  2710. // 当职位暂无配置时,自动创建配置
  2711. try {
  2712. const createResponse = await CreateConfig({
  2713. position_id: id,
  2714. enable_psychological_test: false, // 默认关闭心理测试
  2715. enable_color_blind_test: false, // 默认关闭色盲测试
  2716. digital_human_id: null, // 暂时不设置数字人
  2717. digital_human_opening_speech: '你好,我是本次面试的面试官,欢迎参加本公司的线上面试!面试预计需要15分钟,请你提前安排在网络良好、光线亮度合适、且相对安静的环境参加这次面试以免影响本次面试的结果。如果你在面试过程中遇到问题,请与我们的招聘人员联系。'
  2718. });
  2719. if (createResponse && createResponse.code === 2000) {
  2720. // 创建成功,更新配置ID
  2721. getConfigId.value = createResponse.data?.id || createResponse.data;
  2722. ElMessage.success('已自动创建职位配置');
  2723. // 初始化默认设置
  2724. questionSettings.enableScoreQuestions = false;
  2725. questionSettings.enableColorBlindQuestions = false;
  2726. } else {
  2727. ElMessage.warning('自动创建配置失败,请手动创建');
  2728. }
  2729. } catch (error) {
  2730. console.error('自动创建配置失败:', error);
  2731. ElMessage.warning('自动创建配置失败,请手动创建');
  2732. }
  2733. }
  2734. // 设置职位状态
  2735. positionStatus.value = positionData.status === 1;
  2736. }
  2737. } catch (error) {
  2738. console.error('获取职位详情失败:', error);
  2739. ElMessage.error('获取职位详情失败');
  2740. }
  2741. };
  2742. // 返回列表页
  2743. const goBack = () => {
  2744. router.push('/position/list');
  2745. };
  2746. // 编辑职位
  2747. const handleEdit = () => {
  2748. router.push(`/position/list`);
  2749. };
  2750. // 分享职位
  2751. const handleShare = () => {
  2752. ElMessage.success('分享链接已复制到剪贴板');
  2753. };
  2754. // 修改职位状态
  2755. const handleStatusChange = async (value: boolean) => {
  2756. const id = route.query.id;
  2757. try {
  2758. const newStatus = value ? 1 : 0;
  2759. // 如果是要开启职位,需要先检查视频状态
  2760. if (value) {
  2761. // 检查视频状态
  2762. const videoStatusResponse = await api.CheckQuestionVideosStatus({
  2763. position_id: id,
  2764. tenant_id: Session.get('tenant_id')
  2765. });
  2766. if (videoStatusResponse.code === 2000) {
  2767. const { is_all_completed, total_questions } = videoStatusResponse.data;
  2768. // 检查视频是否全部完成且存在题目
  2769. if (!is_all_completed || total_questions <= 0) {
  2770. ElMessage.warning(`无法开启职位,原因:${total_questions <= 0 ? '当前职位没有设置面试题目' : '面试题目视频未全部生成完成'}`);
  2771. // 恢复原状态
  2772. positionStatus.value = !value;
  2773. return;
  2774. }
  2775. } else {
  2776. ElMessage.error('检查视频状态失败,请稍后重试');
  2777. // 恢复原状态
  2778. positionStatus.value = !value;
  2779. return;
  2780. }
  2781. }
  2782. await api.UpdateObj({
  2783. id: id,
  2784. status: newStatus
  2785. });
  2786. positionData.status = newStatus;
  2787. ElMessage.success(`职位已${value ? '开启' : '关闭'}`);
  2788. } catch (error) {
  2789. console.error('修改状态失败:', error);
  2790. ElMessage.error('修改状态失败');
  2791. // 恢复原状态
  2792. positionStatus.value = !value;
  2793. }
  2794. };
  2795. // 编辑招聘流程
  2796. const handleEditProcess = () => {
  2797. showProcessDialog.value = true;
  2798. };
  2799. // 保存招聘流程
  2800. const saveProcess = () => {
  2801. showProcessDialog.value = false;
  2802. ElMessage.success('招聘流程已更新');
  2803. };
  2804. // 取消编辑
  2805. const cancelEditProcess = () => {
  2806. showProcessDialog.value = false;
  2807. };
  2808. // 删除流程步骤 - 删除后允许重新添加相同类型的步骤
  2809. const deleteProcessStep = (step: any, index: number) => {
  2810. if (step.type == 'ai_video') {
  2811. showAIVideoDialog.value = false;
  2812. }
  2813. if (step.type == 'ai_chat') {
  2814. showAIChatDialog.value = false;
  2815. }
  2816. if (step.type == 'ai_video') {
  2817. UpdateConfig({
  2818. id: getConfigId.value,
  2819. enable_open_questions: false // 如果删除后还有AI考察,则保持为true,否则设为false
  2820. }).then((res: any) => {
  2821. if (res.code == 2000) {
  2822. ElMessage.success('删除成功');
  2823. } else {
  2824. ElMessage.error('删除失败');
  2825. }
  2826. })
  2827. }
  2828. if (step.type == 'psychological_problem') { // 心理测试
  2829. UpdateConfig({
  2830. id: getConfigId.value,
  2831. enable_scoring_questions: false // 删除心理测试时,将enable_scoring_questions设置为false
  2832. }).then((res: any) => {
  2833. console.log(res);
  2834. if (res.code == 2000) {
  2835. // 筛选出心理测试题目的position_id数组
  2836. const psychologicalTestIds = ApplicationsList.value
  2837. .filter(el => el.question_form === 4)
  2838. .map(el => el.question_id)
  2839. .filter(id => id !== undefined && id !== null); // 过滤无效值
  2840. // 只有当存在心理测试题目时才调用删除接口
  2841. console.log(psychologicalTestIds);
  2842. if (psychologicalTestIds.length > 0) {
  2843. DeleteSelectedApplications({
  2844. position_id: route.query.id,
  2845. question_id: psychologicalTestIds
  2846. }).then((respones: any) => {
  2847. console.log(respones);
  2848. if (respones.code == 2000) {
  2849. ApplicationsList.value = []
  2850. }
  2851. })
  2852. }
  2853. ElMessage.success('删除成功');
  2854. } else {
  2855. ElMessage.error('删除失败');
  2856. }
  2857. })
  2858. }
  2859. if (step.type == 'common_sense') { // 处理综合素质考察删除
  2860. UpdateConfig({
  2861. id: getConfigId.value,
  2862. enable_single_choice: false, // 删除综合素质考察时,将enable_scoring_questions设置为
  2863. enable_multiple_choice: false,
  2864. enable_image_choice: false,
  2865. enable_fill_blank: false,
  2866. }).then((res: any) => {
  2867. if (res.code == 2000) {
  2868. ElMessage.success('删除成功');
  2869. // 筛选出心理测试题目的position_id数组
  2870. const psychoTestIds = ApplicationsList.value
  2871. .filter(el => el.question_form !== 4 && el.question_form !== 0)
  2872. .map(el => el.question_id)
  2873. .filter(id => id !== undefined && id !== null); // 过滤无效值
  2874. // 只有当存在心理测试题目时才调用删除接口
  2875. if (psychoTestIds.length > 0) {
  2876. DeleteSelectedApplications({
  2877. position_id: route.query.id,
  2878. question_id: psychoTestIds
  2879. }).then((respones: any) => {
  2880. if (respones.code == 2000) {
  2881. ApplicationsList.value = []
  2882. }
  2883. })
  2884. }
  2885. } else {
  2886. ElMessage.error('删除失败');
  2887. }
  2888. })
  2889. }
  2890. if (step.type == 'resume_collection') {
  2891. UpdateConfig({
  2892. id: getConfigId.value,
  2893. require_resume_upload: false // 删除综合素质考察时,将enable_scoring_questions设置为false
  2894. }).then((res: any) => {
  2895. if (res.code == 2000) {
  2896. ElMessage.success('删除成功');
  2897. } else {
  2898. ElMessage.error('删除失败');
  2899. }
  2900. })
  2901. }
  2902. if (step.type == 'posture_check') {
  2903. UpdateConfig({
  2904. id: getConfigId.value,
  2905. enable_posture_check: false
  2906. }).then((res: any) => {
  2907. if (res.code == 2000) {
  2908. ElMessage.success('删除成功');
  2909. return
  2910. } else {
  2911. ElMessage.error('删除失败');
  2912. }
  2913. })
  2914. }
  2915. if (step.type == 'candidate_questions') {
  2916. UpdateConfig({
  2917. id: getConfigId.value,
  2918. enable_candidate_questions: false
  2919. }).then((res: any) => {
  2920. if (res.code == 2000) {
  2921. ElMessage.success('删除成功');
  2922. return
  2923. } else {
  2924. ElMessage.error('删除失败');
  2925. }
  2926. })
  2927. }
  2928. if (step.type == 'video_presentation') {
  2929. UpdateConfig({
  2930. id: getConfigId.value,
  2931. enable_video_presentation: false
  2932. }).then((res: any) => {
  2933. if (res.code == 2000) {
  2934. ElMessage.success('删除成功');
  2935. return
  2936. } else {
  2937. ElMessage.error('删除失败');
  2938. }
  2939. })
  2940. }
  2941. recruitmentProcess.splice(index, 1);
  2942. console.log('recruitmentProcess', recruitmentProcess);
  2943. };
  2944. // 在特定位置添加流程步骤
  2945. const addProcessStepAt = (index: number) => {
  2946. /* if (recruitmentProcess.length >= 6) {
  2947. ElMessage.warning('最多只能添加6个流程步骤');
  2948. return;
  2949. } */
  2950. const newStep = {
  2951. id: Date.now(),
  2952. name: '新步骤',
  2953. description: '新步骤',
  2954. active: true,
  2955. type: 'custom', // 自定义步骤类型
  2956. visible: true
  2957. };
  2958. recruitmentProcess.splice(index, 0, newStep);
  2959. };
  2960. // 修改原有的添加方法,调用新方法
  2961. const addProcessStep = () => {
  2962. addProcessStepAt(recruitmentProcess.length);
  2963. };
  2964. // 获取状态文本
  2965. const getStatusText = (status: number) => {
  2966. const statusMap: Record<number, string> = {
  2967. 0: '未发布',
  2968. 1: '已发布',
  2969. 2: '已结束'
  2970. };
  2971. return statusMap[status] || '未知状态';
  2972. };
  2973. // 获取状态类型
  2974. const getStatusType = (status: number) => {
  2975. const typeMap: Record<number, string> = {
  2976. 0: 'info',
  2977. 1: 'success',
  2978. 2: 'danger'
  2979. };
  2980. return typeMap[status] || 'info';
  2981. };
  2982. // 获取职位性质文本
  2983. const getJobTypeText = (type: number) => {
  2984. const typeMap: Record<number, string> = {
  2985. 0: '全职',
  2986. 1: '兼职',
  2987. 2: '实习',
  2988. 3: '其他'
  2989. };
  2990. return typeMap[type] || '未知';
  2991. };
  2992. // 显示选项菜单 - 用于在指定位置添加招聘流程步骤
  2993. const showStepOptions = (index: number, event: MouseEvent) => {
  2994. currentAddIndex.value = index;
  2995. showOptionsMenu.value = true;
  2996. console.log(index, event)
  2997. // 计算菜单位置 - 获取按钮元素
  2998. const button = event.currentTarget as HTMLElement;
  2999. // 获取对话框元素,作为相对定位的参考
  3000. const dialogContent = button.closest('.process-dialog-content');
  3001. if (dialogContent) {
  3002. // 获取按钮和对话框的位置信息
  3003. const buttonRect = button.getBoundingClientRect();
  3004. const dialogRect = dialogContent.getBoundingClientRect();
  3005. // 计算相对于对话框的位置
  3006. const top = buttonRect.bottom - dialogRect.top + 5; // 按钮下方5px
  3007. const left = buttonRect.left - dialogRect.left;
  3008. // 设置菜单位置
  3009. menuPosition.top = `${top}px`;
  3010. menuPosition.left = `${left}px`;
  3011. }
  3012. // 阻止事件冒泡
  3013. event.stopPropagation();
  3014. };
  3015. // 修改添加选定类型的步骤方法
  3016. const addSelectedStepType = async (type: string, label: string) => {
  3017. console.log('type', type === 'video_presentation');
  3018. // 检查是否已存在相同类型的步骤,防止重复添加
  3019. /* if (type === 'video_presentation') {
  3020. const exists = recruitmentProcess.some((s: any) => s.type === 'video_presentation' && s.visible !== false);
  3021. if (exists) {
  3022. ElMessage.warning('已存在一个“视频宣讲”步骤');
  3023. showOptionsMenu.value = false;
  3024. return;
  3025. }
  3026. } */
  3027. if (type === 'ai_video') {
  3028. UpdateConfig({
  3029. id: getConfigId.value,
  3030. enable_open_questions: true
  3031. }).then((res: any) => {
  3032. if (res.code == 2000) {
  3033. recruitmentProcess[2].visible = true;
  3034. showOptionsMenu.value = false;
  3035. /* initializeConfigState(); */// 添加初始化调用
  3036. } else {
  3037. ElMessage.error('更新配置失败');
  3038. }
  3039. })
  3040. }
  3041. // 添加综合素质考察类型的处理
  3042. if (type === 'common_sense') {
  3043. UpdateConfig({
  3044. id: getConfigId.value,
  3045. enable_single_choice: true,
  3046. enable_multiple_choice: true,
  3047. enable_image_choice: true,
  3048. enable_fill_blank: true,
  3049. }).then((res: any) => {
  3050. if (res.code == 2000) {
  3051. recruitmentProcess[3].visible = true;
  3052. console.log('recruitmentProcess', recruitmentProcess);
  3053. showOptionsMenu.value = false;
  3054. /* initializeConfigState(); */// 添加初始化调用
  3055. } else {
  3056. ElMessage.error('更新配置失败');
  3057. }
  3058. })
  3059. }
  3060. // 添加心理测试类型的处理
  3061. if (type === 'psychological_problem') {
  3062. UpdateConfig({
  3063. id: getConfigId.value,
  3064. enable_scoring_questions: true
  3065. }).then((res: any) => {
  3066. if (res.code == 2000) {
  3067. recruitmentProcess[4].visible = true;
  3068. showOptionsMenu.value = false;
  3069. /* initializeConfigState(); */
  3070. } else {
  3071. ElMessage.error('更新配置失败');
  3072. }
  3073. })
  3074. }
  3075. if(type === 'candidate_questions'){
  3076. UpdateConfig({
  3077. id: getConfigId.value,
  3078. enable_candidate_questions: true
  3079. }).then((res: any) => {
  3080. if (res.code == 2000) {
  3081. console.log('recruitmentProcess', recruitmentProcess);
  3082. recruitmentProcess[6].visible = true;
  3083. currentAddIndex.value = 6
  3084. } else {
  3085. ElMessage.error('更新配置失败');
  3086. }
  3087. })
  3088. }
  3089. // 添加后渲染提问类型的处理
  3090. if (type === 'post_render_questions') {
  3091. showPostRenderDialog.value = true;
  3092. showOptionsMenu.value = false;
  3093. return;
  3094. }
  3095. // 添加简历收集类型的处理
  3096. if (type === 'resume_collection') {
  3097. showResumeCollectionConfig();
  3098. showOptionsMenu.value = false;
  3099. return;
  3100. }
  3101. //肢体检测
  3102. if (type === 'posture_check') {
  3103. UpdateConfig({
  3104. id: getConfigId.value,
  3105. enable_posture_check: true
  3106. }).then((res: any) => {
  3107. if (res.code == 2000) {
  3108. console.log('recruitmentProcess', recruitmentProcess);
  3109. recruitmentProcess[5].visible = true;
  3110. currentAddIndex.value = 5
  3111. } else {
  3112. ElMessage.error('删除失败');
  3113. }
  3114. })
  3115. }
  3116. // 添加视频宣讲类型的处理
  3117. if (type === 'video_presentation') {
  3118. // 重置编辑索引,表示这是新增模式
  3119. editingVideoLectureIndex.value = -1;
  3120. showVideoLectureDialog.value = true;
  3121. showOptionsMenu.value = false;
  3122. return;
  3123. }
  3124. // 检查是否已存在相同类型的步骤
  3125. const existingStep = recruitmentProcess.find(step => {
  3126. // 对于简历收集和视频宣讲,只检查是否已存在且visible为true的步骤
  3127. if (type === 'resume_collection' || type === 'video_presentation') {
  3128. return step.type === type && step.visible === true;
  3129. }
  3130. // 对于其他类型,检查是否存在相同类型或相同名称的步骤
  3131. if (step.type && step.type === type) {
  3132. return true;
  3133. }
  3134. if (step.name === label) {
  3135. return true;
  3136. }
  3137. return false;
  3138. });
  3139. if (existingStep) {
  3140. ElMessage.warning(`"${label}"步骤已存在,不能重复添加`);
  3141. showOptionsMenu.value = false;
  3142. return;
  3143. }
  3144. // 添加 AI 实时对话类型的处理
  3145. if (type === 'ai_chat') {
  3146. showAIChatDialog.value = true;
  3147. showOptionsMenu.value = false;
  3148. return;
  3149. }
  3150. // 添加资料收集类型的处理
  3151. if (type === 'data_collection') {
  3152. const response = await GetConfig(route.query.id);
  3153. if (response && response.data) {
  3154. getConfigId.value = response.data.id;
  3155. updateFieldConfig(fieldConfig, response.data.profile_fields_config as ProfileFieldsConfig);
  3156. showDataCollectionDialog.value = true;
  3157. showOptionsMenu.value = false;
  3158. } else {
  3159. showDataCollectionDialog.value = true;
  3160. showOptionsMenu.value = false;
  3161. }
  3162. return;
  3163. }
  3164. //添加肢体检测
  3165. if (type === 'posture_check') {
  3166. // showPostureCheckConfig();
  3167. showOptionsMenu.value = false;
  3168. }
  3169. // 添加代码测试类型的处理
  3170. if (type === 'code_test') {
  3171. showCodeTestConfig();
  3172. showOptionsMenu.value = false;
  3173. return;
  3174. }
  3175. // 添加打字测试类型的处理
  3176. if (type === 'typing_test') {
  3177. showTypingTestDialog.value = true;
  3178. showOptionsMenu.value = false;
  3179. return;
  3180. }
  3181. // 添加候选人提问环节的处理
  3182. /* if (type === 'candidate_questions') {
  3183. showCandidateQuestionsDialog.value = true;
  3184. showOptionsMenu.value = false;
  3185. return;
  3186. } */
  3187. // 创建新的招聘流程步骤,包含type字段用于重复检查
  3188. const newStep = {
  3189. id: Date.now(),
  3190. name: label,
  3191. description: label,
  3192. active: true,
  3193. type: type,
  3194. visible: true
  3195. };
  3196. console.log('newStep', newStep);
  3197. // 在指定位置插入新步骤
  3198. recruitmentProcess.splice(type === 'posture_check' ? 5 : currentAddIndex.value, 0, newStep);
  3199. showOptionsMenu.value = false;
  3200. };
  3201. // 下一步
  3202. const nextStep = async () => {
  3203. if (currentStep.value < 3) {
  3204. // 如果从第一步进入第二步,更新已选维度到 selectedAbilities
  3205. if (currentStep.value === 1) {
  3206. // 校验是否选择了胜任力标签
  3207. const selectedTags = competencyTags.value.filter(tag => tag.selected);
  3208. if (selectedTags.length === 0) {
  3209. ElMessage.warning('请至少选择一个胜任力考核维度');
  3210. return; // 阻止进入下一步
  3211. }
  3212. try {
  3213. // 直接使用选中的标签数据构建selectedAbilities数组
  3214. selectedAbilities.value = selectedTags.map((tag) => ({
  3215. label: tag.name,
  3216. value: tag.name.toLowerCase().replace(/\s+/g, '_'),
  3217. id: tag.id, // 使用标签原有的ID
  3218. description: tag.description || '', // 如果没有描述则使用空字符串
  3219. level: 'CC', // 默认等级
  3220. expanded: false,
  3221. isNew: true
  3222. }));
  3223. } catch (error) {
  3224. console.error('处理胜任力标签失败:', error);
  3225. ElMessage.error('处理胜任力标签失败');
  3226. return; // 如果处理失败,不进入下一步
  3227. }
  3228. }
  3229. // 如果从第二步进入第三步,分别处理AI生成和自定义选择的题目
  3230. if (currentStep.value === 2) {
  3231. // 校验是否选择了面试题目
  3232. if (chatQuestions.value.length === 0) {
  3233. ElMessage.warning('请至少添加一个面试题目');
  3234. return; // 阻止进入下一步
  3235. }
  3236. try {
  3237. // 分别处理AI生成的题目和自定义选择的题目
  3238. await handleQuestionsSeparately();
  3239. } catch (error) {
  3240. console.error('保存题目失败:', error);
  3241. ElMessage.error('保存题目失败');
  3242. return; // 如果保存失败,不进入下一步
  3243. }
  3244. }
  3245. // 如果从第三步完成配置,验证面试设置
  3246. if (currentStep.value === 3) {
  3247. const validation = validateInterviewSettings();
  3248. if (!validation.isValid) {
  3249. validation.messages.forEach(message => {
  3250. ElMessage.warning(message);
  3251. });
  3252. return;
  3253. }
  3254. // 打印完整的配置信息
  3255. const completeConfig = getCompleteInterviewConfig();
  3256. console.log('完整的面试配置:', completeConfig);
  3257. }
  3258. currentStep.value++;
  3259. } else {
  3260. finishConfig();
  3261. }
  3262. };
  3263. // 上一步
  3264. const prevStep = () => {
  3265. if (currentStep.value > 1) {
  3266. currentStep.value--;
  3267. }
  3268. };
  3269. // 完成配置
  3270. const finishConfig = async () => {
  3271. // 验证是否选择了面试官
  3272. if (!selectedInterviewer.value || !selectedInterviewer.value.id) {
  3273. ElMessage.warning('请选择面试官形象');
  3274. return; // 阻止继续执行保存操作 interviewerAvatar
  3275. }
  3276. if (!interviewerAvatar.value) {
  3277. ElMessage.warning('请选择面试官');
  3278. return
  3279. }
  3280. // 获取选中的面试官信息
  3281. const selectedInterviewerInfo = {
  3282. id: selectedInterviewer.value.id,
  3283. name: selectedInterviewer.value.name,
  3284. avatar_url: selectedInterviewer.value.avatar_url || selectedInterviewer.value.image
  3285. };
  3286. // 获取面试设置选项
  3287. const interviewSettings = getInterviewSettings();
  3288. // 获取题目设置选项
  3289. const currentQuestionSettings = {
  3290. enableScoreQuestions: questionSettings.enableScoreQuestions,
  3291. enableColorBlindQuestions: questionSettings.enableColorBlindQuestions
  3292. };
  3293. try {
  3294. // 更新职位状态为未发布
  3295. await api.UpdateObj({
  3296. id: route.query.id,
  3297. status: 0
  3298. });
  3299. // 更新本地状态
  3300. positionData.status = 0;
  3301. positionStatus.value = false;
  3302. if (getConfigId.value == 0) {
  3303. const response = await CreateConfig({
  3304. position_id: route.query.id,
  3305. digital_human_id: selectedInterviewer.value.id,
  3306. })
  3307. if (response.code == 2000) {
  3308. ElMessage.success('配置保存成功');
  3309. // 调用获取开场白和视频接口 - 顺序执行,间隔3-5秒
  3310. try {
  3311. const params = { config_id: response.data?.id || response.data };
  3312. // 检查是否有开场白配置
  3313. const hasOpeningSpeech = interviewSettings.beforeStartText &&
  3314. interviewSettings.beforeStartText.trim() !== '';
  3315. if (hasOpeningSpeech) {
  3316. // 有开场白配置,先请求获取开场白接口
  3317. await GetOpeningSpeech(params);
  3318. console.log('开场白生成成功');
  3319. showAIVideoDialog.value = false;
  3320. currentStep.value = 1;
  3321. // 等待3-5秒(这里设置为4秒)
  3322. await new Promise(resolve => setTimeout(resolve, 4000));
  3323. } else {
  3324. console.log('无开场白配置,跳过开场白生成');
  3325. }
  3326. // 请求获取视频接口
  3327. await GetVideo(params);
  3328. ElMessage.success('视频开始生成');
  3329. } catch (error) {
  3330. console.error('获取开场白和视频失败:', error);
  3331. ElMessage.warning('视频生成中,请稍后');
  3332. }
  3333. } else {
  3334. ElMessage.error('配置保存失败');
  3335. }
  3336. } else {
  3337. const response = await UpdateConfig({
  3338. id: getConfigId.value,
  3339. digital_human_id: selectedInterviewer.value.id,
  3340. })
  3341. if (response.code == 2000) {
  3342. ElMessage.success('配置保存成功');
  3343. // 调用获取开场白和视频接口 - 顺序执行,间隔3-5秒
  3344. try {
  3345. const params = { config_id: getConfigId.value };
  3346. // 第一步:请求获取开场白接口
  3347. console.log('开始生成开场白...');
  3348. await GetOpeningSpeech(params);
  3349. console.log('开场白生成成功');
  3350. showAIVideoDialog.value = false;
  3351. currentStep.value = 1;
  3352. // 第二步:等待3-5秒(这里设置为4秒)
  3353. console.log('等待4秒后开始生成视频...');
  3354. await delay(4000);
  3355. // 第三步:请求获取视频接口
  3356. console.log('开始生成视频...');
  3357. await GetVideo(params);
  3358. console.log('视频生成请求已发送');
  3359. ElMessage.success('视频开始生成');
  3360. } catch (error) {
  3361. console.error('获取开场白和视频失败:', error);
  3362. ElMessage.warning('视频生成中,请稍后');
  3363. }
  3364. } else {
  3365. ElMessage.error('配置保存失败');
  3366. }
  3367. }
  3368. } catch (error) {
  3369. console.error('更新职位状态失败:', error);
  3370. ElMessage.error('更新职位状态失败');
  3371. return;
  3372. }
  3373. console.log('保存时选中的面试官:', selectedInterviewerInfo);
  3374. console.log('保存时的面试设置:', interviewSettings);
  3375. console.log('保存时的题目设置:', currentQuestionSettings);
  3376. // 创建新的AI考察步骤
  3377. const newStep = {
  3378. id: Date.now(),
  3379. name: 'AI考察',
  3380. description: `已选择${selectedAbilities.value.length}项能力测评`,
  3381. active: true,
  3382. type: 'ai_video',
  3383. abilities: selectedAbilities.value.map(a => a.value),
  3384. // 添加面试官信息
  3385. interviewer: selectedInterviewerInfo,
  3386. // 添加面试设置信息
  3387. settings: interviewSettings
  3388. };
  3389. // 添加到流程中
  3390. recruitmentProcess.splice(currentAddIndex.value, 0, newStep);
  3391. // 关闭对话框
  3392. showAIVideoDialog.value = false;
  3393. // 重置当前步骤
  3394. currentStep.value = 1;
  3395. // 提示成功
  3396. ElMessage.success('AI考察步骤已添加');
  3397. };
  3398. // 取消AI考察配置
  3399. const cancelAIVideoConfig = () => {
  3400. showAIVideoDialog.value = false;
  3401. currentStep.value = 1; // 重置步骤
  3402. };
  3403. // 跳转到指定步骤
  3404. const jumpToStep = (step: number) => {
  3405. // 确保步骤在有效范围内
  3406. /* if (step >= 1 && step <= 3) {
  3407. currentStep.value = step;
  3408. } */
  3409. };
  3410. // 显示视频宣讲配置弹窗
  3411. const showVideoLectureConfig = () => {
  3412. // 重置编辑索引,表示这是新增模式
  3413. editingVideoLectureIndex.value = -1;
  3414. showVideoLectureDialog.value = true;
  3415. };
  3416. // 取消视频宣讲配置
  3417. const cancelVideoLecture = () => {
  3418. showVideoLectureDialog.value = false;
  3419. // 重置表单
  3420. Object.assign(videoLectureForm, {
  3421. title: '',
  3422. videoFile: null,
  3423. video_url: '',
  3424. description: '',
  3425. broadcastType: 'required'
  3426. });
  3427. };
  3428. // 确认添加视频宣讲
  3429. const confirmVideoLecture = () => {
  3430. if (!videoLectureForm.video_url) {
  3431. ElMessage.warning('请先上传视频文件');
  3432. return;
  3433. }
  3434. // 编辑模式
  3435. if (editingVideoLectureIndex.value >= 0) {
  3436. const step = recruitmentProcess[editingVideoLectureIndex.value];
  3437. step.name = videoLectureForm.title || '视频宣讲';
  3438. step.description = videoLectureForm.description || '视频宣讲';
  3439. step.config = {
  3440. video_url: videoLectureForm.video_url,
  3441. description: videoLectureForm.description,
  3442. broadcastType: videoLectureForm.broadcastType
  3443. };
  3444. ElMessage.success('视频宣讲步骤已更新');
  3445. showVideoLectureDialog.value = false;
  3446. // 重置编辑索引与表单
  3447. editingVideoLectureIndex.value = -1;
  3448. cancelVideoLecture();
  3449. } else {
  3450. // 新增模式 - 先调用UpdateConfig接口
  3451. UpdateConfig({
  3452. id: getConfigId.value,
  3453. enable_video_presentation: true,
  3454. video_presentation_url: videoLectureForm.video_url,
  3455. video_presentation_description: videoLectureForm.description
  3456. }).then((res: any) => {
  3457. if (res.code == 2000) {
  3458. // 创建新的视频宣讲步骤
  3459. const newStep = {
  3460. id: Date.now(),
  3461. name: videoLectureForm.title || '视频宣讲',
  3462. description: videoLectureForm.description || '视频宣讲',
  3463. active: true,
  3464. type: 'video_presentation',
  3465. visible: true,
  3466. config: {
  3467. video_url: videoLectureForm.video_url,
  3468. description: videoLectureForm.description,
  3469. broadcastType: videoLectureForm.broadcastType
  3470. }
  3471. };
  3472. // 添加到流程中
  3473. recruitmentProcess.splice(currentAddIndex.value, 0, newStep);
  3474. ElMessage.success('视频宣讲步骤已添加');
  3475. showVideoLectureDialog.value = false;
  3476. // 重置编辑索引与表单
  3477. editingVideoLectureIndex.value = -1;
  3478. cancelVideoLecture();
  3479. } else {
  3480. ElMessage.error('添加视频宣讲失败');
  3481. }
  3482. }).catch((error: any) => {
  3483. console.error('添加视频宣讲失败:', error);
  3484. ElMessage.error('添加视频宣讲失败');
  3485. });
  3486. }
  3487. };
  3488. // 上传前校验
  3489. const beforeVideoUpload = (file: File) => {
  3490. const isVideo = file.type.startsWith('video/');
  3491. const isLt500M = file.size / 1024 / 1024 < 500;
  3492. if (!isVideo) {
  3493. ElMessage.error('只能上传视频文件');
  3494. return false;
  3495. }
  3496. if (!isLt500M) {
  3497. ElMessage.error('视频大小不能超过500M');
  3498. return false;
  3499. }
  3500. return true;
  3501. };
  3502. // 上传成功
  3503. const handleVideoUploadSuccess = (res: any) => {
  3504. // 兼容后端返回 {url: ...} 或 {data: {url: ...}}
  3505. const url = res?.url || res?.data?.url || res?.data;
  3506. if (url) {
  3507. videoLectureForm.video_url = url;
  3508. ElMessage.success('视频上传成功');
  3509. } else {
  3510. ElMessage.warning('上传成功但未返回地址');
  3511. }
  3512. };
  3513. // 上传失败
  3514. const handleVideoUploadError = () => {
  3515. ElMessage.error('视频上传失败');
  3516. };
  3517. // 添加 AI 实时对话相关方法
  3518. const handleAIChatConfig = () => {
  3519. // 这里添加确认配置的逻辑
  3520. const newStep = {
  3521. id: Date.now(),
  3522. name: 'AI实时对话',
  3523. description: 'AI实时对话',
  3524. active: true,
  3525. type: 'ai_chat'
  3526. };
  3527. recruitmentProcess.splice(currentAddIndex.value, 0, newStep);
  3528. showAIChatDialog.value = false;
  3529. ElMessage.success('AI实时对话步骤已添加');
  3530. };
  3531. const cancelAIChatConfig = () => {
  3532. showAIChatDialog.value = false;
  3533. };
  3534. // 候选人提问环节相关方法 - 用于配置候选人提问环节
  3535. const confirmCandidateQuestions = () => {
  3536. // 验证表单数据
  3537. if (!candidateQuestionsForm.title.trim()) {
  3538. ElMessage.warning('请输入环节名称');
  3539. return;
  3540. }
  3541. if (candidateQuestionsForm.duration <= 0) {
  3542. ElMessage.warning('提问时长必须大于0分钟');
  3543. return;
  3544. }
  3545. if (candidateQuestionsForm.maxQuestions <= 0) {
  3546. ElMessage.warning('最多提问数量必须大于0');
  3547. return;
  3548. }
  3549. // 判断是编辑模式还是新增模式
  3550. if (editingCandidateQuestionsIndex.value >= 0) {
  3551. // 编辑模式:更新现有步骤
  3552. const existingStep = recruitmentProcess[editingCandidateQuestionsIndex.value];
  3553. existingStep.name = candidateQuestionsForm.title;
  3554. existingStep.description = `时长${candidateQuestionsForm.duration}分钟,最多${candidateQuestionsForm.maxQuestions}个问题`;
  3555. existingStep.config = { ...candidateQuestionsForm };
  3556. ElMessage.success('候选人提问环节已更新');
  3557. } else {
  3558. // 新增模式:创建新的候选人提问步骤
  3559. const newStep = {
  3560. id: Date.now(),
  3561. name: candidateQuestionsForm.title,
  3562. description: `时长${candidateQuestionsForm.duration}分钟,最多${candidateQuestionsForm.maxQuestions}个问题`,
  3563. active: true,
  3564. type: 'candidate_questions',
  3565. config: {
  3566. ...candidateQuestionsForm
  3567. },
  3568. visible: true
  3569. };
  3570. // 添加到流程中
  3571. recruitmentProcess.splice(currentAddIndex.value, 0, newStep);
  3572. ElMessage.success('候选人提问环节已添加');
  3573. }
  3574. // 关闭弹窗
  3575. showCandidateQuestionsDialog.value = false;
  3576. // 重置表单为默认值
  3577. resetCandidateQuestionsForm();
  3578. };
  3579. const cancelCandidateQuestions = () => {
  3580. showCandidateQuestionsDialog.value = false;
  3581. resetCandidateQuestionsForm();
  3582. };
  3583. // 重置候选人提问表单为默认值
  3584. const resetCandidateQuestionsForm = () => {
  3585. editingCandidateQuestionsIndex.value = -1; // 重置编辑索引
  3586. Object.assign(candidateQuestionsForm, {
  3587. title: '候选人提问',
  3588. duration: 5,
  3589. maxQuestions: 3,
  3590. description: '候选人可以向面试官提问关于公司、职位或团队的问题',
  3591. isRequired: true,
  3592. allowSkip: false,
  3593. guidelines: [
  3594. '请提出与职位、公司文化或团队相关的问题',
  3595. '避免询问已在职位描述中明确说明的信息',
  3596. '可以询问职业发展机会和团队工作方式'
  3597. ]
  3598. });
  3599. };
  3600. // 添加问题列表数据
  3601. const chatQuestions = ref<ChatQuestion[]>([]);
  3602. // 计算面试总时长
  3603. const totalInterviewDuration = computed(() => {
  3604. return chatQuestions.value.reduce((total, question) => {
  3605. // 根据问题类型分配时长
  3606. switch (question.question_form) {
  3607. case 0: // 开放题
  3608. return total + 60;
  3609. case 1: // 单选题
  3610. return total + 30;
  3611. case 2: // 多选题
  3612. return total + 30;
  3613. default: // 其他类型默认30秒
  3614. return total + 30;
  3615. }
  3616. }, 0);
  3617. });
  3618. // 格式化时长显示(将秒数转换为分钟和秒的格式)
  3619. const formatDuration = computed(() => {
  3620. const seconds = totalInterviewDuration.value;
  3621. if (seconds < 60) {
  3622. return `${seconds}秒`;
  3623. }
  3624. const minutes = Math.floor(seconds / 60);
  3625. const remainingSeconds = seconds % 60;
  3626. return remainingSeconds > 0 ? `${minutes}分${remainingSeconds}秒` : `${minutes}分钟`;
  3627. });
  3628. // 添加问题处理方法
  3629. const handleAddQuestion = () => {
  3630. showQuestionDialog.value = true;
  3631. };
  3632. /* // 取消添加问题
  3633. const cancelAddQuestion = () => {
  3634. showQuestionDialog.value = false;
  3635. // 重置表单
  3636. Object.assign(questionForm, {
  3637. title: '',
  3638. ability: '',
  3639. content: '',
  3640. target: '',
  3641. weight: 100,
  3642. maxAnswers: 1
  3643. });
  3644. }; */
  3645. /* // 确认添加问题
  3646. const confirmAddQuestion = () => {
  3647. if (!questionForm.title || !questionForm.content) {
  3648. ElMessage.warning('请填写必填项');
  3649. return;
  3650. }
  3651. // 创建新问题对象
  3652. const newQuestion = {
  3653. id: Date.now(), // 添加唯一ID
  3654. title: questionForm.title,
  3655. ability: questionForm.ability,
  3656. content: questionForm.content,
  3657. target: questionForm.target,
  3658. weight: questionForm.weight,
  3659. maxAnswers: questionForm.maxAnswers,
  3660. source: 'custom_selected' as const // 标识为自定义题目
  3661. };
  3662. // 添加到问题列表
  3663. chatQuestions.value.push(newQuestion);
  3664. // 关闭弹框
  3665. showQuestionDialog.value = false;
  3666. // 重置表单
  3667. cancelAddQuestion();
  3668. // 提示成功
  3669. ElMessage.success('问题添加成功');
  3670. }; */
  3671. // 导入问题处理方法 - 修改为打开QuestionBankForm
  3672. const handleImportQuestion = () => {
  3673. // 重置表单数据
  3674. questionBankFormData.value = {
  3675. question: '',
  3676. question_form: 0,
  3677. competency_tags: [],
  3678. position_types: [],
  3679. category_id: null,
  3680. tags: [],
  3681. recommended_duration: 60,
  3682. status: 1,
  3683. is_required_correct: false,
  3684. is_system: false,
  3685. sort: 1,
  3686. options: [],
  3687. scoring_reference: '',
  3688. blank_answer_template: []
  3689. };
  3690. isEditingQuestion.value = false;
  3691. showQuestionBankFormDialog.value = true;
  3692. };
  3693. // QuestionBankForm 提交处理
  3694. const handleQuestionBankFormSubmit = async (formData: any) => {
  3695. try {
  3696. // 调用题库API创建题目
  3697. let response
  3698. if (formData.question_form == 6) {
  3699. response = await questionBankApi.AddObjBlank({
  3700. ...formData,
  3701. tenant_id: Session.get('tenant_id')
  3702. });
  3703. } else {
  3704. response = await questionBankApi.AddObj({
  3705. ...formData,
  3706. tenant_id: Session.get('tenant_id')
  3707. });
  3708. }
  3709. console.log(formData);
  3710. if (response && response.code === 2000) {
  3711. ElMessage.success('题目创建成功');
  3712. console.log(response);
  3713. // 将创建的题目添加到当前职位的题目列表中
  3714. if (response.data) {
  3715. const newQuestion = {
  3716. id: response.data.id || response.data,
  3717. title: formData.question,
  3718. content: formData.question,
  3719. question_form: formData.question_form,
  3720. question_form_name: getQuestionFormName(formData.question_form),
  3721. scoring_reference: formData.scoring_reference || '',
  3722. competency_tag_info: formData.competency_tags || [],
  3723. competency_tags: formData.competency_tags || [],
  3724. options: formData.options || [],
  3725. enable_follow_up: false,
  3726. maxAnswers: 1,
  3727. weight: 100,
  3728. source: 'custom_selected' as const,
  3729. question_image_url: formData.question_image_url || ''
  3730. };
  3731. // 添加到题目列表
  3732. chatQuestions.value.push(newQuestion);
  3733. }
  3734. // 关闭对话框
  3735. showQuestionBankFormDialog.value = false;
  3736. } else {
  3737. ElMessage.error('题目创建失败,请重试');
  3738. }
  3739. } catch (error) {
  3740. console.error('创建题目失败:', error);
  3741. ElMessage.error('题目创建失败,请重试');
  3742. }
  3743. };
  3744. // QuestionBankForm 取消处理
  3745. const handleQuestionBankFormCancel = () => {
  3746. showQuestionBankFormDialog.value = false;
  3747. questionBankFormData.value = {};
  3748. };
  3749. // 获取题目形式名称的辅助函数
  3750. const getQuestionFormName = (questionForm: number) => {
  3751. const formMap: { [key: number]: string } = {
  3752. 0: '开放问题',
  3753. 1: '单选题',
  3754. 2: '多选题',
  3755. 3: '识图题',
  3756. 4: '得分题',
  3757. 5: '判断题',
  3758. 6: '填空题'
  3759. };
  3760. return formMap[questionForm] || '未知类型';
  3761. };
  3762. // 在 script setup 中添加
  3763. const editQuestion = (index: number) => {
  3764. if (chatQuestions.value[index].source !== 'ai_generated') {
  3765. console.log(chatQuestions.value[index].id);
  3766. GetQuestionDetail(chatQuestions.value[index].id).then((res: any) => {
  3767. console.log(res);
  3768. if (res.code === 2000) {
  3769. const data = {
  3770. id: res.data.id,
  3771. content: res.data.question,
  3772. question_form: res.data.question_form,
  3773. question_form_name: getQuestionFormName(res.data.question_form),
  3774. scoring_reference: res.data.scoring_reference,
  3775. competency_tag_info: res.data.competency_tag_info,
  3776. competency_tags: res.data.competency_tags,
  3777. options: res.data.options?.map((item: any) => {
  3778. return {
  3779. text: item.option_text,
  3780. is_correct: item.is_correct,
  3781. sort: item.sort
  3782. }
  3783. }),
  3784. enable_follow_up: res.data.enable_follow_up,
  3785. maxAnswers: res.data.maxAnswers,
  3786. weight: res.data.weight,
  3787. question_image_url: res.data.question_image_url,
  3788. blank_answer_template: res.data.blank_answer_template || [],
  3789. tags: res.data.tags,
  3790. category_name: res.data.category_name,
  3791. tag_ids: res.data.tag_ids,
  3792. }
  3793. Object.assign(questionForm, data);
  3794. }
  3795. })
  3796. } else {
  3797. // 编辑问题的逻辑
  3798. const question = chatQuestions.value[index];
  3799. console.log(question);
  3800. Object.assign(questionForm, question);
  3801. console.log(chatQuestions);
  3802. }
  3803. showQuestionDialog.value = true;
  3804. distributeWeightsEvenly()
  3805. };
  3806. const deleteQuestion = (index: number) => {
  3807. ElMessageBox.confirm('确认删除该问题?', '提示', {
  3808. confirmButtonText: '确定',
  3809. cancelButtonText: '取消',
  3810. type: 'warning'
  3811. }).then(() => {
  3812. chatQuestions.value.splice(index, 1);
  3813. ElMessage.success('删除成功');
  3814. });
  3815. };
  3816. const refreshPage = () => {
  3817. window.location.reload();
  3818. };
  3819. // 取消资料收集配置
  3820. const cancelDataCollection = () => {
  3821. // 恢复到备份的原始配置,如果没有备份则使用默认配置
  3822. if (originalFieldConfig.value) {
  3823. nextTick(() => {
  3824. Object.assign(fieldConfig, originalFieldConfig.value);
  3825. });
  3826. console.log(fieldConfig);
  3827. } else {
  3828. nextTick(() => {
  3829. Object.assign(fieldConfig, JSON.parse(JSON.stringify(defaultConfig)));
  3830. })
  3831. console.log('使用默认配置');
  3832. }
  3833. // 清理备份并重新启用自动保存
  3834. originalFieldConfig.value = null;
  3835. enableAutoSave.value = true;
  3836. showDataCollectionDialog.value = false;
  3837. refreshPage()
  3838. // 重新获取职位详情数据
  3839. /* getPositionDetail(); */
  3840. // 重置表单
  3841. /* Object.assign(dataCollectionForm, {
  3842. isRequired: false,
  3843. questions: []
  3844. }); */
  3845. };
  3846. // 确认添加资料收集
  3847. const confirmDataCollection = () => {
  3848. /* if (dataCollectionForm.questions.length === 0) {
  3849. ElMessage.warning('请至少选择一个问题');
  3850. return;
  3851. } */
  3852. // 创建新的资料收集步骤
  3853. const newStep = {
  3854. id: Date.now(),
  3855. name: '资料收集',
  3856. description: `已选择${dataCollectionForm.questions.length}个问题`,
  3857. active: true,
  3858. type: 'data_collection',
  3859. config: {
  3860. ...dataCollectionForm,
  3861. questions: dataCollectionForm.questions.map(q => {
  3862. // 修复 find 方法的使用
  3863. const question = questionTypes.value.find(type => type.value === q);
  3864. return {
  3865. type: q,
  3866. title: question?.label || '',
  3867. required: dataCollectionForm.isRequired
  3868. };
  3869. })
  3870. }
  3871. };
  3872. // 添加到流程中
  3873. recruitmentProcess.splice(currentAddIndex.value, 0, newStep);
  3874. // 关闭弹窗
  3875. showDataCollectionDialog.value = false;
  3876. // 提示成功
  3877. ElMessage.success('资料收集步骤已添加');
  3878. // 重置表单
  3879. cancelDataCollection();
  3880. };
  3881. const ApplicationsList = ref([])
  3882. const init = async () => {
  3883. const response = await GetSelectedApplications({
  3884. page: 1,
  3885. limit: 100,
  3886. job_id: route.query.id
  3887. });
  3888. if (response.data && response.data.items) {
  3889. ApplicationsList.value = response.data.items;
  3890. }
  3891. console.log(ApplicationsList.value)
  3892. }
  3893. // 使用 onMounted 和 onUnmounted 钩子
  3894. onMounted(() => {
  3895. getPositionDetail();
  3896. getCompetencyList();
  3897. getDigitalList();
  3898. getPosList()
  3899. // 初始化配置状态
  3900. initializeConfigState();
  3901. init()
  3902. });
  3903. // 组件销毁时清理轮询
  3904. onUnmounted(() => {
  3905. // 取消正在进行的轮询
  3906. cancelPolling();
  3907. });
  3908. // 初始化配置状态
  3909. const initializeConfigState = async () => {
  3910. try {
  3911. const id = route.query.id;
  3912. if (id) {
  3913. const config = await GetConfig(id);
  3914. if (config && config.data) {
  3915. // 同步得分题开关状态
  3916. if (config.data.enable_psychological_test !== undefined) {
  3917. questionSettings.enableScoreQuestions = config.data.enable_psychological_test;
  3918. }
  3919. // 同步识图题开关状态
  3920. if (config.data.enable_color_blind_test !== undefined) {
  3921. questionSettings.enableColorBlindQuestions = config.data.enable_color_blind_test;
  3922. }
  3923. interviewSettings.positionDetection = config.data.enable_emotion_analysis;
  3924. // 同步AI考察(开放3问题)开关状态
  3925. recruitmentProcess[3].visible = config.data.question_form_switches.enable_open_questions;
  3926. recruitmentProcess[4].visible = config.data.question_form_switches.enable_single_choice ||
  3927. config.data.question_form_switches.enable_multiple_choice ||
  3928. config.data.question_form_switches.enable_image_choice ||
  3929. config.data.question_form_switches.enable_fill_blank;
  3930. recruitmentProcess[5].visible = config.data.question_form_switches.enable_scoring_questions;
  3931. recruitmentProcess[6].visible = config.data.enable_posture_check;
  3932. recruitmentProcess[7].visible = config.data.question_form_switches.enable_candidate_questions;
  3933. // 可以在这里添加其他配置状态的同步
  3934. recruitmentProcess[1].visible = config.data.require_resume_upload;
  3935. recruitmentProcess[0].visible = config.data.enable_video_presentation;
  3936. console.log('recruitmentProcess', recruitmentProcess);
  3937. }
  3938. }
  3939. } catch (error) {
  3940. console.error('初始化配置状态失败:', error);
  3941. }
  3942. };
  3943. // 添加代码测试相关的状态和方法
  3944. const showCodeTestDialog = ref(false);
  3945. const codeTestForm = reactive({
  3946. title: '代码笔试',
  3947. autoJudge: true,
  3948. selectedPaper: null as Paper | null,
  3949. });
  3950. // 显示代码测试弹窗
  3951. const showCodeTestConfig = () => {
  3952. showCodeTestDialog.value = true;
  3953. };
  3954. // 取消代码测试配置
  3955. const cancelCodeTest = () => {
  3956. showCodeTestDialog.value = false;
  3957. // 重置表单
  3958. Object.assign(codeTestForm, {
  3959. title: '代码笔试',
  3960. autoJudge: true,
  3961. selectedPaper: null
  3962. });
  3963. };
  3964. // 确认添加代码测试
  3965. const confirmCodeTest = () => {
  3966. if (!codeTestForm.selectedPaper) {
  3967. ElMessage.warning('请添加考试试卷');
  3968. return;
  3969. }
  3970. // 创建新的代码测试步骤
  3971. const newStep = {
  3972. id: Date.now(),
  3973. name: codeTestForm.title,
  3974. description: `${codeTestForm.selectedPaper.title}`,
  3975. active: true,
  3976. type: 'code_test',
  3977. config: {
  3978. ...codeTestForm
  3979. }
  3980. };
  3981. // 添加到流程中
  3982. recruitmentProcess.splice(currentAddIndex.value, 0, newStep);
  3983. // 关闭弹窗
  3984. showCodeTestDialog.value = false;
  3985. // 提示成功
  3986. ElMessage.success('代码笔试步骤已添加');
  3987. // 重置表单
  3988. cancelCodeTest();
  3989. };
  3990. // 选择试卷
  3991. const handleSelectPaper = () => {
  3992. // 模拟选择试卷
  3993. codeTestForm.selectedPaper = {
  3994. id: 1,
  3995. title: '前端开发工程师笔试',
  3996. questionCount: 4,
  3997. duration: 60,
  3998. difficulty: '中等难度'
  3999. } as Paper;
  4000. };
  4001. // 查看试卷
  4002. const handleViewPaper = () => {
  4003. ElMessage.info('查看试卷功能开发中');
  4004. };
  4005. // 更换试卷
  4006. const handleChangePaper = () => {
  4007. ElMessage.info('更换试卷功能开发中');
  4008. };
  4009. // 删除试卷
  4010. const handleRemovePaper = () => {
  4011. codeTestForm.selectedPaper = null;
  4012. };
  4013. // 添加打字测试对话框
  4014. const showTypingTestDialog = ref(false);
  4015. const typingTestForm = reactive({
  4016. title: '', // 步骤名称
  4017. duration: 5, // 测试时间,默认5分钟
  4018. content: '', // 测试页面标题
  4019. notes: '', // 注意事项内容
  4020. videoMonitoring: false, // 是否启用视频监控
  4021. passCriteria: {
  4022. speed: false, // 是否通过速度标准
  4023. accuracy: true, // 是否通过正确率标准,默认选中
  4024. },
  4025. selectionMode: 'random', // 文章选择方式:random-随机抽取,self-候选人自主选择
  4026. pageTitle: '', // 测试页面标题
  4027. previewImage: null, // 登录界面预览图
  4028. articles: [] // 文章列表
  4029. });
  4030. // 取消打字测试配置
  4031. const cancelTypingTest = () => {
  4032. showTypingTestDialog.value = false;
  4033. // 重置表单
  4034. Object.assign(typingTestForm, {
  4035. title: '',
  4036. duration: 5,
  4037. content: '',
  4038. notes: '',
  4039. videoMonitoring: false,
  4040. passCriteria: {
  4041. speed: false,
  4042. accuracy: true,
  4043. },
  4044. selectionMode: 'random',
  4045. pageTitle: '',
  4046. previewImage: null,
  4047. articles: []
  4048. });
  4049. };
  4050. // 确认添加打字测试
  4051. const confirmTypingTest = () => {
  4052. // 创建新的打字测试步骤
  4053. const newStep = {
  4054. id: Date.now(),
  4055. name: typingTestForm.title,
  4056. description: `打字测试 ${typingTestForm.duration}分钟`,
  4057. active: true,
  4058. type: 'typing_test',
  4059. config: {
  4060. ...typingTestForm
  4061. }
  4062. };
  4063. // 添加到流程中
  4064. recruitmentProcess.splice(currentAddIndex.value, 0, newStep);
  4065. // 关闭弹窗
  4066. showTypingTestDialog.value = false;
  4067. // 提示成功
  4068. ElMessage.success('打字测试步骤已添加');
  4069. // 重置表单
  4070. cancelTypingTest();
  4071. };
  4072. // 添加职位名称编辑功能
  4073. const isEditingTitle = ref(false);
  4074. const editingTitle = ref('');
  4075. // 开始编辑职位名称
  4076. const startEditTitle = () => {
  4077. editingTitle.value = positionData.title;
  4078. isEditingTitle.value = true;
  4079. };
  4080. // 取消编辑职位名称
  4081. const cancelEditTitle = () => {
  4082. isEditingTitle.value = false;
  4083. };
  4084. // 保存编辑后的职位名称
  4085. const saveTitle = async () => {
  4086. if (!editingTitle.value.trim()) {
  4087. ElMessage.warning('职位名称不能为空');
  4088. return;
  4089. }
  4090. try {
  4091. const id = route.query.id;
  4092. await api.UpdateObj({
  4093. id: id,
  4094. title: editingTitle.value.trim()
  4095. });
  4096. // 更新本地数据
  4097. positionData.title = editingTitle.value.trim();
  4098. isEditingTitle.value = false;
  4099. ElMessage.success('职位名称已更新');
  4100. } catch (error) {
  4101. console.error('更新职位名称失败:', error);
  4102. ElMessage.error('更新职位名称失败');
  4103. }
  4104. };
  4105. /* 详细地址 */
  4106. const isDetailedAddress = ref(false);
  4107. const detailed_address = ref('');
  4108. // 开始编辑职位名称
  4109. const startDetailedAddress = () => {
  4110. detailed_address.value = positionData.detailed_address;
  4111. isDetailedAddress.value = true;
  4112. };
  4113. // 取消编辑职位名称
  4114. const cancelDetailedAddress = () => {
  4115. isDetailedAddress.value = false;
  4116. };
  4117. const saveDetailedAddress = async () => {
  4118. if (!detailed_address.value.trim()) {
  4119. ElMessage.warning('详细地址不能为空');
  4120. return;
  4121. }
  4122. try {
  4123. const id = route.query.id;
  4124. await api.UpdateObj({
  4125. id: id,
  4126. detailed_address: detailed_address.value.trim()
  4127. });
  4128. // 更新本地数据
  4129. positionData.detailed_address = detailed_address.value.trim();
  4130. isDetailedAddress.value = false;
  4131. ElMessage.success('详细地址已更新');
  4132. } catch (error) {
  4133. console.error('更新详细地址失败:', error);
  4134. ElMessage.error('更新详细地址失败');
  4135. }
  4136. }
  4137. // 工作地点编辑
  4138. const isEditingLocation = ref(false);
  4139. const editingLocation = ref('');
  4140. // 开始编辑工作地点
  4141. const startEditLocation = () => {
  4142. if (!positionData.location) {
  4143. editingLocation.value = '';
  4144. } else if (Array.isArray(positionData.location)) {
  4145. editingLocation.value = positionData.location;
  4146. } else if (typeof positionData.location === 'string') {
  4147. try {
  4148. // 移除方括号和单引号,并分割成数组
  4149. const cleanStr = positionData.location.replace(/[\[\]']/g, '');
  4150. const locationArray = cleanStr.split(',').map(item => item.trim());
  4151. // 根据地址数据查找对应的code
  4152. const province = locationOptions.find(p => p.name === locationArray[0]);
  4153. let city, district;
  4154. if (province && province.children) {
  4155. city = province.children.find(c => c.name === locationArray[1]);
  4156. if (city && city.children) {
  4157. district = city.children.find(d => d.name === locationArray[2]);
  4158. }
  4159. }
  4160. editingLocation.value = [
  4161. province?.code,
  4162. city?.code,
  4163. district?.code
  4164. ].filter(Boolean);
  4165. } catch (e) {
  4166. console.error('解析地址失败:', e);
  4167. editingLocation.value = '';
  4168. }
  4169. }
  4170. isEditingLocation.value = true;
  4171. };
  4172. // 取消编辑工作地点
  4173. const cancelEditLocation = () => {
  4174. isEditingLocation.value = false;
  4175. };
  4176. // 保存工作地点
  4177. const saveLocation = async () => {
  4178. try {
  4179. const id = route.query.id;
  4180. // 获取选中地址的名称
  4181. const locationNames = [];
  4182. if (editingLocation.value && Array.isArray(editingLocation.value)) {
  4183. const [provinceCode, cityCode, districtCode] = editingLocation.value;
  4184. const province = locationOptions.find(p => p.code === provinceCode);
  4185. let city, district;
  4186. if (province) {
  4187. locationNames.push(province.name);
  4188. if (province.children) {
  4189. city = province.children.find(c => c.code === cityCode);
  4190. if (city) {
  4191. locationNames.push(city.name);
  4192. if (city.children) {
  4193. district = city.children.find(d => d.code === districtCode);
  4194. if (district) {
  4195. locationNames.push(district.name);
  4196. }
  4197. }
  4198. }
  4199. }
  4200. }
  4201. }
  4202. await api.UpdateObj({
  4203. id: id,
  4204. location: locationNames
  4205. });
  4206. // 更新本地数据
  4207. positionData.location = locationNames;
  4208. isEditingLocation.value = false;
  4209. ElMessage.success('工作地点已更新');
  4210. } catch (error) {
  4211. console.error('更新工作地点失败:', error);
  4212. ElMessage.error('更新工作地点失败');
  4213. }
  4214. };
  4215. // 职位薪资编辑
  4216. const isEditingSalary = ref(false);
  4217. const editingSalaryType = ref('月薪');
  4218. const editingStartSalary = ref(0);
  4219. const editingEndSalary = ref(0);
  4220. // 开始编辑职位薪资
  4221. const startEditSalary = () => {
  4222. if (positionData.salary_range) {
  4223. const parts = positionData.salary_range.split(' ');
  4224. if (parts.length >= 2) {
  4225. editingSalaryType.value = parts[0];
  4226. const range = parts[1].split('-');
  4227. if (range.length === 2) {
  4228. editingStartSalary.value = parseInt(range[0]) || 0;
  4229. editingEndSalary.value = parseInt(range[1]) || 0;
  4230. }
  4231. }
  4232. } else {
  4233. editingSalaryType.value = '月薪';
  4234. editingStartSalary.value = 0;
  4235. editingEndSalary.value = 0;
  4236. }
  4237. isEditingSalary.value = true;
  4238. };
  4239. // 取消编辑职位薪资
  4240. const cancelEditSalary = () => {
  4241. isEditingSalary.value = false;
  4242. };
  4243. // 更新薪资范围
  4244. const updateSalaryRange = (isBlur = false) => {
  4245. // 验证薪资范围
  4246. if (editingStartSalary.value && editingEndSalary.value) {
  4247. const startSalary = Number(editingStartSalary.value);
  4248. const endSalary = Number(editingEndSalary.value);
  4249. // 如果起始薪资大于最高薪资,自动调整
  4250. if (startSalary > endSalary) {
  4251. editingEndSalary.value = editingStartSalary.value;
  4252. if (isBlur) {
  4253. ElMessage.warning('起始薪资不能高于最高薪资,已自动调整为相同值');
  4254. } else {
  4255. ElMessage.warning('起始薪资不能高于最高薪资,已自动调整');
  4256. }
  4257. }
  4258. }
  4259. };
  4260. // 失焦时的薪资调整处理
  4261. const handleSalaryBlur = () => {
  4262. // 检查是否需要调整薪资范围
  4263. if (editingStartSalary.value && editingEndSalary.value) {
  4264. const startSalary = Number(editingStartSalary.value);
  4265. const endSalary = Number(editingEndSalary.value);
  4266. // 如果起始薪资大于最高薪资,自动调整
  4267. if (startSalary > endSalary) {
  4268. editingEndSalary.value = editingStartSalary.value;
  4269. ElMessage.warning('起始薪资不能高于最高薪资,已自动调整为相同值');
  4270. }
  4271. }
  4272. // 更新薪资范围显示
  4273. updateSalaryRange(true);
  4274. };
  4275. // 保存职位薪资
  4276. const saveSalary = async () => {
  4277. try {
  4278. const id = route.query.id;
  4279. const salaryRange = `${editingSalaryType.value} ${editingStartSalary.value}-${editingEndSalary.value}`;
  4280. await api.UpdateObj({
  4281. id: id,
  4282. salary_range: salaryRange
  4283. });
  4284. // 更新本地数据
  4285. positionData.salary_range = salaryRange;
  4286. isEditingSalary.value = false;
  4287. ElMessage.success('职位薪资已更新');
  4288. } catch (error) {
  4289. console.error('更新职位薪资失败:', error);
  4290. ElMessage.error('更新职位薪资失败');
  4291. }
  4292. };
  4293. // 工作经验编辑
  4294. const isEditingExperience = ref(false);
  4295. const editingExperience = ref('');
  4296. // 开始编辑工作经验
  4297. const startEditExperience = () => {
  4298. editingExperience.value = positionData.work_experience_required || '';
  4299. isEditingExperience.value = true;
  4300. };
  4301. // 取消编辑工作经验
  4302. const cancelEditExperience = () => {
  4303. isEditingExperience.value = false;
  4304. };
  4305. // 保存工作经验
  4306. const saveExperience = async () => {
  4307. try {
  4308. const id = route.query.id;
  4309. await api.UpdateObj({
  4310. id: id,
  4311. work_experience_required: editingExperience.value
  4312. });
  4313. // 更新本地数据
  4314. positionData.work_experience_required = editingExperience.value;
  4315. isEditingExperience.value = false;
  4316. ElMessage.success('工作经验已更新');
  4317. } catch (error) {
  4318. console.error('更新工作经验失败:', error);
  4319. ElMessage.error('更新工作经验失败');
  4320. }
  4321. };
  4322. // 学历要求编辑
  4323. const isEditingEducation = ref(false);
  4324. const editingEducation = ref('');
  4325. // 开始编辑学历要求
  4326. const startEditEducation = () => {
  4327. editingEducation.value = positionData.education_required || '';
  4328. isEditingEducation.value = true;
  4329. };
  4330. // 取消编辑学历要求
  4331. const cancelEditEducation = () => {
  4332. isEditingEducation.value = false;
  4333. };
  4334. // 保存学历要求
  4335. const saveEducation = async () => {
  4336. try {
  4337. const id = route.query.id;
  4338. await api.UpdateObj({
  4339. id: id,
  4340. education_required: editingEducation.value
  4341. });
  4342. // 更新本地数据
  4343. positionData.education_required = editingEducation.value;
  4344. isEditingEducation.value = false;
  4345. ElMessage.success('学历要求已更新');
  4346. } catch (error) {
  4347. console.error('更新学历要求失败:', error);
  4348. ElMessage.error('更新学历要求失败');
  4349. }
  4350. };
  4351. // 所属部门编辑
  4352. const isEditingDepartment = ref(false);
  4353. const editingDepartment = ref('');
  4354. // 开始编辑所属部门
  4355. const startEditDepartment = () => {
  4356. editingDepartment.value = positionData.department || '';
  4357. isEditingDepartment.value = true;
  4358. };
  4359. // 取消编辑所属部门
  4360. const cancelEditDepartment = () => {
  4361. isEditingDepartment.value = false;
  4362. };
  4363. // 保存所属部门
  4364. const saveDepartment = async () => {
  4365. try {
  4366. const id = route.query.id;
  4367. await api.UpdateObj({
  4368. id: id,
  4369. department: editingDepartment.value.trim()
  4370. });
  4371. // 更新本地数据
  4372. positionData.department = editingDepartment.value.trim();
  4373. isEditingDepartment.value = false;
  4374. ElMessage.success('所属部门已更新');
  4375. } catch (error) {
  4376. console.error('更新所属部门失败:', error);
  4377. ElMessage.error('更新所属部门失败');
  4378. }
  4379. };
  4380. // 截止日期编辑
  4381. const isEditingEndDate = ref(false);
  4382. const editingEndDate = ref('');
  4383. // 开始编辑截止日期
  4384. const startEditEndDate = () => {
  4385. editingEndDate.value = positionData.end_date || '';
  4386. isEditingEndDate.value = true;
  4387. };
  4388. // 取消编辑截止日期
  4389. const cancelEditEndDate = () => {
  4390. isEditingEndDate.value = false;
  4391. };
  4392. // 保存截止日期
  4393. const saveEndDate = async () => {
  4394. try {
  4395. const id = route.query.id;
  4396. await api.UpdateObj({
  4397. id: id,
  4398. end_date: editingEndDate.value
  4399. });
  4400. // 更新本地数据
  4401. positionData.end_date = editingEndDate.value;
  4402. isEditingEndDate.value = false;
  4403. ElMessage.success('截止日期已更新');
  4404. } catch (error) {
  4405. console.error('更新截止日期失败:', error);
  4406. ElMessage.error('更新截止日期失败');
  4407. }
  4408. };
  4409. // 职位要求编辑
  4410. const isEditingRequirements = ref(false);
  4411. const editingRequirements = ref('');
  4412. // 开始编辑职位要求
  4413. const startEditRequirements = () => {
  4414. editingRequirements.value = positionData.requirements || '';
  4415. isEditingRequirements.value = true;
  4416. };
  4417. // 取消编辑职位要求
  4418. const cancelEditRequirements = () => {
  4419. isEditingRequirements.value = false;
  4420. };
  4421. // 保存职位要求
  4422. const saveRequirements = async () => {
  4423. try {
  4424. const id = route.query.id;
  4425. await api.UpdateObj({
  4426. id: id,
  4427. requirements: editingRequirements.value
  4428. });
  4429. // 更新本地数据
  4430. positionData.requirements = editingRequirements.value;
  4431. isEditingRequirements.value = false;
  4432. ElMessage.success('职位要求已更新');
  4433. } catch (error) {
  4434. console.error('更新职位要求失败:', error);
  4435. ElMessage.error('更新职位要求失败');
  4436. }
  4437. };
  4438. // 职位描述编辑
  4439. const isEditingDescription = ref(false);
  4440. const editingDescription = ref('');
  4441. // 开始编辑职位描述
  4442. const startEditDescription = () => {
  4443. editingDescription.value = positionData.description || '';
  4444. isEditingDescription.value = true;
  4445. };
  4446. // 取消编辑职位描述
  4447. const cancelEditDescription = () => {
  4448. isEditingDescription.value = false;
  4449. };
  4450. // 保存职位描述
  4451. const saveDescription = async () => {
  4452. try {
  4453. const id = route.query.id;
  4454. await api.UpdateObj({
  4455. id: id,
  4456. description: editingDescription.value
  4457. });
  4458. // 更新本地数据
  4459. positionData.description = editingDescription.value;
  4460. isEditingDescription.value = false;
  4461. ElMessage.success('职位描述已更新');
  4462. } catch (error) {
  4463. console.error('更新职位描述失败:', error);
  4464. ElMessage.error('更新职位描述失败');
  4465. }
  4466. };
  4467. // 职位性质编辑
  4468. const isEditingJobType = ref(false);
  4469. const editingJobType = ref(0);
  4470. // 开始编辑职位性质
  4471. const startEditJobType = () => {
  4472. editingJobType.value = positionData.job_type || 0;
  4473. isEditingJobType.value = true;
  4474. };
  4475. // 取消编辑职位性质
  4476. const cancelEditJobType = () => {
  4477. isEditingJobType.value = false;
  4478. };
  4479. // 保存职位性质
  4480. const saveEditJobType = async () => {
  4481. try {
  4482. const id = route.query.id;
  4483. await api.UpdateObj({
  4484. id: id,
  4485. job_type: editingJobType.value
  4486. });
  4487. // 更新本地数据
  4488. positionData.job_type = editingJobType.value;
  4489. isEditingJobType.value = false;
  4490. ElMessage.success('职位性质已更新');
  4491. } catch (error) {
  4492. console.error('更新职位性质失败:', error);
  4493. ElMessage.error('更新职位性质失败');
  4494. }
  4495. };
  4496. // 添加能力标签相关的响应式变量
  4497. const competencyTags = ref<CompetencyTag[]>([]);
  4498. const isEditingCompetency = ref(false);
  4499. const selectedCompetencies = ref([]);
  4500. // 获取能力标签列表
  4501. const getCompetencyList = async () => {
  4502. try {
  4503. const response = await GetCompetencyList({ page: 1, pageSize: 1000 });
  4504. if (response && response.data) {
  4505. competencyTagOptions.value = response.data.items.map((item: any) => ({
  4506. id: item.id,
  4507. name: item.name,
  4508. selected: false
  4509. }));
  4510. }
  4511. } catch (error) {
  4512. console.error('获取能力标签列表失败:', error);
  4513. ElMessage.error('获取能力标签列表失败');
  4514. }
  4515. };
  4516. // 开始编辑能力标签
  4517. const startEditCompetency = async () => {
  4518. // 先获取所有可用的胜任力标签
  4519. await getCompetencyList();
  4520. // 设置已选中的标签
  4521. selectedCompetencies.value = positionData.competency_tags?.map((tag: any) => tag.id) || [];
  4522. // 标记已选中的标签
  4523. competencyTagOptions.value.forEach(tag => {
  4524. tag.selected = selectedCompetencies.value.includes(tag.id);
  4525. });
  4526. isEditingCompetency.value = true;
  4527. };
  4528. // 取消编辑能力标签
  4529. const cancelEditCompetency = () => {
  4530. isEditingCompetency.value = false;
  4531. };
  4532. // 保存能力标签
  4533. const saveCompetency = async () => {
  4534. try {
  4535. const id = route.query.id;
  4536. await api.UpdateObj({
  4537. id: id,
  4538. competency_tags: selectedCompetencies.value
  4539. });
  4540. // 更新本地数据
  4541. positionData.competency_tags = competencyTagOptions.value
  4542. .filter(tag => selectedCompetencies.value.includes(tag.id))
  4543. .map(tag => ({
  4544. id: tag.id,
  4545. name: tag.name
  4546. }));
  4547. isEditingCompetency.value = false;
  4548. ElMessage.success('能力标签已更新');
  4549. } catch (error) {
  4550. console.error('更新能力标签失败:', error);
  4551. ElMessage.error('更新能力标签失败');
  4552. }
  4553. };
  4554. // 在setup中添加
  4555. const currentEditingStep = ref<AIVideoStep | null>(null);
  4556. /* 获取已配置ID */
  4557. const getConfigId = ref(0)
  4558. const originalFieldConfig = ref(null)
  4559. // 禁用自动保存,防止修改被写入缓存
  4560. const enableAutoSave = ref(false);
  4561. // 编辑AI考察
  4562. const editAIVideo = async (step: AIVideoStep) => {
  4563. // 添加资料收集类型的处理
  4564. if (step.name === '资料收集') {
  4565. const response = await GetConfig(route.query.id);
  4566. if (response && response.data) {
  4567. getConfigId.value = response.data.id;
  4568. // 打开对话框前备份当前配置
  4569. originalFieldConfig.value = JSON.parse(JSON.stringify(fieldConfig));
  4570. console.log(originalFieldConfig.value);
  4571. updateFieldConfig(fieldConfig, response.data.profile_fields_config as ProfileFieldsConfig);
  4572. showDataCollectionDialog.value = true;
  4573. showOptionsMenu.value = false;
  4574. }/*else{
  4575. showDataCollectionDialog.value = true;
  4576. showOptionsMenu.value = false;
  4577. } */
  4578. return;
  4579. }
  4580. if (step.name === 'AI考察') {
  4581. // 如果是删除操作,先删除相关的AI考察问题
  4582. if (step.action === 'delete') {
  4583. await handleDeleteAIQuestions();
  4584. return;
  4585. }
  4586. const response = await GetConfig(route.query.id);
  4587. competencyLoading.value = true;
  4588. currentEditingStep.value = step;
  4589. showAIVideoDialog.value = true;
  4590. currentStep.value = 1;
  4591. try {
  4592. // 先尝试获取职位问题
  4593. const positionQuestionResponse = await GetPositionQuestions({
  4594. job_id: route.query.id,
  4595. page: 1,
  4596. limit: 20
  4597. });
  4598. let hasPositionQuestions = false;
  4599. // 检查 GetPositionQuestions 是否有数据
  4600. if (positionQuestionResponse && positionQuestionResponse.data &&
  4601. positionQuestionResponse.data.dataList &&
  4602. positionQuestionResponse.data.dataList.length > 0) {
  4603. // 如果有数据,使用 GetPositionQuestions 的数据
  4604. chatQuestions.value = positionQuestionResponse.data.dataList.map((item: any) => {
  4605. item.content = item.question;
  4606. item.title = item.question;
  4607. item.competency_tags = item.competency_tag_info
  4608. item.source = 'position_questions';
  4609. return item;
  4610. });
  4611. hasPositionQuestions = true;
  4612. }
  4613. // 如果 GetPositionQuestions 没有数据,则使用 GetDraftList
  4614. if (hasPositionQuestions) {
  4615. const questionResponse = await GetDraftList({
  4616. position_id: route.query.id,
  4617. page: 1,
  4618. limit: 20
  4619. });
  4620. if (questionResponse && questionResponse.data && questionResponse.data.dataList) {
  4621. chatQuestions.value = questionResponse.data.dataList.map((item: any) => {
  4622. item.content = item.question;
  4623. item.title = item.question;
  4624. item.competency_tags = item.competency_tag_info
  4625. item.source = 'ai_generated';
  4626. return item;
  4627. });
  4628. }
  4629. }
  4630. // 新增:获取选定的申请数据
  4631. const selectedApplicationsResponse = await GetSelectedApplications({
  4632. job_id: route.query.id,
  4633. page: 1,
  4634. limit: 50
  4635. });
  4636. // 处理获取的申请数据并合并到 chatQuestions
  4637. if (selectedApplicationsResponse && selectedApplicationsResponse.data) {
  4638. const selectedApplications = selectedApplicationsResponse.data.items
  4639. .filter((app: any) => app.question_form !== 4 && app.question_form !== 5) // 过滤掉 question_form == 4 的项
  4640. .map((app: any) => {
  4641. app.content = app.question, // 假设这里是申请内容
  4642. app.title = app.question, // 假设这里是申请标题
  4643. app.source = 'selected_application',
  4644. app.id = app.question_id
  4645. return app
  4646. });
  4647. // 合并数据并过滤掉重复的问题
  4648. const existingQuestionIds = new Set(chatQuestions.value.map((item: any) => item.content)); // 创建一个集合,存储现有问题的 ID
  4649. const uniqueSelectedApplications = selectedApplications.filter((app: any) => {
  4650. // 只保留那些不在现有问题集合中的申请
  4651. return !existingQuestionIds.has(app.content);
  4652. });
  4653. // 合并数据
  4654. chatQuestions.value = [...chatQuestions.value, ...uniqueSelectedApplications];
  4655. console.log('chatQuestions', chatQuestions.value);
  4656. }
  4657. // 首先尝试获取已有的职位标签
  4658. const existingTags = await GetPositionTags({ position_id: route.query.id, page: 1, limit: 50 });
  4659. console.log('existingTags', existingTags);
  4660. if (existingTags && existingTags.data && existingTags.data.items.length > 0) {
  4661. // 如果存在已有标签,直接使用
  4662. existingTags.data.items.forEach((item: any) => {
  4663. // 设置默认值
  4664. item.importance = 1;
  4665. item.selected = false;
  4666. // 如果存在competency快照数据,则更新选中状态
  4667. if (positionData.latest_competency_snapshot && Array.isArray(positionData.latest_competency_snapshot)) {
  4668. // 使用some方法替代内部循环,提高代码可读性
  4669. item.selected = positionData.latest_competency_snapshot.some(
  4670. (snapshot: any) => snapshot.id === item.id
  4671. );
  4672. }
  4673. });
  4674. competencyTags.value = existingTags.data.items;
  4675. } else {
  4676. // 如果没有已有标签,则生成新的标签
  4677. const response2 = await GenerateCompetency({ position_id: route.query.id });
  4678. if (response2 && response2.data) {
  4679. response2.data.forEach((item: any) => {
  4680. item.selected = false;
  4681. });
  4682. competencyTags.value = response2.data;
  4683. // 保存生成的胜任力标签
  4684. try {
  4685. await SaveCompetency({
  4686. position_id: route.query.id,
  4687. tags: competencyTags.value
  4688. });
  4689. ElMessage.success('胜任力标签保存成功');
  4690. } catch (error) {
  4691. console.error('保存胜任力标签失败:', error);
  4692. ElMessage.error('胜任力标签保存失败');
  4693. }
  4694. }
  4695. }
  4696. if (response && response.data) {
  4697. getConfigId.value = response.data.id;
  4698. }
  4699. } catch (error) {
  4700. console.error('获取或生成能力标签失败:', error);
  4701. ElMessage.error('获取或生成能力标签失败');
  4702. } finally {
  4703. competencyLoading.value = false;
  4704. }
  4705. // 重置选项
  4706. (Object.keys(aiVideoOptions) as Array<keyof typeof aiVideoOptions>).forEach(key => {
  4707. aiVideoOptions[key] = false;
  4708. });
  4709. }
  4710. if (step.name === '综合素质考察') {
  4711. GetQuestionList({ page: 1, limit: 20, question_form: "1,2,3,4,6" }).then((res: any) => {
  4712. if (res.data) {
  4713. questionList.value = res.data.items;
  4714. questionListpage.page = res.data.page;
  4715. questionListpage.pageSize = res.data.limit;
  4716. totalQuestions2.value = res.data.total;
  4717. getTreeData();
  4718. QuestionSelectDialog.value = true;
  4719. }
  4720. })
  4721. }
  4722. if (step.name === '心理测试') {
  4723. GetQuestionList({ page: 1, limit: 30, question_form: 4 }).then((res: any) => {
  4724. if (res.data) {
  4725. questionListQuery.question_form = 4
  4726. questionList.value = res.data.items;
  4727. questionListpage.page = res.data.page;
  4728. questionListpage.pageSize = res.data.limit;
  4729. totalQuestions2.value = res.data.total;
  4730. getDFTreeData();
  4731. showQuestionSelectDialog.value = true;
  4732. }
  4733. })
  4734. }
  4735. };
  4736. // 保存AI考察配置
  4737. const saveAIVideoConfig = async () => {
  4738. try {
  4739. // 获取选中的能力标签
  4740. const selectedAbilities = Object.entries(aiVideoOptions)
  4741. .filter(([_, value]) => value)
  4742. .map(([key]) => key);
  4743. // 更新当前编辑的步骤
  4744. if (currentEditingStep.value) {
  4745. currentEditingStep.value.aiConfig = {
  4746. abilities: selectedAbilities,
  4747. // 可以根据需要添加其他配置
  4748. };
  4749. }
  4750. // 关闭对话框
  4751. showAIVideoDialog.value = false;
  4752. ElMessage.success('AI考察配置已保存');
  4753. } catch (error) {
  4754. console.error('保存AI考察配置失败:', error);
  4755. ElMessage.error('保存失败,请重试');
  4756. }
  4757. };
  4758. // 在 script setup 部分添加以下响应式变量
  4759. const customDimension = ref('');
  4760. const customDescription = ref('');
  4761. const continueAdd = ref(false);
  4762. // 添加新的维度
  4763. const addDimension = async () => {
  4764. // 验证输入
  4765. if (!customDimension.value.trim()) {
  4766. ElMessage.warning('请输入维度名称');
  4767. return;
  4768. }
  4769. // 检查维度名称是否已存在
  4770. const existingDimension = competencyTags.value.find(tag => tag.name === customDimension.value.trim());
  4771. if (existingDimension) {
  4772. ElMessage.warning('该维度名称已存在,请重新输入');
  4773. return;
  4774. }
  4775. try {
  4776. // 1. 首先调用创建标签接口
  4777. const createResponse = await CreateCompetency({
  4778. name: customDimension.value.trim(),
  4779. description: customDescription.value.trim(),
  4780. importance: 1,
  4781. weight: 0
  4782. });
  4783. if (!createResponse.data) {
  4784. throw new Error('创建标签失败');
  4785. }
  4786. const newTagId = createResponse.data.id;
  4787. // 2. 调用绑定接口,将新创建的标签绑定到当前职位
  4788. await BatchUpdateTags({
  4789. tag_ids: [newTagId],
  4790. position_ids: [positionId.value]
  4791. });
  4792. // 创建新的维度对象
  4793. const newDimension = {
  4794. id: newTagId, // 使用API返回的真实ID
  4795. name: customDimension.value.trim(),
  4796. description: customDescription.value.trim(),
  4797. selected: false,
  4798. importance: 1,
  4799. weight: 0,
  4800. isCustom: true // 标记为自定义维度
  4801. };
  4802. // 将新维度添加到competencyTags数组中(添加到备选维度区域)
  4803. competencyTags.value.push(newDimension);
  4804. /* ElMessage.success(`自定义维度"${newDimension.name}"已添加并绑定到当前职位`); */
  4805. // 如果不是继续新增,则重置表单
  4806. if (!continueAdd.value) {
  4807. customDimension.value = '';
  4808. customDescription.value = '';
  4809. } else {
  4810. // 继续新增时只清空输入框,保持焦点便于继续输入
  4811. customDimension.value = '';
  4812. customDescription.value = '';
  4813. }
  4814. console.log('添加新维度成功:', newDimension);
  4815. console.log('当前备选维度列表:', competencyTags.value);
  4816. } catch (error) {
  4817. console.error('添加自定义维度失败:', error);
  4818. ElMessage.error('添加自定义维度失败,请重试');
  4819. }
  4820. }
  4821. // 删除自定义维度
  4822. const deleteCustomDimension = (dimension: any) => {
  4823. ElMessageBox.confirm(
  4824. `确认删除自定义维度"${dimension.name}"吗?`,
  4825. '删除确认',
  4826. {
  4827. confirmButtonText: '确定',
  4828. cancelButtonText: '取消',
  4829. type: 'warning',
  4830. }
  4831. ).then(() => {
  4832. // 从competencyTags数组中移除
  4833. const index = competencyTags.value.findIndex(tag => tag.id === dimension.id && tag.isCustom);
  4834. if (index > -1) {
  4835. competencyTags.value.splice(index, 1);
  4836. // 如果该维度已被选中,也要从已选维度列表中移除
  4837. const selectedIndex = selectedDimensions.value.findIndex(item => item.name === dimension.name);
  4838. if (selectedIndex > -1) {
  4839. selectedDimensions.value.splice(selectedIndex, 1);
  4840. }
  4841. ElMessage.success(`自定义维度"${dimension.name}"已删除`);
  4842. console.log('删除自定义维度成功:', dimension);
  4843. } else {
  4844. ElMessage.error('删除失败,未找到该维度');
  4845. }
  4846. }).catch(() => {
  4847. // 用户取消删除
  4848. console.log('用户取消删除自定义维度');
  4849. });
  4850. };
  4851. const settings = reactive({
  4852. keepVideo: true,
  4853. interruptionMode: '',
  4854. smartFollowUp: false,
  4855. followUpStyle: ''
  4856. });
  4857. interface InterviewerAvatar {
  4858. id: number;
  4859. name: string;
  4860. image: string;
  4861. }
  4862. const interviewerAvatars = ref<InterviewerAvatar[]>([
  4863. {
  4864. id: 1,
  4865. name: 'Jessica',
  4866. image: '/avatars/jessica.png'
  4867. },
  4868. {
  4869. id: 2,
  4870. name: 'Mike',
  4871. image: '/avatars/mike.png'
  4872. },
  4873. {
  4874. id: 3,
  4875. name: 'Sarah',
  4876. image: '/avatars/sarah.png'
  4877. }
  4878. ]);
  4879. const selectedInterviewer = ref(interviewerAvatars.value[0]);
  4880. const interviewerAvatar = ref('');
  4881. // 添加面试官选择处理函数
  4882. const handleInterviewerSelect = (avatar: any) => {
  4883. // 更新选中的面试官
  4884. selectedInterviewer.value = avatar;
  4885. // 打印选中的面试官信息(调试用)
  4886. console.log('选中的面试官信息:', {
  4887. id: avatar.id,
  4888. name: avatar.name,
  4889. avatar_url: avatar.avatar_url || avatar.image
  4890. });
  4891. interviewerAvatar.value = avatar.avatar_url;
  4892. // 可以在这里添加其他处理逻辑
  4893. // 比如:保存到后端、触发其他事件等
  4894. // 提示用户
  4895. ElMessage.success(`已选择面试官:${avatar.name}`);
  4896. };
  4897. // 分别处理AI生成的题目和自定义选择的题目
  4898. const handleQuestionsSeparately = async () => {
  4899. // 分离AI生成的题目和自定义选择的题目
  4900. const aiGeneratedQuestions = chatQuestions.value.filter(q => q.source === 'ai_generated');
  4901. const customSelectedQuestions = chatQuestions.value.filter(q => q.source === 'custom_selected');
  4902. console.log('AI生成的题目:', aiGeneratedQuestions);
  4903. console.log('自定义选择的题目:', customSelectedQuestions);
  4904. // 处理AI生成的题目 - 调用SaveDraft接口
  4905. if (aiGeneratedQuestions.length > 0) {
  4906. await SaveDraft({
  4907. draft_ids: aiGeneratedQuestions.map((draft: any) => draft.id)
  4908. });
  4909. console.log('AI生成的题目已保存到草稿');
  4910. }
  4911. // 处理自定义选择的题目 - 调用其他接口(这里需要根据实际需求调整)
  4912. if (customSelectedQuestions.length > 0) {
  4913. // 构建API请求数据 - 修改为符合API期望的格式
  4914. const requestData = {
  4915. position_id: route.query.id,
  4916. question_id: {
  4917. question_id: customSelectedQuestions
  4918. },
  4919. duration: 60,
  4920. tenant_id: Session.get('tenant_id')
  4921. };
  4922. // 这里可以调用专门处理自定义题目的接口
  4923. // 例如:await SaveCustomQuestions({ question_ids: customSelectedQuestions.map(q => q.id) });
  4924. console.log('自定义选择的题目需要调用专门的接口处理');
  4925. // 临时处理:也使用SaveDraft接口,但可以在后端根据source字段区分处理
  4926. const response = await AddDocument(requestData);
  4927. if (response && response.code === 2000) {
  4928. ElMessage.success('自定义选择的题目已保存');
  4929. } else {
  4930. ElMessage.error('自定义选择的题目保存失败');
  4931. }
  4932. }
  4933. };
  4934. // 获取面试设置选项的函数
  4935. const getInterviewSettings = () => {
  4936. const interviewSettings = {
  4937. // 回答视频保留设置
  4938. keepVideo: settings.keepVideo,
  4939. // 中断后作答限制模式
  4940. interruptionMode: settings.interruptionMode,
  4941. // 追问风格设置
  4942. followUpStyle: settings.followUpStyle,
  4943. // 智慧追问设置
  4944. smartFollowUp: settings.smartFollowUp
  4945. };
  4946. console.log('当前面试设置:', interviewSettings);
  4947. return interviewSettings;
  4948. };
  4949. // 处理面试设置变化的函数
  4950. const handleSettingChange = (settingType: string, value: any) => {
  4951. console.log(`面试设置 ${settingType} 已更改为:`, value);
  4952. // 根据不同的设置类型进行处理
  4953. switch (settingType) {
  4954. case 'keepVideo':
  4955. console.log('回答视频保留设置:', value ? '开启' : '关闭');
  4956. break;
  4957. case 'interruptionMode':
  4958. console.log('中断后作答限制模式:', value === 'previous' ? '从上一问重新作答' : '从当前问题作答');
  4959. break;
  4960. case 'followUpStyle':
  4961. console.log('追问风格:', value);
  4962. break;
  4963. case 'smartFollowUp':
  4964. console.log('智慧追问设置:', value ? '开启' : '关闭');
  4965. break;
  4966. }
  4967. // 可以在这里添加其他处理逻辑
  4968. // 比如:实时保存到后端、触发相关事件等
  4969. };
  4970. // 验证面试设置配置完整性
  4971. const validateInterviewSettings = () => {
  4972. const currentSettings = getInterviewSettings();
  4973. // 检查必要设置是否已配置
  4974. const validation = {
  4975. isValid: true,
  4976. messages: [] as string[]
  4977. };
  4978. // 这里可以添加具体的验证逻辑
  4979. if (currentSettings.interruptionMode === '') {
  4980. validation.isValid = false;
  4981. validation.messages.push('请选择中断后作答限制模式');
  4982. }
  4983. return validation;
  4984. };
  4985. // 拖拽开始处理
  4986. const handleDragStart = (evt: any) => {
  4987. drag.value = true;
  4988. console.log('开始拖拽题目:', evt);
  4989. };
  4990. // 拖拽结束处理
  4991. const handleDragEnd = (evt: any) => {
  4992. drag.value = false;
  4993. console.log('拖拽结束:', evt);
  4994. console.log('当前题目顺序:', chatQuestions.value);
  4995. // 这里可以调用API保存新的题目顺序
  4996. // await saveQuestionOrder(chatQuestions.value);
  4997. ElMessage.success('题目顺序已更新');
  4998. };
  4999. // 获取完整的面试配置信息
  5000. const getCompleteInterviewConfig = () => {
  5001. return {
  5002. // 面试官信息
  5003. interviewer: {
  5004. id: selectedInterviewer.value.id,
  5005. name: selectedInterviewer.value.name,
  5006. avatar_url: selectedInterviewer.value.avatar_url || selectedInterviewer.value.image
  5007. },
  5008. // 面试设置
  5009. settings: getInterviewSettings(),
  5010. // 题目设置
  5011. questionSettings: {
  5012. enableScoreQuestions: questionSettings.enableScoreQuestions,
  5013. enableColorBlindQuestions: questionSettings.enableColorBlindQuestions
  5014. },
  5015. // 选中的能力标签
  5016. abilities: selectedAbilities.value,
  5017. // 题目列表
  5018. questions: chatQuestions.value,
  5019. // 当前步骤
  5020. currentStep: currentStep.value
  5021. };
  5022. };
  5023. /* 获取数字人列表 */
  5024. const getDigitalList = async () => {
  5025. const response = await GetDigitalList({ page: 1, pageSize: 1000 });
  5026. if (response && response.data) {
  5027. interviewerAvatars.value = response.data;
  5028. }
  5029. };
  5030. const handleCompetencyChange = (item: any, val: boolean) => {
  5031. if (val) {
  5032. // 当选中时,添加到已选维度列表
  5033. selectedDimensions.value.push({
  5034. name: item.name,
  5035. description: item.description || '暂无描述' // 如果没有描述则显示默认值
  5036. });
  5037. } else {
  5038. // 当取消选中时,从已选维度列表中移除
  5039. selectedDimensions.value = selectedDimensions.value.filter(
  5040. dimension => dimension.name !== item.name
  5041. );
  5042. }
  5043. };
  5044. // 删除选中的胜任力标签
  5045. const removeSelectedAbility = (index: number) => {
  5046. ElMessageBox.confirm('确认删除这个胜任力标签吗?', '提示', {
  5047. confirmButtonText: '确定',
  5048. cancelButtonText: '取消',
  5049. type: 'warning'
  5050. }).then(() => {
  5051. // 获取被删除的能力标签
  5052. const removedAbility = selectedAbilities.value[index];
  5053. // 从 selectedAbilities 数组中移除
  5054. selectedAbilities.value.splice(index, 1);
  5055. // 同时从 selectedDimensions 数组中移除对应项
  5056. selectedDimensions.value = selectedDimensions.value.filter(
  5057. dimension => dimension.name !== removedAbility.label
  5058. );
  5059. // 更新对应的checkbox状态 - 将对应的competencyTags项的selected设为false
  5060. const competencyTag = competencyTags.value.find((tag: any) => tag.name === removedAbility.label);
  5061. if (competencyTag) {
  5062. competencyTag.selected = false;
  5063. }
  5064. ElMessage.success('胜任力标签已删除');
  5065. }).catch(() => {
  5066. // 用户取消删除
  5067. });
  5068. };
  5069. // 在 script setup 部分添加以下代码
  5070. const selectedDimensions = ref<Array<{ name: string, description: string }>>([]);
  5071. // 创建延迟函数
  5072. const delay = (ms: number): Promise<void> => {
  5073. return new Promise(resolve => setTimeout(resolve, ms));
  5074. };
  5075. // 轮询控制器,用于取消轮询
  5076. let pollingController: { cancelled: boolean } | null = null;
  5077. // 轮询获取草稿列表的函数
  5078. const pollForDraftList = async (params: { tenant_id: number; page: number; limit: number; position_id: string }, maxAttempts: number = 300, interval: number = 2000): Promise<any> => {
  5079. let attempts = 0;
  5080. // 创建轮询控制器
  5081. pollingController = { cancelled: false };
  5082. const currentController = pollingController;
  5083. return new Promise((resolve, reject) => {
  5084. const poll = async () => {
  5085. try {
  5086. // 检查是否已取消轮询
  5087. if (currentController.cancelled) {
  5088. console.log('轮询已被取消');
  5089. resolve(null);
  5090. return;
  5091. }
  5092. attempts++;
  5093. console.log(`第${attempts}次轮询获取草稿列表...`);
  5094. const response = await GetDraftList(params);
  5095. // 再次检查是否已取消轮询(防止在异步请求期间被取消)
  5096. if (currentController.cancelled) {
  5097. console.log('轮询已被取消');
  5098. resolve(null);
  5099. return;
  5100. }
  5101. // 检查是否有数据
  5102. if (response && response.data && response.data.dataList && response.data.dataList.length > 0) {
  5103. console.log('轮询成功,获取到草稿列表数据');
  5104. pollingController = null; // 清理控制器
  5105. resolve(response);
  5106. return;
  5107. }
  5108. // 如果超过最大尝试次数,停止轮询
  5109. if (attempts >= maxAttempts) {
  5110. console.log('轮询达到最大次数,停止轮询');
  5111. pollingController = null; // 清理控制器
  5112. resolve(response); // 返回最后一次的响应,即使没有数据
  5113. return;
  5114. }
  5115. // 继续轮询
  5116. setTimeout(poll, interval);
  5117. } catch (error) {
  5118. console.error(`第${attempts}次轮询失败:`, error);
  5119. // 检查是否已取消轮询
  5120. if (currentController.cancelled) {
  5121. console.log('轮询已被取消');
  5122. resolve(null);
  5123. return;
  5124. }
  5125. // 如果是网络错误且还没超过最大尝试次数,继续尝试
  5126. if (attempts < maxAttempts) {
  5127. setTimeout(poll, interval);
  5128. } else {
  5129. pollingController = null; // 清理控制器
  5130. reject(error);
  5131. }
  5132. }
  5133. };
  5134. // 开始第一次轮询
  5135. poll();
  5136. });
  5137. };
  5138. // 取消轮询的函数
  5139. const cancelPolling = () => {
  5140. if (pollingController) {
  5141. pollingController.cancelled = true;
  5142. pollingController = null;
  5143. console.log('轮询已被手动取消');
  5144. }
  5145. };
  5146. // 处理取消生成题目
  5147. const handleCancelGenerate = () => {
  5148. cancelPolling();
  5149. autoGenerateLoading.value = false;
  5150. ElMessage.info('已取消AI自动组题');
  5151. };
  5152. // 处理得分题开关变化
  5153. const handleScoreQuestionsChange = async (value: boolean) => {
  5154. console.log('得分题开关状态:', value ? '开启' : '关闭');
  5155. if (value) {
  5156. const response = await GetQuestionList({ page: 1, limit: 1000, question_form: 4, tenant_id: Session.get('tenant_id') });
  5157. console.log(response);
  5158. if (response && response.data) {
  5159. BatchBind({
  5160. position_id: route.query.id,
  5161. question_id: { question_id: response.data.items.map((item: any) => item.id) },
  5162. tenant_id: Session.get('tenant_id')
  5163. }).then(async (res: any) => {
  5164. if (res.code === 2000) {
  5165. // 绑定成功后更新配置中的enable_psychological_test字段
  5166. try {
  5167. if (getConfigId.value > 0) {
  5168. await UpdateConfig({
  5169. id: getConfigId.value,
  5170. enable_psychological_test: true
  5171. });
  5172. console.log('配置中的enable_psychological_test字段已更新为true');
  5173. } else {
  5174. const createResponse = await CreateConfig({
  5175. position_id: route.query.id,
  5176. enable_psychological_test: true,
  5177. tenant_id: Session.get('tenant_id')
  5178. });
  5179. if (createResponse && createResponse.code === 2000) {
  5180. getConfigId.value = createResponse.data?.id || createResponse.data;
  5181. console.log('配置创建成功,enable_psychological_test字段已设置为true');
  5182. }
  5183. }
  5184. } catch (error) {
  5185. console.error('更新enable_psychological_test字段失败:', error);
  5186. // 不显示错误消息给用户,避免影响主要功能的用户体验
  5187. }
  5188. ElMessage.success('得分题功能已开启');
  5189. } else {
  5190. ElMessage.error('得分题功能开启失败');
  5191. }
  5192. })
  5193. } else {
  5194. ElMessage.error('获取题目列表失败');
  5195. }
  5196. // 可以在这里添加开启得分题时的特殊逻辑
  5197. // 比如:自动添加得分相关的题目类型、更新界面显示等
  5198. } else {
  5199. try {
  5200. const response = await GetQuestionList({ page: 1, limit: 1000, question_form: 4, tenant_id: 1 });
  5201. if (response && response.data) {
  5202. await BatchUnbind({
  5203. position_id: route.query.id,
  5204. question_id: response.data.items.map((item: any) => item.id),
  5205. tenant_id: Session.get('tenant_id')
  5206. });
  5207. // 解绑成功后更新配置中的enable_psychological_test字段
  5208. if (getConfigId.value > 0) {
  5209. await UpdateConfig({
  5210. id: getConfigId.value,
  5211. enable_psychological_test: false
  5212. });
  5213. console.log('配置中的enable_psychological_test字段已更新为false');
  5214. } else {
  5215. // 如果没有配置ID,创建新配置并设置为false
  5216. const createResponse = await CreateConfig({
  5217. position_id: route.query.id,
  5218. enable_psychological_test: false,
  5219. tenant_id: Session.get('tenant_id')
  5220. });
  5221. if (createResponse && createResponse.code === 2000) {
  5222. getConfigId.value = createResponse.data?.id || createResponse.data;
  5223. console.log('配置创建成功,enable_psychological_test字段已设置为false');
  5224. }
  5225. }
  5226. ElMessage.info('得分题功能已关闭');
  5227. }
  5228. } catch (error) {
  5229. console.error('关闭得分题功能失败:', error);
  5230. ElMessage.error('关闭得分题功能失败');
  5231. }
  5232. // 可以在这里添加关闭得分题时的清理逻辑
  5233. }
  5234. // 可以在这里调用API保存设置状态
  5235. // await saveQuestionSettings(questionSettings);
  5236. };
  5237. // 处理识图题开关变化
  5238. const handleColorBlindQuestionsChange = async (value: boolean) => {
  5239. console.log('识图题开关状态:', value ? '开启' : '关闭');
  5240. if (value) {
  5241. // 开启识图题
  5242. const response = await GetQuestionList({ page: 1, limit: 1000, question_form: 3, tenant_id: 1 }); // 假设识图题的question_form为5
  5243. console.log(response);
  5244. if (response && response.data) {
  5245. BatchBind({
  5246. position_id: route.query.id,
  5247. question_id: { question_id: response.data.items.map((item: any) => item.id) },
  5248. tenant_id: Session.get('tenant_id')
  5249. }).then(async (res: any) => {
  5250. if (res.code === 2000) {
  5251. // 绑定成功后更新配置中的enable_color_blind_test字段
  5252. try {
  5253. if (getConfigId.value > 0) {
  5254. await UpdateConfig({
  5255. id: getConfigId.value,
  5256. enable_color_blind_test: true
  5257. });
  5258. console.log('配置中的enable_color_blind_test字段已更新为true');
  5259. } else {
  5260. const createResponse = await CreateConfig({
  5261. position_id: route.query.id,
  5262. enable_color_blind_test: true,
  5263. tenant_id: Session.get('tenant_id')
  5264. });
  5265. if (createResponse && createResponse.code === 2000) {
  5266. getConfigId.value = createResponse.data?.id || createResponse.data;
  5267. console.log('配置创建成功,enable_color_blind_test字段已设置为true');
  5268. }
  5269. }
  5270. } catch (error) {
  5271. console.error('更新enable_color_blind_test字段失败:', error);
  5272. // 不显示错误消息给用户,避免影响主要功能的用户体验
  5273. }
  5274. ElMessage.success('识图题功能已开启');
  5275. } else {
  5276. ElMessage.error('识图题功能开启失败');
  5277. }
  5278. })
  5279. } else {
  5280. ElMessage.error('获取识图题目列表失败');
  5281. }
  5282. // 可以在这里添加开启识图题时的特殊逻辑
  5283. // 比如:自动添加色盲测试相关题目、设置特殊的评分标准等
  5284. } else {
  5285. // 关闭识图题
  5286. try {
  5287. const response = await GetQuestionList({ page: 1, limit: 1000, question_form: 3, tenant_id: 1 }); // 假设识图题的question_form为5
  5288. if (response && response.data) {
  5289. await BatchUnbind({
  5290. position_id: route.query.id,
  5291. question_id: response.data.items.map((item: any) => item.id),
  5292. tenant_id: Session.get('tenant_id')
  5293. });
  5294. // 解绑成功后更新配置中的enable_color_blind_test字段
  5295. if (getConfigId.value > 0) {
  5296. await UpdateConfig({
  5297. id: getConfigId.value,
  5298. enable_color_blind_test: false
  5299. });
  5300. console.log('配置中的enable_color_blind_test字段已更新为false');
  5301. } else {
  5302. // 如果没有配置ID,创建新配置并设置为false
  5303. const createResponse = await CreateConfig({
  5304. position_id: route.query.id,
  5305. enable_color_blind_test: false,
  5306. tenant_id: Session.get('tenant_id')
  5307. });
  5308. if (createResponse && createResponse.code === 2000) {
  5309. getConfigId.value = createResponse.data?.id || createResponse.data;
  5310. console.log('配置创建成功,enable_color_blind_test字段已设置为false');
  5311. }
  5312. }
  5313. ElMessage.info('识图题功能已关闭');
  5314. }
  5315. } catch (error) {
  5316. console.error('关闭识图题功能失败:', error);
  5317. ElMessage.error('关闭识图题功能失败');
  5318. }
  5319. // 可以在这里添加关闭识图题时的清理逻辑
  5320. }
  5321. };
  5322. // 在 script setup 中添加以下内容
  5323. // 题目选择相关的响应式变量
  5324. const showQuestionSelectDialog = ref(false);
  5325. const questionSearchKeyword = ref('');
  5326. const questionList = ref([]);
  5327. const selectedQuestions = ref([]);
  5328. // QuestionBankForm 相关的状态管理
  5329. const showQuestionBankFormDialog = ref(false);
  5330. const questionBankFormData = ref({});
  5331. const isEditingQuestion = ref(false);
  5332. const totalQuestions = ref(0);
  5333. const totalQuestions2 = ref(0);
  5334. const questionListQuery = reactive({
  5335. page: 1,
  5336. pageSize: 10,
  5337. keyword: '',
  5338. question_form: null as number | null
  5339. });
  5340. const questionListpage = reactive({
  5341. page: 1,
  5342. limit: 20,
  5343. keyword: '',
  5344. question_form: null as number | null
  5345. });
  5346. // 获取题目列表
  5347. const fetchQuestionList = async () => {
  5348. try {
  5349. const params = {
  5350. ...questionListQuery,
  5351. keyword: questionSearchKeyword.value,
  5352. // 只有当选择了题目类型时才传递该参数
  5353. ...(questionListQuery.question_form !== null && { question_form: questionListQuery.question_form })
  5354. };
  5355. const response = await GetQuestionList(params);
  5356. if (response && response.data) {
  5357. console.log(response.data);
  5358. questionList.value = response.data.items;
  5359. totalQuestions2.value = response.data.total;
  5360. }
  5361. } catch (error) {
  5362. console.error('获取题目列表失败:', error);
  5363. ElMessage.error('获取题目列表失败');
  5364. }
  5365. };
  5366. // 处理题目搜索
  5367. const handleQuestionSearch = () => {
  5368. questionListQuery.page = 1;
  5369. fetchQuestionList();
  5370. };
  5371. // 处理分页大小变化
  5372. const handleQuestionSizeChange = (val: number) => {
  5373. questionListQuery.pageSize = val;
  5374. fetchQuestionList();
  5375. };
  5376. // 处理页码变化
  5377. const handleQuestionPageChange = (val: number) => {
  5378. questionListQuery.page = val;
  5379. fetchQuestionList();
  5380. };
  5381. // 处理题目选择变化
  5382. const handleQuestionSelectionChange = (selection: any[]) => {
  5383. selectedQuestions.value = selection;
  5384. };
  5385. // 取消选择题目
  5386. const cancelQuestionSelect = () => {
  5387. showQuestionSelectDialog.value = false;
  5388. selectedQuestions.value = [];
  5389. questionSearchKeyword.value = '';
  5390. };
  5391. const confirmQuestionSelect = async () => {
  5392. // 1. 验证是否选择了题目
  5393. if (selectedQuestions.value.length === 0) {
  5394. ElMessage.warning('请至少选择一个题目');
  5395. return;
  5396. }
  5397. try {
  5398. // 2. 获取选中题目的ID列表
  5399. const selectedQuestionIds = selectedQuestions.value.map(item => item.id);
  5400. if (selectedQuestionIds.length == 0) return
  5401. // 3. 调用AddDocument接口进行绑定
  5402. await AddDocument({
  5403. position_id: route.query.id,
  5404. question_id: selectedQuestionIds,
  5405. duration: 60, // 默认时长
  5406. tenant_id: Session.get('tenant_id')
  5407. });
  5408. // 4. 将选中的题目添加到本地列表中
  5409. const formattedQuestions = selectedQuestions.value.map((question: any) => ({
  5410. id: question.id,
  5411. title: question.question || question.title,
  5412. ability: question.ability || '未设置',
  5413. content: question.content || question.question,
  5414. target: question.target || '未设置',
  5415. competency_tags: question.competency_tags || [],
  5416. question_form_name: question.question_form_name,
  5417. scoring_reference: question.scoring_reference,
  5418. question_form: question.question_form,
  5419. is_required_correct: question.is_required_correct,
  5420. weight: 100,
  5421. maxAnswers: 1,
  5422. enable_follow_up: true,
  5423. options: question.options || [],
  5424. source: 'custom_selected' as const
  5425. }));
  5426. chatQuestions.value.push(...formattedQuestions);
  5427. // 5. 关闭对话框并重置状态
  5428. showQuestionSelectDialog.value = false;
  5429. selectedQuestions.value = [];
  5430. questionSearchKeyword.value = '';
  5431. // 6. 显示成功提示
  5432. ElMessage.success('题目添加成功');
  5433. refreshPage()
  5434. } catch (error: any) {
  5435. console.error('添加题目失败:', error);
  5436. showQuestionSelectDialog.value = false;
  5437. if (error.code === 4000) {
  5438. ElMessage.warning('该题目已绑定,请选择其他题目');
  5439. } else {
  5440. ElMessage.error('添加题目失败,请重试');
  5441. }
  5442. }
  5443. };
  5444. // 字段配置数据
  5445. // 默认配置
  5446. const defaultConfig = {
  5447. // 基本信息
  5448. name: { visible: true, label: '姓名', required: true },
  5449. gender: { visible: true, label: '性别', required: true },
  5450. // three_period: { visible: true, label: '是否为三期', required: true },
  5451. phone: { visible: true, label: '手机号', required: true },
  5452. id_card: { visible: true, label: '身份证号', required: true },
  5453. ethnicity: { visible: true, label: '民族', required: true },
  5454. height: { visible: true, label: '身高(cm)', required: false },
  5455. weight: { visible: true, label: '体重(kg)', required: false },
  5456. current_address: { visible: true, label: '现居住地址', required: true },
  5457. household_location: { visible: true, label: '户口所在地', required: false },
  5458. marital_status: { visible: true, label: '婚育状况', required: false },
  5459. expected_salary: { visible: true, label: '期望薪资(元/月)', required: false },
  5460. /* political_status: { visible: true, label: '政治面貌', required: false }, */
  5461. // 家庭成员信息
  5462. family_member: {
  5463. visible: true,
  5464. label: '家庭成员',
  5465. fields: {
  5466. relation: { visible: true, label: '称谓', required: true },
  5467. name: { visible: true, label: '姓名', required: true },
  5468. workplace: { visible: true, label: '工作单位/家庭地址', required: false },//workplace_address
  5469. position: { visible: true, label: '职务', required: false },
  5470. phone: { visible: true, label: '联系电话', required: true },
  5471. family_contact_person: { visible: true, label: '是否为紧急联系人', required: false }//is_emergency_contact
  5472. }
  5473. },
  5474. // 教育经历
  5475. education: {
  5476. visible: true,
  5477. label: '教育经历',
  5478. fields: {
  5479. start_time: { visible: true, label: '开始时间', required: true },
  5480. end_time: { visible: true, label: '结束时间', required: true },
  5481. school_name: { visible: true, label: '学校名称', required: true },
  5482. major: { visible: true, label: '专业', required: true },
  5483. degree: { visible: true, label: '学历', required: true }
  5484. }
  5485. },
  5486. // 专业技能
  5487. require_professional_skills: { visible: false, label: '专业技能', required: true },
  5488. training_fields_config: { visible: false, label: '受过的培训', required: true },
  5489. // 工作经历
  5490. work_experience: {
  5491. visible: true,
  5492. label: '工作经历',
  5493. fields: {
  5494. start_time: { visible: true, label: '开始时间', required: true },
  5495. end_time: { visible: true, label: '结束时间', required: true },
  5496. company_name: { visible: true, label: '单位名称', required: true },
  5497. company_size: { visible: true, label: '单位人数', required: true },
  5498. department: { visible: true, label: '任职部门', required: true },
  5499. position: { visible: true, label: '担任职务', required: true },
  5500. monthly_salary: { visible: true, label: '月总收入', required: true },
  5501. /* monthly_income:{visible: true, label: '月总收入', required: true}, */
  5502. direct_supervisor: { visible: true, label: '直接领导', required: true },
  5503. supervisor_phone: { visible: true, label: '领导电话', required: true }
  5504. }
  5505. },
  5506. question_form_switches: {
  5507. enable_open_questions: true, // 0: 开放问题
  5508. enable_single_choice: true, // 1: 单选题
  5509. enable_multiple_choice: true, // 2: 多选题
  5510. enable_image_choice: true, // 3: 识图题
  5511. enable_scoring_questions: true, // 4: 得分题
  5512. enable_follow_up_questions: true, // 5: 追加型开放问题
  5513. enable_fill_blank: true // 6: 填空题
  5514. }
  5515. };
  5516. // 当前配置 - 优先从缓存读取,如果没有则使用默认配置
  5517. const fieldConfig = reactive(() => {
  5518. const cachedConfig = loadFieldConfigFromCache(route.query.id as string);
  5519. if (cachedConfig) {
  5520. console.log('从缓存加载字段配置');
  5521. return cachedConfig;
  5522. } else {
  5523. console.log('使用默认字段配置');
  5524. return JSON.parse(JSON.stringify(defaultConfig));
  5525. }
  5526. })();
  5527. // 保存字段配置
  5528. const saveFieldConfig = async () => {
  5529. try {
  5530. // 构建配置数据
  5531. const configData = {
  5532. position_id: route.query.id, // 职位ID
  5533. /* config_name: "咖啡师配置", // 配置名称
  5534. digital_human_id: 4, // 数字人ID */
  5535. // 简历上传配置
  5536. require_resume_upload: true, // 是否要求上传简历
  5537. resume_file_types: ["pdf", "docx", "doc"], // 允许的文件类型
  5538. max_resume_size_mb: 10, // 最大文件大小(MB)
  5539. digital_human_opening_speech: '你好,我是本次面试的面试官,欢迎参加本公司的线上面试!面试预计需要15分钟,请你提前安排在网络良好、光线亮度合适、且相对安静的环境参加这次面试以免影响本次面试的结果。如果你在面试过程中遇到问题,请与我们的招聘人员联系。',
  5540. // 个人信息配置
  5541. require_basic_info: true, // 是否要求填写基本信息
  5542. profile_fields_config: {
  5543. name: { required: fieldConfig.name.required, visible: fieldConfig.name.visible },
  5544. phone: { required: fieldConfig.phone.required, visible: fieldConfig.phone.visible },
  5545. id_card: { required: fieldConfig.id_card.required, visible: fieldConfig.id_card.visible },
  5546. gender: { required: fieldConfig.gender.required, visible: fieldConfig.gender.visible },
  5547. birth_date: { required: false, visible: true },
  5548. /* political_status: { required: fieldConfig.political_status.required, visible: fieldConfig.political_status.visible }, */
  5549. ethnicity: { required: fieldConfig.ethnicity.required, visible: fieldConfig.ethnicity.visible },
  5550. height: { required: fieldConfig.height.required, visible: fieldConfig.height.visible },
  5551. weight: { required: fieldConfig.weight.required, visible: fieldConfig.weight.visible },
  5552. marital_status: { required: fieldConfig.marital_status.required, visible: fieldConfig.marital_status.visible },
  5553. expected_salary: { required: fieldConfig.expected_salary.required, visible: fieldConfig.expected_salary.visible },
  5554. current_address: { required: fieldConfig.current_address.required, visible: fieldConfig.current_address.visible },
  5555. household_location: { required: fieldConfig.household_location.required, visible: fieldConfig.household_location.visible }
  5556. },
  5557. // 教育信息配置
  5558. require_education_info: fieldConfig.education.visible, // 是否要求填写教育信息
  5559. education_fields_config: {
  5560. school_name: { required: fieldConfig.education.fields.school_name.required, visible: fieldConfig.education.fields.school_name.visible },
  5561. major: { required: fieldConfig.education.fields.major.required, visible: fieldConfig.education.fields.major.visible },
  5562. degree: { required: fieldConfig.education.fields.degree.required, visible: fieldConfig.education.fields.degree.visible },
  5563. start_date: { required: fieldConfig.education.fields.start_time.required, visible: fieldConfig.education.fields.start_time.visible },
  5564. end_date: { required: fieldConfig.education.fields.end_time.required, visible: fieldConfig.education.fields.end_time.visible }
  5565. },
  5566. // 工作经验配置
  5567. require_work_experience: fieldConfig.work_experience.visible, // 是否要求填写工作经验
  5568. work_experience_fields_config: {
  5569. company_name: { required: fieldConfig.work_experience.fields.company_name.required, visible: fieldConfig.work_experience.fields.company_name.visible },
  5570. position: { required: fieldConfig.work_experience.fields.position.required, visible: fieldConfig.work_experience.fields.position.visible },
  5571. department: { required: fieldConfig.work_experience.fields.department.required, visible: fieldConfig.work_experience.fields.department.visible },
  5572. start_date: { required: fieldConfig.work_experience.fields.start_time.required, visible: fieldConfig.work_experience.fields.start_time.visible },
  5573. end_date: { required: fieldConfig.work_experience.fields.end_time.required, visible: fieldConfig.work_experience.fields.end_time.visible },
  5574. job_description: { required: true, visible: true },
  5575. achievements: { required: false, visible: true },
  5576. resignation_reason: { required: false, visible: true },
  5577. company_size: { required: fieldConfig.work_experience.fields.company_size.required, visible: fieldConfig.work_experience.fields.company_size.visible },
  5578. monthly_salary: { required: fieldConfig.work_experience.fields.monthly_salary.required, visible: fieldConfig.work_experience.fields.monthly_salary.visible },
  5579. direct_supervisor:{required: fieldConfig.work_experience.fields.direct_supervisor.required, visible: fieldConfig.work_experience.fields.direct_supervisor.visible},
  5580. supervisor_phone: { required: fieldConfig.work_experience.fields.supervisor_phone.required, visible: fieldConfig.work_experience.fields.supervisor_phone.visible }
  5581. },
  5582. // 家庭信息配置
  5583. require_family_info: fieldConfig.family_member.visible, // 是否要求填写家庭信息
  5584. family_fields_config: {
  5585. relation: { required: fieldConfig.family_member.fields.relation.required, visible: fieldConfig.family_member.fields.relation.visible },
  5586. name: { required: fieldConfig.family_member.fields.name.required, visible: fieldConfig.family_member.fields.name.visible },
  5587. workplace: { required: fieldConfig.family_member.fields.workplace.required, visible: fieldConfig.family_member.fields.workplace.visible },
  5588. position: { required: fieldConfig.family_member.fields.position.required, visible: fieldConfig.family_member.fields.position.visible },
  5589. phone: { required: fieldConfig.family_member.fields.phone.required, visible: fieldConfig.family_member.fields.phone.visible },
  5590. family_contact_person: { required: fieldConfig.family_member.fields.family_contact_person.required, visible: fieldConfig.family_member.fields.family_contact_person.visible }
  5591. },
  5592. // 培训经历配置
  5593. require_training_info: fieldConfig.training_fields_config.visible,
  5594. require_professional_skills: fieldConfig.require_professional_skills.visible,
  5595. /* training_fields_config: {
  5596. training_name: { required: true, visible: true },
  5597. institution: { required: true, visible: true },
  5598. start_date: { required: true, visible: true },
  5599. end_date: { required: false, visible: true },
  5600. certificate: { required: false, visible: true }
  5601. },
  5602. */
  5603. // 姿态配置
  5604. enable_posture_check: true,
  5605. posture_check_config: {
  5606. required_photos: ["front", "side", "back"],
  5607. instructions: "请按照提示上传您的全身照片,确保光线充足,背景简洁"
  5608. },
  5609. // 面试配置
  5610. interview_duration_minutes: 45,
  5611. enable_video_recording: true,
  5612. enable_face_recognition: true,
  5613. // 评分配置
  5614. scoring_method: "hybrid",
  5615. auto_analysis_enabled: true,
  5616. // 其他配置
  5617. custom_welcome_message: "欢迎参加咖啡师岗位的面试,请放松心情,准备好开始面试吧!",
  5618. custom_instructions: "本次面试共有10个问题,每个问题有60秒的回答时间,请简洁明了地表达您的观点。",
  5619. notification_config: {
  5620. send_email: true,
  5621. send_sms: false,
  5622. notify_hr: true,
  5623. completion_message: "感谢您完成面试,我们将在3个工作日内给您回复。"
  5624. },
  5625. is_active: true,
  5626. is_template: false
  5627. };
  5628. if (getConfigId.value == 0) {
  5629. } else {
  5630. configData.id = getConfigId.value;
  5631. }
  5632. // 调用后端 API
  5633. if (getConfigId.value == 0) {
  5634. const response = await CreateConfig(configData);
  5635. if (response.code == 2000) {
  5636. ElMessage.success('配置保存成功');
  5637. // 保存到本地缓存
  5638. saveFieldConfigToCache(fieldConfig, route.query.id as string);
  5639. showDataCollectionDialog.value = false;
  5640. showOptionsMenu.value = false;
  5641. } else {
  5642. ElMessage.error('配置保存失败');
  5643. }
  5644. } else {
  5645. //configData.digital_human_opening_speech='你好,我是本次面试的面试官,欢迎参加本公司的线上面试!面试预计需要15分钟,请你提前安排在网络良好、光线亮度合适、且相对安静的环境参加这次面试以免影响本次面试的结果。如果你在面试过程中遇到问题,请与我们的招聘人员联系。'
  5646. const response = await UpdateConfig(configData);
  5647. if (response.code == 2000) {
  5648. ElMessage.success('配置保存成功');
  5649. // 保存到本地缓存
  5650. saveFieldConfigToCache(fieldConfig, route.query.id as string);
  5651. showDataCollectionDialog.value = false;
  5652. showOptionsMenu.value = false;
  5653. } else {
  5654. ElMessage.error('配置保存失败');
  5655. }
  5656. }
  5657. } catch (error) {
  5658. console.error('保存配置失败:', error);
  5659. ElMessage.error('配置保存失败');
  5660. }
  5661. };
  5662. // 获取基本信息字段three_period: fieldConfig.three_period,
  5663. const getBasicFields = () => {
  5664. const basicFields = {
  5665. name: fieldConfig.name,
  5666. id_card: fieldConfig.id_card,
  5667. phone: fieldConfig.phone,
  5668. gender: fieldConfig.gender,
  5669. ethnicity: fieldConfig.ethnicity,
  5670. height: fieldConfig.height,
  5671. weight: fieldConfig.weight,
  5672. current_address: fieldConfig.current_address,
  5673. household_location: fieldConfig.household_location,
  5674. marital_status: fieldConfig.marital_status,
  5675. expected_salary: fieldConfig.expected_salary,
  5676. /* political_status: fieldConfig.political_status */
  5677. };
  5678. return basicFields;
  5679. };
  5680. // 获取专业技能字段
  5681. const getSkillFields = () => {
  5682. const skillFields = {
  5683. skills: fieldConfig.require_professional_skills,
  5684. training: fieldConfig.training_fields_config
  5685. };
  5686. return skillFields;
  5687. };
  5688. // 面试设置弹窗
  5689. const showInterviewSettingsDialog = ref(false);
  5690. const interviewSettings = reactive({
  5691. faceConsistency: true,
  5692. preventScreenshot: true,
  5693. screenSwitchAlert: true,
  5694. positionDetection: true,
  5695. eyeMovementDetection: true,
  5696. reviewInterview: false,
  5697. beforeStart: true,
  5698. beforeStartText: '你好,我是本次面试的面试官,欢迎参加本公司的线上面试!面试预计需要15分钟,请你提前安排在网络良好、光线亮度合适、且相对安静的环境参加这次面试以免影响本次面试的结果。如果你在面试过程中遇到问题,请与我们的招聘人员联系。',
  5699. afterStartReminder: false,
  5700. afterStartText: '本次面试如有15分钟,高效调前后流程,安静好心理',
  5701. disclaimer: false,
  5702. disclaimerText: '本次面试已全部结束,感谢您的参与,请等待后续通知。'
  5703. });
  5704. // 编辑状态变量
  5705. const isEditingBeforeStart = ref(false);
  5706. const isEditingAfterStart = ref(false);
  5707. const isEditingDisclaimer = ref(false);
  5708. // 编辑中的文本内容
  5709. const editingBeforeStartText = ref('');
  5710. const editingAfterStartText = ref('');
  5711. const editingDisclaimerText = ref('');
  5712. // 取消面试设置
  5713. const cancelInterviewSettings = () => {
  5714. // 恢复原始配置
  5715. if (originalInterviewSettings.value && Object.keys(originalInterviewSettings.value).length > 0) {
  5716. Object.assign(interviewSettings, originalInterviewSettings.value);
  5717. console.log('已恢复原始配置:', originalInterviewSettings.value);
  5718. }
  5719. showInterviewSettingsDialog.value = false;
  5720. // 重置编辑状态
  5721. isEditingBeforeStart.value = false;
  5722. isEditingAfterStart.value = false;
  5723. isEditingDisclaimer.value = false;
  5724. // 清空备份
  5725. originalInterviewSettings.value = {};
  5726. refreshPage()
  5727. };
  5728. /* 眼动情绪 */
  5729. const handlePositionDetectionChange = async () => {
  5730. let response;
  5731. response = await UpdateConfig({
  5732. id: getConfigId.value,
  5733. enable_emotion_analysis: interviewSettings.positionDetection
  5734. });
  5735. if (response.code == 2000) {
  5736. // ElMessage.success('配置保存成功');
  5737. } else {
  5738. ElMessage.error('配置保存失败');
  5739. }
  5740. }
  5741. // 确认面试设置
  5742. const confirmInterviewSettings = () => {
  5743. // 这里可以添加保存面试设置的API调用
  5744. // console.log('保存面试设置:', interviewSettings);
  5745. handlePositionDetectionChange()
  5746. ElMessage.success('面试设置已保存');
  5747. // 按职位ID区分保存到 sessionStorage
  5748. const positionId = route.query.id as string;
  5749. const storageKey = `reviewInterview_${positionId}`;
  5750. sessionStorage.setItem(storageKey, String(interviewSettings.reviewInterview));
  5751. console.log(`职位 ${positionId} 的 reviewInterview 已保存:`, interviewSettings.reviewInterview);
  5752. showInterviewSettingsDialog.value = false;
  5753. // 清空备份
  5754. originalInterviewSettings.value = {};
  5755. refreshPage()
  5756. };
  5757. // 备份原始配置
  5758. const originalInterviewSettings = ref({});
  5759. // 编辑面试设置
  5760. const handleEditInterview = async () => {
  5761. try {
  5762. const res = await GetConfig(route.query.id);
  5763. if (res && res.data) {
  5764. // 配置存在的情况
  5765. nextTick(() => {
  5766. // 备份当前配置
  5767. originalInterviewSettings.value = { ...interviewSettings };
  5768. // 按职位ID区分存储和读取 reviewInterview 配置
  5769. const positionId = route.query.id as string;
  5770. const storageKey = `reviewInterview_${positionId}`;
  5771. const storedReviewInterview = sessionStorage.getItem(storageKey);
  5772. // 如果当前职位没有设置过,则使用默认值 false
  5773. interviewSettings.reviewInterview = storedReviewInterview === 'true';
  5774. console.log(`职位 ${positionId} 的 reviewInterview 配置:`, storedReviewInterview, '转换后:', interviewSettings.reviewInterview);
  5775. });
  5776. showInterviewSettingsDialog.value = true;
  5777. getConfigId.value = res.data.id;
  5778. interviewSettings.beforeStartText = res.data.digital_human_opening_speech;
  5779. } else if (res && res.code === 2000 && res.message === "该职位暂无配置") {
  5780. // 当职位暂无配置时,自动创建配置
  5781. try {
  5782. const createResponse = await CreateConfig({
  5783. position_id: route.query.id,
  5784. enable_psychological_test: false,
  5785. enable_color_blind_test: false,
  5786. digital_human_id: null,
  5787. digital_human_opening_speech: '你好,我是本次面试的面试官,欢迎参加本公司的线上面试!面试预计需要15分钟,请你提前安排在网络良好、光线亮度合适、且相对安静的环境参加这次面试以免影响本次面试的结果。如果你在面试过程中遇到问题,请与我们的招聘人员联系。'
  5788. });
  5789. if (createResponse && createResponse.code === 2000) {
  5790. // 创建成功,更新配置ID
  5791. getConfigId.value = createResponse.data?.id || createResponse.data;
  5792. ElMessage.success('已自动创建职位配置');
  5793. // 打开对话框
  5794. nextTick(() => {
  5795. // 备份当前配置
  5796. originalInterviewSettings.value = { ...interviewSettings };
  5797. // 按职位ID区分存储和读取 reviewInterview 配置
  5798. const positionId = route.query.id as string;
  5799. const storageKey = `reviewInterview_${positionId}`;
  5800. const storedReviewInterview = sessionStorage.getItem(storageKey);
  5801. // 如果当前职位没有设置过,则使用默认值 false
  5802. interviewSettings.reviewInterview = storedReviewInterview === 'true';
  5803. console.log(`职位 ${positionId} 的 reviewInterview 配置:`, storedReviewInterview, '转换后:', interviewSettings.reviewInterview);
  5804. });
  5805. showInterviewSettingsDialog.value = true;
  5806. interviewSettings.beforeStartText = '你好,我是本次面试的面试官,欢迎参加本公司的线上面试!面试预计需要15分钟,请你提前安排在网络良好、光线亮度合适、且相对安静的环境参加这次面试以免影响本次面试的结果。如果你在面试过程中遇到问题,请与我们的招聘人员联系。';
  5807. } else {
  5808. ElMessage.warning('自动创建配置失败,请手动创建');
  5809. }
  5810. } catch (error) {
  5811. console.error('自动创建配置失败:', error);
  5812. ElMessage.warning('自动创建配置失败,请手动创建');
  5813. }
  5814. }
  5815. } catch (error) {
  5816. console.error('获取配置失败:', error);
  5817. ElMessage.error('获取配置失败');
  5818. }
  5819. };
  5820. // 开始前文本编辑方法
  5821. const startEditBeforeStart = () => {
  5822. editingBeforeStartText.value = interviewSettings.beforeStartText;
  5823. isEditingBeforeStart.value = true;
  5824. };
  5825. const cancelEditBeforeStart = () => {
  5826. isEditingBeforeStart.value = false;
  5827. editingBeforeStartText.value = '';
  5828. };
  5829. const saveBeforeStart = async () => {
  5830. if (!editingBeforeStartText.value.trim()) {
  5831. ElMessage.warning('开始前提示内容不能为空');
  5832. return;
  5833. }
  5834. interviewSettings.beforeStartText = editingBeforeStartText.value.trim();
  5835. await api.UpdateObj({
  5836. id: route.query.id,
  5837. status: false
  5838. })
  5839. try {
  5840. let response;
  5841. console.log(getConfigId.value);
  5842. // 根据getConfigId的值决定调用哪个接口
  5843. if (getConfigId.value == 0) {
  5844. // getConfigId不等于0时,调用CreateConfig接口
  5845. response = await CreateConfig({
  5846. position_id: route.query.id,
  5847. digital_human_opening_speech: editingBeforeStartText.value.trim()
  5848. });
  5849. if (response.code === 2000) {
  5850. // 创建成功后更新getConfigId
  5851. getConfigId.value = response.data?.id || response.data;
  5852. ElMessage.success('开场词提示内容已创建');
  5853. isEditingBeforeStart.value = false;
  5854. } else {
  5855. ElMessage.error('开场词提示内容创建失败');
  5856. }
  5857. } else {
  5858. // getConfigId等于0时,调用UpdateConfig接口
  5859. response = await UpdateConfig({
  5860. id: getConfigId.value,
  5861. digital_human_opening_speech: editingBeforeStartText.value.trim()
  5862. });
  5863. if (response.code === 2000) {
  5864. ElMessage.success('开场词提示内容已更新');
  5865. isEditingBeforeStart.value = false;
  5866. // 调用GetOpeningSpeech接口
  5867. try {
  5868. const params = { config_id: getConfigId.value };
  5869. await GetOpeningSpeech(params);
  5870. console.log('开场词生成成功');
  5871. } catch (error) {
  5872. console.error('获取开场词失败:', error);
  5873. ElMessage.warning('开场词生成中,请稍后');
  5874. }
  5875. } else {
  5876. ElMessage.error('开场词提示内容更新失败');
  5877. }
  5878. }
  5879. } catch (error) {
  5880. console.error('保存开场词提示内容失败:', error);
  5881. ElMessage.error('保存失败,请重试');
  5882. }
  5883. };
  5884. // 开始后提醒编辑方法
  5885. const startEditAfterStart = () => {
  5886. editingAfterStartText.value = interviewSettings.afterStartText;
  5887. isEditingAfterStart.value = true;
  5888. };
  5889. const cancelEditAfterStart = () => {
  5890. isEditingAfterStart.value = false;
  5891. editingAfterStartText.value = '';
  5892. };
  5893. const saveAfterStart = () => {
  5894. if (!editingAfterStartText.value.trim()) {
  5895. ElMessage.warning('开始后提醒内容不能为空');
  5896. return;
  5897. }
  5898. interviewSettings.afterStartText = editingAfterStartText.value.trim();
  5899. isEditingAfterStart.value = false;
  5900. ElMessage.success('开始后提醒内容已更新');
  5901. };
  5902. // 免责声明编辑方法
  5903. const startEditDisclaimer = () => {
  5904. editingDisclaimerText.value = interviewSettings.disclaimerText;
  5905. isEditingDisclaimer.value = true;
  5906. };
  5907. const cancelEditDisclaimer = () => {
  5908. isEditingDisclaimer.value = false;
  5909. editingDisclaimerText.value = '';
  5910. refreshPage()
  5911. };
  5912. const saveDisclaimer = () => {
  5913. if (!editingDisclaimerText.value.trim()) {
  5914. ElMessage.warning('内容不能为空');
  5915. return;
  5916. }
  5917. interviewSettings.disclaimerText = editingDisclaimerText.value.trim();
  5918. isEditingDisclaimer.value = false;
  5919. ElMessage.success('内容已更新');
  5920. };
  5921. // 题目数量设置
  5922. const questionCountSettings = reactive({
  5923. openQuestionsCount: 5,
  5924. choiceQuestionsCount: 10
  5925. });
  5926. // 处理题目数量变化
  5927. const handleQuestionCountChange = () => {
  5928. // 这里可以添加其他逻辑,比如保存到本地存储等
  5929. console.log('题目数量已更新:', questionCountSettings);
  5930. };
  5931. // 修改自动生成题目的方法
  5932. const handleAutoGenerate = async () => {
  5933. try {
  5934. autoGenerateLoading.value = true;
  5935. // 获取选中的能力标签ID
  5936. const competencyTagIds = selectedAbilities.value.map(ability => {
  5937. return ability.id;
  5938. });/* selectedDimensions.value.map(dimension => {
  5939. const tag = competencyTags.value.find(tag => tag.name === dimension.name);
  5940. return tag ? tag.id : null;
  5941. }).filter(id => id !== null); */
  5942. if (competencyTagIds.length === 0) {
  5943. ElMessage.warning('请先选择能力标签');
  5944. return;
  5945. }
  5946. const params = {
  5947. position_id: route.query.id,
  5948. competency_tag_ids: competencyTagIds,
  5949. open_questions_count: questionCountSettings.openQuestionsCount, // 使用设置的开放题数量
  5950. choice_questions_count: questionCountSettings.choiceQuestionsCount, // 使用设置的选择题数量
  5951. auto_save: true,
  5952. tenant_id: Session.get('tenant_id')
  5953. };
  5954. // 调用生成题目的接口
  5955. const response = await GenerateQuestions(params);
  5956. if (response && response.data) {
  5957. // 生成成功后,使用轮询获取最新生成的题目
  5958. const draftResponse = await pollForDraftList({
  5959. tenant_id: Session.get('tenant_id'),
  5960. page: 1,
  5961. limit: 30,
  5962. position_id: route.query.id as string
  5963. });
  5964. console.log(draftResponse);
  5965. // 检查轮询是否被取消
  5966. if (draftResponse === null) {
  5967. console.log('轮询被取消,停止处理');
  5968. return;
  5969. }
  5970. if (draftResponse && draftResponse.data && draftResponse.data.dataList) {
  5971. // 将获取到的题目格式化并添加到 chatQuestions 列表中
  5972. const formattedQuestions = draftResponse.data.dataList.map((question: any) => ({
  5973. id: question.id || Date.now(),
  5974. title: question.question || question.title,
  5975. ability: question.ability || '未设置',
  5976. content: question.content || question.question,
  5977. target: question.target || '未设置',
  5978. competency_tags: question.competency_tag_info || [],
  5979. question_form_name: question.question_form_name,
  5980. scoring_reference: question.scoring_reference,
  5981. question_form: question.question_form,
  5982. is_required_correct: question.is_required_correct,
  5983. weight: 100,
  5984. maxAnswers: 1,
  5985. enable_follow_up: true,
  5986. options: question.options || [],
  5987. source: 'ai_generated' as const // 标识为AI生成的题目
  5988. }));
  5989. // 清空现有题目并添加新生成的题目
  5990. chatQuestions.value = formattedQuestions;
  5991. ElMessage.success(`已成功生成${formattedQuestions.length}个面试题目`);
  5992. } else {
  5993. ElMessage.warning('AI生成题目中,请稍后再试');
  5994. }
  5995. }
  5996. } catch (error) {
  5997. console.error('AI自动组题失败:', error);
  5998. ElMessage.error('AI自动组题失败,请重试');
  5999. } finally {
  6000. // 无论成功或失败都要关闭loading状态
  6001. autoGenerateLoading.value = false;
  6002. }
  6003. };
  6004. // 处理胜任力标签变化
  6005. const handleCompetencyTagsChange = (value: string[]) => {
  6006. // 更新选中的标签ID
  6007. questionForm.competency_tags = [];
  6008. if (value.length > 0) {
  6009. // 计算每个标签的均分权重
  6010. const averageWeight = Math.floor(100 / value.length);
  6011. const remainder = 100 % value.length;
  6012. // 根据选中的标签ID,从competencyTagOptions中获取完整的标签信息
  6013. questionForm.competency_tags = value.map((tagId, index) => {
  6014. const tagOption = competencyTagOptions.value.find(option => option.id === tagId);
  6015. return {
  6016. id: tagId,
  6017. name: tagOption ? tagOption.name : `标签${tagId}`,
  6018. // 均分权重,余数分配给前几个标签
  6019. weight: averageWeight + (index < remainder ? 1 : 0)
  6020. };
  6021. });
  6022. }
  6023. };
  6024. // 验证胜任力配置
  6025. const validateCompetencyConfig = () => {
  6026. if (questionForm.competency_tags.length === 0) {
  6027. ElMessage.warning('请至少选择一个胜任力标签');
  6028. return false;
  6029. }
  6030. if (questionForm.weight <= 0 || questionForm.weight > 100) {
  6031. ElMessage.warning('权重必须在1-100之间');
  6032. return false;
  6033. }
  6034. return true;
  6035. };
  6036. // 处理题目形式变化
  6037. const handleQuestionTypeChange = (value: string) => {
  6038. questionForm.type = value;
  6039. // 根据题目形式设置默认建议时长
  6040. switch (value) {
  6041. case 'open':
  6042. questionForm.suggestedDuration = 120; // 开放题默认2分钟
  6043. break;
  6044. case 'single':
  6045. case 'multiple':
  6046. questionForm.suggestedDuration = 60; // 选择题默认1分钟
  6047. break;
  6048. case 'fill':
  6049. questionForm.suggestedDuration = 30; // 填空题默认30秒
  6050. break;
  6051. case 'self':
  6052. questionForm.suggestedDuration = 180; // 自言语默认3分钟
  6053. break;
  6054. default:
  6055. questionForm.suggestedDuration = 60;
  6056. }
  6057. };
  6058. // 添加状态变量
  6059. const competencyLoading = ref(false);
  6060. // 候选人提问环节相关变量 - 用于候选人向面试官提问的环节配置
  6061. const showCandidateQuestionsDialog = ref(false); // 控制候选人提问弹窗显示
  6062. const editingCandidateQuestionsIndex = ref(-1); // 编辑候选人提问环节的索引
  6063. const candidateQuestionsForm = reactive({
  6064. title: '候选人提问', // 环节名称
  6065. duration: 5, // 提问时长(分钟)
  6066. maxQuestions: 3, // 最多提问数量
  6067. description: '候选人可以向面试官提问关于公司、职位或团队的问题', // 环节描述
  6068. isRequired: true, // 是否必须环节
  6069. allowSkip: false, // 是否允许跳过
  6070. guidelines: [ // 提问指引
  6071. '请提出与职位、公司文化或团队相关的问题',
  6072. '避免询问已在职位描述中明确说明的信息',
  6073. '可以询问职业发展机会和团队工作方式'
  6074. ]
  6075. });
  6076. const removeGuideline = (index: number) => {
  6077. candidateQuestionsForm.guidelines.splice(index, 1);
  6078. };
  6079. const addGuideline = () => {
  6080. candidateQuestionsForm.guidelines.push('');
  6081. };
  6082. // 编辑候选人提问环节
  6083. const editCandidateQuestions = (step: any, index: number) => {
  6084. editingCandidateQuestionsIndex.value = index;
  6085. // 如果步骤有配置信息,加载到表单中
  6086. if (step.config) {
  6087. Object.assign(candidateQuestionsForm, step.config);
  6088. } else {
  6089. // 如果没有配置信息,从步骤的基本信息推断
  6090. candidateQuestionsForm.title = step.name;
  6091. candidateQuestionsForm.description = step.description || '候选人可以向面试官提问关于公司、职位或团队的问题';
  6092. candidateQuestionsForm.duration = 5;
  6093. candidateQuestionsForm.maxQuestions = 3;
  6094. candidateQuestionsForm.isRequired = step.active;
  6095. candidateQuestionsForm.allowSkip = false;
  6096. candidateQuestionsForm.guidelines = [
  6097. '请提出与职位、公司文化或团队相关的问题',
  6098. '避免询问已在职位描述中明确说明的信息',
  6099. '可以询问职业发展机会和团队工作方式'
  6100. ];
  6101. }
  6102. showCandidateQuestionsDialog.value = true;
  6103. };
  6104. // 统一的编辑流程步骤方法
  6105. const editProcessStep = (step: any, index: number) => {
  6106. switch (step.type) {
  6107. case 'ai_video':
  6108. editAIVideo(step);
  6109. break;
  6110. case 'video_presentation':
  6111. editVideoLecture(step, index);
  6112. break;
  6113. case 'candidate_questions':
  6114. editCandidateQuestions(step, index);
  6115. break;
  6116. case 'data_collection':
  6117. // TODO: 添加资料收集编辑功能
  6118. editAIVideo(step);
  6119. /* ElMessage.info('资料收集编辑功能开发中'); */
  6120. break;
  6121. case 'resume_collection':
  6122. // TODO: 添加简历收集编辑功能
  6123. editAIVideo(step);
  6124. break;
  6125. default:
  6126. editAIVideo(step);
  6127. break;
  6128. }
  6129. };
  6130. // 编辑视频宣讲
  6131. const editingVideoLectureIndex = ref(-1);
  6132. // 视频宣讲弹窗标题计算属性
  6133. const videoLectureDialogTitle = computed(() => {
  6134. return editingVideoLectureIndex.value >= 0 ? '修改视频宣讲' : '新增视频宣讲';
  6135. });
  6136. const editVideoLecture = async (step: any, index: number) => {
  6137. editingVideoLectureIndex.value = index;
  6138. try {
  6139. // 请求接口获取最新的配置数据
  6140. const response = await GetConfig(route.query.id);
  6141. if (response && response.data) {
  6142. // 从接口返回的数据中获取视频宣讲配置
  6143. const configData = response.data;
  6144. // 回填表单数据
  6145. videoLectureForm.title = step.name || '视频宣讲';
  6146. videoLectureForm.description = configData.video_presentation_description || step?.config?.description || '';
  6147. videoLectureForm.broadcastType = step?.config?.broadcastType || 'required';
  6148. videoLectureForm.video_url = configData.video_presentation_url || step?.config?.video_url || '';
  6149. console.log('视频宣讲配置数据:', {
  6150. title: videoLectureForm.title,
  6151. description: videoLectureForm.description,
  6152. video_url: videoLectureForm.video_url,
  6153. broadcastType: videoLectureForm.broadcastType
  6154. });
  6155. } else {
  6156. // 如果接口请求失败,使用步骤中的现有数据
  6157. videoLectureForm.title = step.name || '视频宣讲';
  6158. videoLectureForm.description = step?.config?.description || '';
  6159. videoLectureForm.broadcastType = step?.config?.broadcastType || 'required';
  6160. videoLectureForm.video_url = step?.config?.video_url || '';
  6161. }
  6162. } catch (error) {
  6163. console.error('获取视频宣讲配置失败:', error);
  6164. // 请求失败时使用步骤中的现有数据
  6165. videoLectureForm.title = step.name || '视频宣讲';
  6166. videoLectureForm.description = step?.config?.description || '';
  6167. videoLectureForm.broadcastType = step?.config?.broadcastType || 'required';
  6168. videoLectureForm.video_url = step?.config?.video_url || '';
  6169. }
  6170. showVideoLectureDialog.value = true;
  6171. };
  6172. // 权重变化处理函数
  6173. const handleWeightChange = (index: number, value: number | null) => {
  6174. if (value === null || value < 0) value = 0;
  6175. if (value > 100) value = 100;
  6176. const tags = questionForm.competency_tags as CompetencyTagItem[];
  6177. const oldWeight = tags[index].weight;
  6178. const newWeight = Math.round(value);
  6179. // 更新当前标签权重
  6180. tags[index].weight = newWeight;
  6181. // 如果只有一个标签,直接设置为100
  6182. if (tags.length === 1) {
  6183. tags[0].weight = 100;
  6184. return;
  6185. }
  6186. // 计算权重差值
  6187. const weightDiff = newWeight - oldWeight;
  6188. const otherTags = tags.filter((_, i) => i !== index);
  6189. const totalOtherWeight = otherTags.reduce((sum, tag) => sum + tag.weight, 0);
  6190. // 如果其他标签总权重为0,则平均分配剩余权重
  6191. if (totalOtherWeight === 0) {
  6192. const remainingWeight = 100 - newWeight;
  6193. const averageWeight = Math.floor(remainingWeight / otherTags.length);
  6194. const remainder = remainingWeight % otherTags.length;
  6195. otherTags.forEach((tag, i) => {
  6196. const tagIndex = tags.findIndex(t => t.id === tag.id);
  6197. tags[tagIndex].weight = averageWeight + (i < remainder ? 1 : 0);
  6198. });
  6199. } else {
  6200. // 按比例调整其他标签权重
  6201. const targetTotalOther = 100 - newWeight;
  6202. if (targetTotalOther > 0) {
  6203. otherTags.forEach(tag => {
  6204. const tagIndex = tags.findIndex(t => t.id === tag.id);
  6205. const proportion = tag.weight / totalOtherWeight;
  6206. tags[tagIndex].weight = Math.round(targetTotalOther * proportion);
  6207. });
  6208. // 处理舍入误差
  6209. const currentTotal = getTotalWeight();
  6210. if (currentTotal !== 100) {
  6211. const diff = 100 - currentTotal;
  6212. // 找到最大权重的其他标签进行调整
  6213. const maxOtherTag = otherTags.reduce((max, tag) =>
  6214. tag.weight > max.weight ? tag : max
  6215. );
  6216. const maxTagIndex = tags.findIndex(t => t.id === maxOtherTag.id);
  6217. tags[maxTagIndex].weight += diff;
  6218. }
  6219. } else {
  6220. // 如果当前权重已经是100%,其他标签设为0
  6221. otherTags.forEach(tag => {
  6222. const tagIndex = tags.findIndex(t => t.id === tag.id);
  6223. tags[tagIndex].weight = 0;
  6224. });
  6225. }
  6226. }
  6227. };
  6228. // 获取总权重
  6229. const getTotalWeight = (): number => {
  6230. const tags = questionForm.competency_tags as CompetencyTagItem[];
  6231. return tags.reduce((total, tag) => total + (tag.weight || 0), 0);
  6232. };
  6233. // 平均分配权重
  6234. const distributeWeightsEvenly = () => {
  6235. const tags = questionForm.competency_tags as CompetencyTagItem[];
  6236. if (tags.length === 0) return;
  6237. const averageWeight = Math.floor(100 / tags.length);
  6238. const remainder = 100 % tags.length;
  6239. tags.forEach((tag, index) => {
  6240. tag.weight = averageWeight + (index < remainder ? 1 : 0);
  6241. });
  6242. };
  6243. const competencyTagsValue = computed({
  6244. get: () => getcompetency_tags(questionForm.competency_tags),
  6245. set: (value) => {
  6246. questionForm.competency_tags = value;
  6247. handleCompetencyTagsChange(value);
  6248. }
  6249. });
  6250. // 删除AI考察相关问题
  6251. const handleDeleteAIQuestions = async () => {
  6252. try {
  6253. // 获取当前职位下的所有AI生成的问题
  6254. const response = await GetPositionQuestions({
  6255. position_id: route.query.id,
  6256. tenant_id: Session.get('tenant_id'),
  6257. is_ai_generated: true // 只获取AI生成的问题
  6258. });
  6259. if (response && response.data && response.data.length > 0) {
  6260. // 批量解绑AI生成的问题
  6261. await BatchUnbind({
  6262. position_id: route.query.id,
  6263. question_id: response.data.map((item: any) => item.id),
  6264. tenant_id: Session.get('tenant_id')
  6265. });
  6266. // 更新配置
  6267. if (getConfigId.value > 0) {
  6268. await UpdateConfig({
  6269. id: getConfigId.value,
  6270. enable_ai_questions: false
  6271. });
  6272. }
  6273. ElMessage.success('AI考察问题已删除');
  6274. }
  6275. } catch (error) {
  6276. console.error('删除AI考察问题失败:', error);
  6277. ElMessage.error('删除AI考察问题失败');
  6278. }
  6279. };
  6280. // 在setup部分添加以下代码
  6281. // ... existing code ...
  6282. // 监听enable_open_questions的变化
  6283. watch(
  6284. () => fieldConfig.question_form_switches.enable_open_questions,
  6285. (newVal) => {
  6286. if (!newVal) {
  6287. // 如果禁用了AI考察,隐藏相关内容
  6288. recruitmentProcess.forEach((process) => {
  6289. if (process.type === 'ai_interview') {
  6290. process.visible = false;
  6291. }
  6292. });
  6293. } else {
  6294. // 如果启用了AI考察,显示相关内容
  6295. recruitmentProcess.forEach((process) => {
  6296. if (process.type === 'ai_interview') {
  6297. process.visible = true;
  6298. }
  6299. });
  6300. }
  6301. },
  6302. { immediate: true }
  6303. );
  6304. // 监听字段配置变化,自动保存到本地缓存
  6305. watch(
  6306. fieldConfig,
  6307. (newConfig) => {
  6308. // 只在启用自动保存时才执行
  6309. if (!enableAutoSave.value) {
  6310. return;
  6311. }
  6312. // 防抖处理,避免频繁保存
  6313. if (saveConfigTimer.value) {
  6314. clearTimeout(saveConfigTimer.value);
  6315. }
  6316. saveConfigTimer.value = setTimeout(() => {
  6317. saveFieldConfigToCache(newConfig, route.query.id as string);
  6318. console.log('字段配置已自动保存到本地缓存');
  6319. }, 1000); // 1秒后保存
  6320. },
  6321. { deep: true }
  6322. );
  6323. // 修改添加AI考察的方法
  6324. const addAIInterview = () => {
  6325. // 添加AI考察时,自动启用enable_open_questions
  6326. UpdateConfig({
  6327. id: route.query.id,
  6328. question_form_switches: {
  6329. ...fieldConfig.question_form_switches,
  6330. enable_open_questions: true
  6331. }
  6332. }).then((res: any) => {
  6333. if (res.code == 2000) {
  6334. // 添加AI考察的其他逻辑...
  6335. }
  6336. });
  6337. };
  6338. // ... existing code ...
  6339. // 新增对话框控制变量
  6340. const showCommonSenseDialog = ref(false);
  6341. const showPsychologicalDialog = ref(false);
  6342. const showPostRenderDialog = ref(false);
  6343. // 在setup中添加相关变量和方法
  6344. const bodyDetectionConfigVisible = ref(false)
  6345. const bodyDetectionConfig = ref({
  6346. duration: 5,
  6347. detectionItems: ['face', 'pose'],
  6348. promptText: '请按照提示完成相应的动作,系统将自动进行检测。'
  6349. })
  6350. // 打开肢体检测配置
  6351. const openBodyDetectionConfig = () => {
  6352. bodyDetectionConfigVisible.value = true
  6353. }
  6354. // 取消肢体检测配置
  6355. const cancelBodyDetectionConfig = () => {
  6356. bodyDetectionConfigVisible.value = false
  6357. }
  6358. // 保存肢体检测配置
  6359. const saveBodyDetectionConfig = async () => {
  6360. try {
  6361. // TODO: 调用API保存配置
  6362. await saveProcessConfig({
  6363. position_id: route.params.id,
  6364. process_type: 'posture_check',
  6365. config: bodyDetectionConfig.value
  6366. })
  6367. ElMessage.success('配置保存成功')
  6368. bodyDetectionConfigVisible.value = false
  6369. } catch (error) {
  6370. console.error('保存肢体检测配置失败:', error)
  6371. ElMessage.error('配置保存失败')
  6372. }
  6373. }
  6374. // 在handleProcessConfig方法中添加肢体检测的处理
  6375. const handleProcessConfig = (type: string) => {
  6376. switch (type) {
  6377. // ... existing code ...
  6378. case 'posture_check':
  6379. openBodyDetectionConfig()
  6380. break
  6381. // ... existing code ...
  6382. }
  6383. }
  6384. /* 树状 */
  6385. const documentTreeLoading = ref(false);
  6386. const documentTreeData = ref([]);
  6387. const treeListRef = ref();
  6388. const handleTreeClick = (data: any) => {
  6389. console.log(data)
  6390. // 获取点击的节点信息
  6391. const { firstLevel, secondLevel, currentNode } = data;
  6392. // 如果点击的是二级节点(标签)
  6393. if (secondLevel) {
  6394. // 根据标签ID搜索题目
  6395. GetQuestionList({
  6396. page: 1,
  6397. limit: 20,
  6398. question_tags: secondLevel.id.replace('tag_', ''), // 移除前缀获取真实ID
  6399. question_form: questionListQuery.question_form
  6400. }).then((res: any) => {
  6401. if (res.data) {
  6402. questionList.value = res.data.items;
  6403. questionListpage.page = res.data.page;
  6404. questionListpage.pageSize = res.data.limit;
  6405. totalQuestions2.value = res.data.total;
  6406. }
  6407. });
  6408. }
  6409. // 如果点击的是一级节点(分类)
  6410. else if (firstLevel) {
  6411. // 根据分类ID搜索题目
  6412. GetQuestionList({
  6413. page: 1,
  6414. limit: 20,
  6415. question_category: firstLevel.id.replace('category_', ''), // 移除前缀获取真实ID
  6416. question_form: questionListQuery.question_form
  6417. }).then((res: any) => {
  6418. if (res.data) {
  6419. questionList.value = res.data.items;
  6420. questionListpage.page = res.data.page;
  6421. questionListpage.pageSize = res.data.limit;
  6422. totalQuestions2.value = res.data.total;
  6423. }
  6424. });
  6425. }
  6426. }
  6427. const handleUpdateDocument = (data: any) => {
  6428. console.log(data)
  6429. }
  6430. const handleDeleteDocument = (data: any) => {
  6431. console.log(data)
  6432. }
  6433. // 获取文档树数据
  6434. const getTreeData = () => {
  6435. documentTreeLoading.value = true;
  6436. GetDocumentTree({ page: 1, limit: 100 }).then((ret: any) => {
  6437. console.log('ret', ret);
  6438. const responseData = ret.data;
  6439. GetCategoryList({ page: 1, limit: 100 }).then((res: any) => {
  6440. console.log('res', res);
  6441. const categoryList = res.data.items.filter((item: any) =>
  6442. !['技术能力', '服务意识', '职业素质', '基本常识', '色盲检测', '内容铺垫', '心理测试1', '心理测试'].includes(item.name)
  6443. );
  6444. GetTagList({}).then((res: any) => {
  6445. console.log('res', res);
  6446. const tagList = res.data.items;
  6447. // 创建根节点
  6448. const rootNode = [
  6449. /* {
  6450. id: 'root',
  6451. title: '职位开放题',
  6452. children: responseData.map((item: any) => ({
  6453. ...item,
  6454. id: `open_${item.id}`,
  6455. title: item.name || item.title,
  6456. question_type: 'open',
  6457. chinese_explanation: item.chinese_explanation || ''
  6458. }))
  6459. },
  6460. {
  6461. id: 'professional_questions',
  6462. title: '专业考察题库',
  6463. children: responseData.map((item: any) => ({
  6464. ...item,
  6465. id: `prof_${item.id}`,
  6466. title: item.name || item.title,
  6467. question_type: 'professional',
  6468. chinese_explanation: item.chinese_explanation || ''
  6469. }))
  6470. }, */
  6471. ...categoryList.map((item: any) => ({
  6472. id: `category_${item.id}`,
  6473. title: item.name,
  6474. children: tagList.filter((tag: any) => tag.category_id === item.id).map((tag: any) => ({
  6475. id: `tag_${tag.id}`,
  6476. title: tag.name,
  6477. chinese_explanation: tag.chinese_explanation || ''
  6478. }))
  6479. }))
  6480. ];
  6481. documentTreeData.value = rootNode;
  6482. documentTreeLoading.value = false;
  6483. });
  6484. });
  6485. }).catch(() => { documentTreeLoading.value = false; }).finally(() => { documentTreeLoading.value = false; });
  6486. };
  6487. /* 得分题 */
  6488. const treeLoading = ref(false);
  6489. const treeData = ref([]);
  6490. const defaultProps = {
  6491. children: 'children',
  6492. label: 'title'
  6493. }
  6494. const handleNodeClick = async (data: any) => {
  6495. try {
  6496. // 如果点击的是分类节点(没有 tag_ 前缀的 id)
  6497. if (data.id.startsWith('category_')) {
  6498. const categoryId = data.id.replace('category_', '');
  6499. const response = await GetQuestionList({
  6500. page: 1,
  6501. limit: 1000, // 设置较大的限制以获取所有题目
  6502. question_category: categoryId,
  6503. question_form: questionListQuery.question_form
  6504. });
  6505. if (response && response.data) {
  6506. // 更新题目列表显示
  6507. questionList.value = response.data.items;
  6508. questionListpage.page = response.data.page;
  6509. questionListpage.pageSize = response.data.limit;
  6510. totalQuestions2.value = response.data.total;
  6511. // 自动选中所有题目
  6512. const table = document.querySelector('.el-table__header-wrapper');
  6513. if (table) {
  6514. const checkAll = table.querySelector('.el-checkbox');
  6515. if (checkAll) {
  6516. (checkAll as any).__vueParentComponent.emit('change', true);
  6517. }
  6518. }
  6519. // 更新选中的题目列表
  6520. selectedQuestions.value = response.data.items;
  6521. }
  6522. }
  6523. // 如果点击的是标签节点
  6524. else if (data.id.startsWith('tag_')) {
  6525. const tagId = data.id.replace('tag_', '');
  6526. const response = await GetQuestionList({
  6527. page: 1,
  6528. limit: 1000,
  6529. question_tags: tagId,
  6530. question_form: questionListQuery.question_form
  6531. });
  6532. if (response && response.data) {
  6533. // 更新题目列表显示
  6534. questionList.value = response.data.items;
  6535. questionListpage.page = response.data.page;
  6536. questionListpage.pageSize = response.data.limit;
  6537. totalQuestions2.value = response.data.total;
  6538. // 自动选中所有题目
  6539. const table = document.querySelector('.el-table__header-wrapper');
  6540. if (table) {
  6541. const checkAll = table.querySelector('.el-checkbox');
  6542. if (checkAll) {
  6543. (checkAll as any).__vueParentComponent.emit('change', true);
  6544. }
  6545. }
  6546. // 更新选中的题目列表
  6547. selectedQuestions.value = response.data.items;
  6548. }
  6549. }
  6550. } catch (error) {
  6551. console.error('获取题目列表失败:', error);
  6552. ElMessage.error('获取题目列表失败');
  6553. }
  6554. }
  6555. const getDFTreeData = () => {
  6556. treeLoading.value = true;
  6557. GetDocumentTree({ page: 1, limit: 100 }).then((ret: any) => {
  6558. console.log('ret', ret);
  6559. const responseData = ret.data;
  6560. GetCategoryList({ page: 1, limit: 100 }).then((res: any) => {
  6561. console.log('res', res);
  6562. const categoryList = res.data.items.filter((item: any) =>
  6563. ['心理测试'].includes(item.name)
  6564. );
  6565. GetTagList({}).then((res: any) => {
  6566. console.log('res', res);
  6567. const tagList = res.data.items;
  6568. // 创建根节点
  6569. const rootNode = [
  6570. ...categoryList.map((item: any) => ({
  6571. id: `category_${item.id}`,
  6572. title: item.name,
  6573. children: tagList.filter((tag: any) => tag.category_id === item.id).map((tag: any) => ({
  6574. id: `tag_${tag.id}`,
  6575. title: tag.name,
  6576. chinese_explanation: tag.chinese_explanation || ''
  6577. }))
  6578. }))
  6579. ];
  6580. treeData.value = rootNode;
  6581. treeLoading.value = false;
  6582. });
  6583. });
  6584. }).catch(() => { treeLoading.value = false; }).finally(() => { treeLoading.value = false; });
  6585. }
  6586. </script>
  6587. <style scoped>
  6588. @import './index.css';
  6589. .selected-applications-preview {
  6590. padding: 20px;
  6591. }
  6592. .preview-title {
  6593. font-size: 16px;
  6594. font-weight: bold;
  6595. margin-bottom: 20px;
  6596. color: #333;
  6597. }
  6598. .preview-footer {
  6599. margin-top: 20px;
  6600. text-align: right;
  6601. }
  6602. :deep(.el-table) {
  6603. margin-top: 15px;
  6604. }
  6605. :deep(.el-table th) {
  6606. background-color: #f5f7fa;
  6607. }
  6608. :deep(.el-tag) {
  6609. margin-right: 5px;
  6610. }
  6611. </style>
  6612. <style lang="scss" scoped>
  6613. /* 修改 checkbox 选中颜色 */
  6614. :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
  6615. background-color: #0419a3 !important;
  6616. /* 选中背景色 */
  6617. border-color: #0419a3 !important;
  6618. /* 选中边框色 */
  6619. }
  6620. /* 修改 checkbox 选中时的勾选标记颜色 */
  6621. :deep(.el-checkbox__input.is-checked .el-checkbox__inner::after) {
  6622. border-color: #ffffff !important;
  6623. /* 勾选标记颜色,通常为白色 */
  6624. }
  6625. /* 修改 checkbox hover 状态颜色 */
  6626. :deep(.el-checkbox__inner:hover) {
  6627. border-color: #0419a3 !important;
  6628. }
  6629. /* 修改 checkbox focus 状态颜色 */
  6630. :deep(.el-checkbox__input.is-focus .el-checkbox__inner) {
  6631. border-color: #0419a3 !important;
  6632. }
  6633. /* 如果需要修改半选中状态的颜色 */
  6634. :deep(.el-checkbox__input.is-indeterminate .el-checkbox__inner) {
  6635. background-color: #0419a3 !important;
  6636. border-color: #0419a3 !important;
  6637. }
  6638. /* 为特定的 checkbox 组件自定义样式 */
  6639. :deep(.custom-checkbox .el-checkbox__input.is-checked .el-checkbox__inner) {
  6640. background-color: #548235 !important;
  6641. border-color: #548235 !important;
  6642. }
  6643. :deep(.custom-checkbox .el-checkbox__inner:hover) {
  6644. border-color: #0419a3 !important;
  6645. }
  6646. /* 自定义维度标签样式 */
  6647. .custom-tag {
  6648. display: inline-block;
  6649. background-color: #409eff;
  6650. color: white;
  6651. font-size: 10px;
  6652. padding: 2px 6px;
  6653. border-radius: 4px;
  6654. margin-left: 8px;
  6655. font-weight: 500;
  6656. transform: scale(0.8);
  6657. transform-origin: left center;
  6658. }
  6659. /* 自定义维度项容器样式 */
  6660. .custom-dimension-item {
  6661. display: flex;
  6662. align-items: center;
  6663. justify-content: space-between;
  6664. position: relative;
  6665. padding: 4px 8px;
  6666. border-radius: 4px;
  6667. margin: 2px 0;
  6668. transition: background-color 0.2s;
  6669. &:hover {
  6670. background-color: #f5f7fa;
  6671. .delete-custom-btn {
  6672. opacity: 1;
  6673. }
  6674. }
  6675. .delete-custom-btn {
  6676. opacity: 0;
  6677. transition: opacity 0.2s;
  6678. color: #f56c6c;
  6679. margin-left: 8px;
  6680. padding: 4px;
  6681. &:hover {
  6682. background-color: #fef0f0;
  6683. color: #f56c6c;
  6684. }
  6685. }
  6686. }
  6687. /* 添加题目选择对话框样式 */
  6688. .question-select-dialog {
  6689. :deep(.el-dialog__body) {
  6690. padding: 20px;
  6691. }
  6692. }
  6693. .question-select-content {
  6694. .search-bar {
  6695. margin-bottom: 20px;
  6696. .search-inputs {
  6697. display: flex;
  6698. align-items: center;
  6699. gap: 10px;
  6700. }
  6701. }
  6702. .question-list {
  6703. margin-top: 20px;
  6704. }
  6705. .pagination-container {
  6706. margin-top: 20px;
  6707. display: flex;
  6708. justify-content: flex-end;
  6709. }
  6710. }
  6711. /* 添加拖拽相关样式 */
  6712. .question-card {
  6713. position: relative;
  6714. border: 1px solid #e4e7ed;
  6715. border-radius: 4px;
  6716. padding: 16px;
  6717. margin-bottom: 12px;
  6718. background: #fff;
  6719. transition: all 0.3s;
  6720. &:hover {
  6721. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  6722. }
  6723. }
  6724. /* 拖拽状态样式 */
  6725. .sortable-ghost {
  6726. opacity: 0.5;
  6727. background: #f5f7fa;
  6728. border: 2px dashed #c0c4cc;
  6729. }
  6730. // .sortable-chosen {
  6731. // cursor: grabbing;
  6732. // transform: rotate(5deg);
  6733. // }
  6734. .sortable-drag {
  6735. cursor: grabbing;
  6736. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
  6737. transform: rotate(5deg);
  6738. }
  6739. .question-header {
  6740. display: flex;
  6741. justify-content: space-between;
  6742. align-items: flex-start;
  6743. margin-bottom: 12px;
  6744. .question-title {
  6745. display: flex;
  6746. align-items: flex-start;
  6747. gap: 12px;
  6748. flex: 1;
  6749. .title-content {
  6750. flex: 1;
  6751. display: flex;
  6752. align-items: center;
  6753. justify-content: space-between;
  6754. gap: 20px;
  6755. .question-content {
  6756. flex: 1;
  6757. display: flex;
  6758. align-items: flex-start;
  6759. gap: 10px;
  6760. flex-direction: column;
  6761. .question-number {
  6762. color: #909399;
  6763. font-size: 12px;
  6764. }
  6765. }
  6766. .question-tags {
  6767. margin-top: 20px;
  6768. flex-shrink: 0;
  6769. display: flex;
  6770. flex-wrap: nowrap;
  6771. gap: 8px;
  6772. align-items: center;
  6773. max-width: 300px;
  6774. .competency-tag {
  6775. margin: 0;
  6776. border-color: #ed7d31;
  6777. color: #fff;
  6778. background-color: #ed7d31;
  6779. font-size: 12px;
  6780. white-space: nowrap;
  6781. &:hover {
  6782. background-color: #ed7d31;
  6783. border-color: #ed7d31;
  6784. color: #fff;
  6785. }
  6786. }
  6787. .more-tags-indicator {
  6788. font-size: 12px;
  6789. color: #909399;
  6790. background-color: #f0f2f5;
  6791. padding: 2px 6px;
  6792. border-radius: 3px;
  6793. white-space: nowrap;
  6794. flex-shrink: 0;
  6795. &:hover {
  6796. color: #409eff;
  6797. background-color: #ecf5ff;
  6798. }
  6799. }
  6800. }
  6801. }
  6802. }
  6803. .question-actions {
  6804. display: flex;
  6805. gap: 4px;
  6806. }
  6807. }
  6808. .question-info {
  6809. .info-item {
  6810. margin-bottom: 8px;
  6811. &:last-child {
  6812. margin-bottom: 0;
  6813. }
  6814. .info-label {
  6815. color: #909399;
  6816. margin-right: 8px;
  6817. }
  6818. }
  6819. }
  6820. .field-config-panel {
  6821. background: #fff;
  6822. border-radius: 8px;
  6823. padding: 24px;
  6824. margin-bottom: 20px;
  6825. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  6826. .panel-header {
  6827. display: flex;
  6828. justify-content: space-between;
  6829. align-items: center;
  6830. margin-bottom: 24px;
  6831. h3 {
  6832. margin: 0;
  6833. font-size: 18px;
  6834. color: #303133;
  6835. font-weight: 600;
  6836. }
  6837. .panel-actions {
  6838. display: flex;
  6839. gap: 12px;
  6840. .el-button {
  6841. font-size: 12px;
  6842. padding: 6px 12px;
  6843. height: 28px;
  6844. line-height: 16px;
  6845. }
  6846. }
  6847. }
  6848. .section-title {
  6849. font-size: 16px;
  6850. font-weight: 500;
  6851. color: #303133;
  6852. margin: 24px 0 16px;
  6853. padding-left: 10px;
  6854. border-left: 4px solid #0419a3;
  6855. }
  6856. .field-config-item {
  6857. display: flex;
  6858. align-items: center;
  6859. justify-content: space-between;
  6860. margin-bottom: 12px;
  6861. padding: 12px 16px;
  6862. background: #f5f7fa;
  6863. border-radius: 6px;
  6864. transition: all 0.3s ease;
  6865. &:hover {
  6866. background: #eef1f6;
  6867. }
  6868. &.sub-field {
  6869. background: #fff;
  6870. border: 1px solid #e4e7ed;
  6871. margin-left: 12px;
  6872. &:hover {
  6873. border-color: #0419a3;
  6874. }
  6875. }
  6876. .field-label {
  6877. flex: 1;
  6878. font-size: 14px;
  6879. color: #606266;
  6880. display: flex;
  6881. align-items: center;
  6882. gap: 8px;
  6883. }
  6884. .required-tag {
  6885. font-size: 12px;
  6886. padding: 0 6px;
  6887. height: 20px;
  6888. line-height: 18px;
  6889. }
  6890. :deep(.el-switch.is-checked .el-switch__core) {
  6891. border-color: #0419a3;
  6892. background-color: #0419a3;
  6893. }
  6894. }
  6895. }
  6896. :deep(.el-switch.is-checked .el-switch__core) {
  6897. border-color: #0419a3;
  6898. background-color: #0419a3;
  6899. }
  6900. .el-row {
  6901. margin-bottom: 12px;
  6902. }
  6903. /* 题目开关样式 */
  6904. .question-switches {
  6905. display: flex;
  6906. align-items: center;
  6907. gap: 20px;
  6908. margin-left: auto;
  6909. padding-left: 20px;
  6910. border-left: 1px solid #e4e7ed;
  6911. .switch-item {
  6912. display: flex;
  6913. align-items: center;
  6914. gap: 8px;
  6915. .switch-label {
  6916. font-size: 14px;
  6917. color: #606266;
  6918. white-space: nowrap;
  6919. }
  6920. .el-input-number {
  6921. width: 100px;
  6922. }
  6923. }
  6924. }
  6925. /* 确保interview-actions布局正确 */
  6926. .interview-actions {
  6927. display: flex;
  6928. align-items: center;
  6929. gap: 12px;
  6930. margin-bottom: 16px;
  6931. .import-btn,
  6932. .custom-btn {
  6933. flex-shrink: 0;
  6934. }
  6935. .import-btn,
  6936. .custom-btn:hover {
  6937. color: #0419a3;
  6938. border-color: #0419a3;
  6939. }
  6940. }
  6941. /* 面试设置弹窗样式 */
  6942. .interview-settings-dialog {
  6943. :deep(.el-dialog__body) {
  6944. padding: 20px;
  6945. }
  6946. }
  6947. .interview-settings-content {
  6948. .settings-section {
  6949. margin-bottom: 24px;
  6950. &:last-child {
  6951. margin-bottom: 0;
  6952. }
  6953. .section-header {
  6954. margin-bottom: 16px;
  6955. .section-title {
  6956. font-size: 16px;
  6957. font-weight: 500;
  6958. color: #303133;
  6959. padding-left: 10px;
  6960. border-left: 4px solid #0419a3;
  6961. }
  6962. }
  6963. .setting-item {
  6964. display: flex;
  6965. align-items: center;
  6966. justify-content: space-between;
  6967. padding: 0 30px;
  6968. border-radius: 20px;
  6969. border: 1px solid #f0f0f0;
  6970. .setting-label {
  6971. min-width: 150px;
  6972. text-align: justify;
  6973. text-align-last: justify;
  6974. text-justify: inter-character;
  6975. }
  6976. &:last-child {
  6977. border-bottom: none;
  6978. }
  6979. .setting-info {
  6980. display: flex;
  6981. align-items: center;
  6982. flex: 1;
  6983. // margin-right: 16px;
  6984. .setting-name {
  6985. width: 150px;
  6986. font-size: 14px;
  6987. font-weight: 500;
  6988. margin-right: 100px;
  6989. color: #808080;
  6990. display: block;
  6991. margin-bottom: 4px;
  6992. text-align: justify;
  6993. text-justify: inter-character;
  6994. }
  6995. .setting-desc {
  6996. width: 390px;
  6997. font-size: 12px;
  6998. color: #909399;
  6999. margin: 0;
  7000. line-height: 1.4;
  7001. }
  7002. }
  7003. :deep(.el-switch.is-checked .el-switch__core) {
  7004. border-color: #0419a3;
  7005. background-color: #0419a3;
  7006. }
  7007. .switch-desc {
  7008. font-size: 12px;
  7009. color: #909399;
  7010. margin-left: 8px;
  7011. }
  7012. .setting-desc-container {
  7013. display: flex;
  7014. align-items: center;
  7015. justify-content: space-between;
  7016. gap: 8px;
  7017. /* flex: 1; */
  7018. width: 80%;
  7019. .setting-desc {
  7020. width: 390px;
  7021. margin: 0;
  7022. }
  7023. .edit-desc-btn {
  7024. padding: 4px;
  7025. min-height: auto;
  7026. &:hover {
  7027. color: #0419a3;
  7028. }
  7029. }
  7030. }
  7031. .desc-edit-container {
  7032. display: flex;
  7033. align-items: center;
  7034. justify-content: space-between;
  7035. flex: 1;
  7036. .desc-edit-actions {
  7037. display: flex;
  7038. gap: 8px;
  7039. margin-left: 15px;
  7040. margin-top: 8px;
  7041. justify-content: flex-end;
  7042. }
  7043. }
  7044. }
  7045. }
  7046. }
  7047. .avatar-name {
  7048. margin-top: 8px;
  7049. font-size: 14px;
  7050. color: #333;
  7051. width: 100%;
  7052. text-align: center;
  7053. overflow: hidden;
  7054. white-space: nowrap;
  7055. text-overflow: ellipsis;
  7056. }
  7057. .avatar-wrapper {
  7058. display: flex;
  7059. flex-direction: column;
  7060. align-items: center;
  7061. width: 80px; // 设置一个固定宽度
  7062. }
  7063. .question-form {
  7064. .form-item {
  7065. margin-bottom: 20px;
  7066. .form-label {
  7067. font-size: 14px;
  7068. color: #333;
  7069. margin-bottom: 8px;
  7070. font-weight: 500;
  7071. .required {
  7072. color: #f56c6c;
  7073. margin-left: 4px;
  7074. }
  7075. }
  7076. .form-tip {
  7077. font-size: 12px;
  7078. color: #999;
  7079. margin-top: 4px;
  7080. }
  7081. .competency-config {
  7082. border: 1px solid #dcdfe6;
  7083. border-radius: 4px;
  7084. padding: 12px;
  7085. .config-row {
  7086. display: flex;
  7087. align-items: center;
  7088. margin-bottom: 8px;
  7089. &:last-child {
  7090. margin-bottom: 0;
  7091. }
  7092. .config-label {
  7093. flex: 1;
  7094. font-size: 14px;
  7095. color: #666;
  7096. text-align: center;
  7097. }
  7098. .tag-name {
  7099. flex: 1;
  7100. font-size: 14px;
  7101. color: #333;
  7102. text-align: center;
  7103. }
  7104. .el-input-number {
  7105. flex: 1;
  7106. margin: 0 10px;
  7107. }
  7108. }
  7109. }
  7110. .full-width {
  7111. width: 100%;
  7112. }
  7113. }
  7114. }
  7115. /* 标题和按钮左右布局样式 */
  7116. .header-row {
  7117. display: flex;
  7118. justify-content: space-between;
  7119. align-items: center;
  7120. margin-bottom: 16px;
  7121. h3 {
  7122. margin: 0;
  7123. flex: 1;
  7124. }
  7125. .right_btn {
  7126. flex-shrink: 0;
  7127. }
  7128. }
  7129. /* QuestionBankForm 对话框样式 */
  7130. :deep(.question-bank-form-dialog) {
  7131. .el-dialog__header {
  7132. padding: 20px 20px 10px 20px;
  7133. border-bottom: 1px solid #ebeef5;
  7134. .el-dialog__title {
  7135. font-size: 18px;
  7136. font-weight: 600;
  7137. color: #303133;
  7138. }
  7139. }
  7140. .el-dialog__body {
  7141. padding: 0;
  7142. max-height: 70vh;
  7143. overflow-y: auto;
  7144. }
  7145. .el-dialog__footer {
  7146. padding: 10px 20px 20px 20px;
  7147. border-top: 1px solid #ebeef5;
  7148. }
  7149. }
  7150. /* 权重配置增强样式 */
  7151. .competency-config {
  7152. .config-row {
  7153. &:first-child {
  7154. background-color: #f8f9fa;
  7155. border-radius: 4px 4px 0 0;
  7156. font-weight: 600;
  7157. color: #409eff;
  7158. }
  7159. /* 平均分配按钮样式 */
  7160. .el-button--text {
  7161. padding: 4px 8px;
  7162. margin-left: auto;
  7163. font-size: 12px;
  7164. &:hover {
  7165. background-color: #ecf5ff;
  7166. color: #409eff;
  7167. }
  7168. }
  7169. }
  7170. .tag-name {
  7171. font-weight: 500;
  7172. color: #303133;
  7173. }
  7174. }
  7175. .weight-sum {
  7176. font-weight: bold;
  7177. color: #67c23a;
  7178. margin-left: 8px;
  7179. font-size: 13px;
  7180. &.weight-error {
  7181. color: #f56c6c;
  7182. animation: shake 0.5s ease-in-out;
  7183. }
  7184. }
  7185. /* 输入框优化样式 */
  7186. :deep(.el-input-number) {
  7187. .el-input__inner {
  7188. text-align: center;
  7189. font-weight: 500;
  7190. }
  7191. &.is-focus .el-input__inner {
  7192. border-color: #409eff;
  7193. box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
  7194. }
  7195. }
  7196. /* 权重错误提示动画 */
  7197. @keyframes shake {
  7198. 0%,
  7199. 100% {
  7200. transform: translateX(0);
  7201. }
  7202. 25% {
  7203. transform: translateX(-2px);
  7204. }
  7205. 75% {
  7206. transform: translateX(2px);
  7207. }
  7208. }
  7209. /* 权重配置增强样式 */
  7210. .competency-config {
  7211. .config-row {
  7212. &:first-child {
  7213. background-color: #f8f9fa;
  7214. border-radius: 4px 4px 0 0;
  7215. font-weight: 600;
  7216. color: #409eff;
  7217. }
  7218. /* 平均分配按钮样式 */
  7219. .el-button--text {
  7220. padding: 4px 8px;
  7221. margin-left: auto;
  7222. font-size: 12px;
  7223. &:hover {
  7224. background-color: #ecf5ff;
  7225. color: #409eff;
  7226. }
  7227. }
  7228. }
  7229. .tag-name {
  7230. font-weight: 500;
  7231. color: #303133;
  7232. }
  7233. }
  7234. .weight-sum {
  7235. font-weight: bold;
  7236. color: #67c23a;
  7237. margin-left: 8px;
  7238. font-size: 13px;
  7239. &.weight-error {
  7240. color: #f56c6c;
  7241. animation: shake 0.5s ease-in-out;
  7242. }
  7243. }
  7244. /* 输入框优化样式 */
  7245. :deep(.el-input-number) {
  7246. .el-input__inner {
  7247. text-align: center;
  7248. font-weight: 500;
  7249. }
  7250. &.is-focus .el-input__inner {
  7251. border-color: #409eff;
  7252. box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
  7253. }
  7254. }
  7255. /* 权重错误提示动画 */
  7256. @keyframes shake {
  7257. 0%,
  7258. 100% {
  7259. transform: translateX(0);
  7260. }
  7261. 25% {
  7262. transform: translateX(-2px);
  7263. }
  7264. 75% {
  7265. transform: translateX(2px);
  7266. }
  7267. }
  7268. </style>