index.vue 230 KB

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