ss-components.js 387 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357103581035910360103611036210363103641036510366103671036810369103701037110372103731037410375103761037710378103791038010381103821038310384103851038610387103881038910390103911039210393103941039510396103971039810399104001040110402104031040410405104061040710408104091041010411104121041310414104151041610417104181041910420104211042210423104241042510426104271042810429104301043110432104331043410435104361043710438104391044010441104421044310444104451044610447104481044910450104511045210453104541045510456104571045810459104601046110462104631046410465104661046710468104691047010471104721047310474104751047610477104781047910480104811048210483104841048510486104871048810489104901049110492104931049410495104961049710498104991050010501105021050310504105051050610507105081050910510105111051210513105141051510516105171051810519105201052110522105231052410525105261052710528105291053010531105321053310534105351053610537105381053910540105411054210543105441054510546105471054810549105501055110552105531055410555105561055710558105591056010561105621056310564105651056610567105681056910570105711057210573105741057510576105771057810579105801058110582105831058410585105861058710588105891059010591105921059310594105951059610597105981059910600106011060210603106041060510606106071060810609106101061110612106131061410615106161061710618106191062010621106221062310624106251062610627106281062910630106311063210633106341063510636106371063810639106401064110642106431064410645106461064710648106491065010651106521065310654106551065610657106581065910660106611066210663106641066510666106671066810669106701067110672106731067410675106761067710678106791068010681106821068310684106851068610687106881068910690106911069210693106941069510696106971069810699107001070110702107031070410705107061070710708107091071010711107121071310714107151071610717107181071910720107211072210723107241072510726107271072810729107301073110732107331073410735107361073710738107391074010741107421074310744107451074610747107481074910750107511075210753107541075510756107571075810759107601076110762107631076410765107661076710768107691077010771107721077310774107751077610777107781077910780107811078210783107841078510786107871078810789107901079110792107931079410795107961079710798107991080010801108021080310804108051080610807108081080910810108111081210813108141081510816108171081810819108201082110822108231082410825108261082710828108291083010831108321083310834108351083610837108381083910840108411084210843108441084510846108471084810849108501085110852108531085410855108561085710858108591086010861108621086310864108651086610867108681086910870108711087210873108741087510876108771087810879108801088110882108831088410885108861088710888108891089010891108921089310894108951089610897108981089910900109011090210903109041090510906109071090810909109101091110912109131091410915109161091710918109191092010921109221092310924109251092610927109281092910930109311093210933109341093510936109371093810939109401094110942109431094410945109461094710948109491095010951109521095310954109551095610957109581095910960109611096210963109641096510966109671096810969109701097110972109731097410975109761097710978109791098010981109821098310984109851098610987109881098910990109911099210993109941099510996109971099810999110001100111002110031100411005110061100711008110091101011011110121101311014110151101611017110181101911020110211102211023110241102511026110271102811029110301103111032110331103411035110361103711038110391104011041110421104311044110451104611047110481104911050110511105211053110541105511056110571105811059110601106111062110631106411065110661106711068110691107011071110721107311074110751107611077110781107911080110811108211083110841108511086110871108811089110901109111092110931109411095110961109711098110991110011101111021110311104111051110611107111081110911110111111111211113111141111511116111171111811119111201112111122111231112411125111261112711128111291113011131111321113311134111351113611137111381113911140111411114211143111441114511146111471114811149111501115111152111531115411155111561115711158111591116011161111621116311164111651116611167111681116911170111711117211173111741117511176111771117811179111801118111182111831118411185111861118711188111891119011191111921119311194111951119611197111981119911200112011120211203
  1. import { ssIcon, commonIcon } from "./icon-config.js";
  2. import * as IndexComponents from "./ss-index-components.js";
  3. import * as EchartComponents from "./ss-echarts-compnents.js";
  4. import {
  5. isNum,
  6. toStyleStr,
  7. buildThumbUrl,
  8. openServiceDialog,
  9. pickSearchParams,
  10. } from "./tools.js"; // 功能说明:组件内统一构建缩略图URL、打开服务弹窗,减少 JSP 层字段转换 by xu 20260122
  11. import { EVEN_VAR } from "./EventBus.js";
  12. // import * as elements from "../lib/element-plus.js";
  13. (function () {
  14. const {
  15. createApp,
  16. ref,
  17. reactive,
  18. watch,
  19. onMounted,
  20. onBeforeUnmount,
  21. h,
  22. computed,
  23. resolveComponent,
  24. watchEffect,
  25. nextTick,
  26. onVnodeMounted,
  27. Teleport,
  28. inject,
  29. provide,
  30. } = Vue;
  31. // 弹窗默认遮罩z-index
  32. let currentZIndex = 100;
  33. // 目前已存在的弹窗
  34. const topWindow = window.top;
  35. topWindow.dialogInstances = topWindow.dialogInstances || [];
  36. // 新建弹窗
  37. function createSsDialogInstance(setting, callbackEvent) {
  38. currentZIndex += 10; // 动态提升 z-index
  39. const container = document.createElement("div");
  40. document.body.appendChild(container);
  41. const app = Vue.createApp({
  42. render() {
  43. return h(SsDialog, {
  44. ...setting,
  45. zIndex: currentZIndex,
  46. onClose() {
  47. document.body.removeChild(container); // 仅移除弹窗容器
  48. const index = topWindow.dialogInstances.indexOf(app);
  49. if (index > -1) {
  50. topWindow.dialogInstances.splice(index, 1); // 移除实例
  51. }
  52. // 关闭后的回调
  53. if (callbackEvent && typeof callbackEvent === "function") {
  54. callbackEvent();
  55. }
  56. app.unmount(); // 仅卸载弹窗实例
  57. if (container.parentNode) {
  58. container.parentNode.removeChild(container); // 确保移除容器
  59. }
  60. },
  61. });
  62. },
  63. });
  64. topWindow.dialogInstances.push({ app, callbackEvent, container });
  65. app.component("ss-mark", SsMark); // 注册 ss-mark 组件
  66. app.component("ss-icon", SsIcon);
  67. app.component("ss-full-style-header", SsFullStyleHeader); // 注册 ss-full-style-header 组件
  68. app.mount(container);
  69. }
  70. // ss-breadcrumb 一级菜单页面面包屑
  71. const SsBreadcrumb = {
  72. name: "SsBreadcrumb",
  73. props: {
  74. level: {
  75. type: Object,
  76. default: null,
  77. },
  78. },
  79. setup(props) {
  80. const currentMenu = ref(null);
  81. const folderPath = ref([]);
  82. const eventBus = window.parent.sharedEventBus;
  83. // 监听页面变化
  84. onMounted(() => {
  85. // 获取初始页面
  86. currentMenu.value = eventBus.getState(EVEN_VAR.currentPage);
  87. folderPath.value = eventBus.getState("folderPath") || [];
  88. // 订阅页面变化
  89. eventBus.subscribe(EVEN_VAR.currentPage, (page) => {
  90. currentMenu.value = page;
  91. });
  92. eventBus.subscribe("folderPath", (path) => {
  93. folderPath.value = path || [];
  94. });
  95. });
  96. // 修改点击处理函数
  97. const handlePathClick = (index) => {
  98. if (props.level?.onBack) {
  99. // 截取到点击的位置,后面的路径会被销毁
  100. const newPath = folderPath.value.slice(0, index + 1);
  101. eventBus.publish("folderPath", newPath);
  102. // 返回到对应层级
  103. const targetFolder = newPath[newPath.length - 1]?.folder || null;
  104. props.level.onBack(targetFolder);
  105. }
  106. };
  107. const SsCommonIcon = resolveComponent("SsCommonIcon");
  108. return () =>
  109. h("div", { class: "bread-crumb" }, [
  110. currentMenu.value &&
  111. h(
  112. "div",
  113. {
  114. onClick: () => {
  115. if (props.level?.onBack) {
  116. eventBus.publish("folderPath", []);
  117. props.level.onBack(null); // 返回到根目录
  118. } else {
  119. eventBus.publish(EVEN_VAR.currentPage, currentMenu.value);
  120. }
  121. },
  122. },
  123. currentMenu.value.label || currentMenu.value.name
  124. ),
  125. ...(folderPath.value || [])
  126. .map((folder, index) => [
  127. h(SsCommonIcon, { class: "common-icon-arrow-right" }),
  128. h(
  129. "div",
  130. {
  131. class: "bread-crumb-item",
  132. onClick: () => handlePathClick(index),
  133. style: { cursor: "pointer" },
  134. },
  135. folder.title
  136. ),
  137. ])
  138. .flat(),
  139. ]);
  140. },
  141. };
  142. // ss-input form表单的输入
  143. const SsInput = {
  144. name: "SsInp", //把SsInput改为SsInp Ben(20251225)
  145. inheritAttrs: false, // 不直接继承属性到组件根元素
  146. props: {
  147. name: {
  148. type: String,
  149. required: true,
  150. default: "",
  151. },
  152. // 接收 v-model 绑定的值
  153. errTip: {
  154. type: String,
  155. },
  156. required: {
  157. type: Boolean,
  158. default: false,
  159. },
  160. placeholder: {
  161. type: String,
  162. default: "请输入",
  163. },
  164. defaultValue: [String, Number],
  165. modelValue: [String, Number],
  166. // 新增:附件配置
  167. fj: {
  168. type: Object,
  169. default: null,
  170. },
  171. // 新增:param 配置(用于附件功能)
  172. param: {
  173. type: Object,
  174. default: null,
  175. },
  176. // 新增:高度配置
  177. height: {
  178. type: String,
  179. default: "",
  180. },
  181. // 功能说明:传 height 时允许回车换行(多行输入);未传 height 默认单行 by xu 20260204
  182. },
  183. emits: ["update:modelValue", "input", "blur", "change"], // 允许更新 v-model 绑定的值
  184. setup(props, { emit }) {
  185. const errMsg = ref("");
  186. const inputRef = ref(null);
  187. const textareaRef = ref(null);
  188. const inputValue = ref(props.modelValue || props.defaultValue || "");
  189. const contentFloatingDiv = ref(false); // 控制浮动 DIV 的显示
  190. const floatingDivPosition = ref("bottom"); // 'bottom' 或 'top'
  191. const isFocused = ref(false); // 跟踪焦点状态
  192. // 附件相关变量(仅在传入 param 时初始化)
  193. let fjid = ref(null);
  194. let fjName = null;
  195. let mode = null;
  196. if (props.param && props.param.button) {
  197. fjid = ref(props.param.button.val);
  198. fjName = props.param.button.desc;
  199. mode = props.param.mode;
  200. }
  201. // 功能说明:只有存在非空校验(notNull)且值为空时,才显示红线 by xu 20260402
  202. const hasNotNullRule = () => {
  203. const validations = window.ssVm?.validations?.get(props.name);
  204. if (!validations || validations.length === 0) return false;
  205. return validations.some(v => v.ruleName && v.ruleName.includes('notNull'));
  206. };
  207. const showRequired = computed(() => {
  208. const validations = window.ssVm?.validations?.get(props.name);
  209. if (!validations || validations.length === 0) return false;
  210. // 检查是否存在 notNull 规则
  211. const hasNotNull = validations.some(v => v.ruleName && v.ruleName.includes('notNull'));
  212. // 有非空规则且值为空时显示红线
  213. if (hasNotNull && !inputValue.value) {
  214. return true;
  215. }
  216. return false;
  217. });
  218. // 计算floatdiv应该向上还是向下展开
  219. const calculateFloatingDivPosition = () => {
  220. nextTick(() => {
  221. const textarea = inputRef.value;
  222. if (!textarea) return;
  223. const rect = textarea.getBoundingClientRect();
  224. const viewportHeight = window.innerHeight;
  225. // 预估floatdiv的高度(最多5行 * 20px + 上下padding + border)
  226. const estimatedFloatDivHeight = 20 * 5 + 10 + 2; // 5行 + padding + border = 112px
  227. // 检查下方空间
  228. const spaceBelow = viewportHeight - rect.bottom;
  229. // 如果下方空间不足,且上方空间足够,则向上展开
  230. if (
  231. spaceBelow < estimatedFloatDivHeight &&
  232. rect.top > estimatedFloatDivHeight
  233. ) {
  234. floatingDivPosition.value = "top";
  235. } else {
  236. floatingDivPosition.value = "bottom";
  237. }
  238. });
  239. };
  240. // 计算floatdiv的top偏移量
  241. const getFloatingDivTop = computed(() => {
  242. if (props.height) {
  243. // 有height时,padding是5px
  244. return "5px";
  245. } else {
  246. // 没有height时是单行居中,需要计算居中位置
  247. // 假设input-container高度是32px(或者从CSS读取),单行20px
  248. // 居中偏移 = (容器高度 - 行高) / 2 = (32 - 20) / 2 = 6px
  249. return "6px";
  250. }
  251. });
  252. const validate = () => {
  253. if (window.ssVm) {
  254. const result = window.ssVm.validateField(props.name);
  255. errMsg.value = result.valid ? "" : result.message;
  256. }
  257. };
  258. // 使用 watch 监听 props.errTip 和 props.modelValue 的变化
  259. watch(
  260. () => props.errTip,
  261. (newVal) => {
  262. errMsg.value = newVal;
  263. },
  264. { immediate: true }
  265. );
  266. watch(
  267. () => props.modelValue,
  268. (newVal) => {
  269. inputValue.value = newVal;
  270. }
  271. );
  272. // 挂载时的逻辑
  273. onMounted(() => {
  274. errMsg.value = props.errTip;
  275. inputValue.value = props.modelValue || props.defaultValue || "";
  276. });
  277. // 计算并调整textarea的高度
  278. const adjustHeight = () => {
  279. nextTick(() => {
  280. const textarea = textareaRef.value;
  281. if (!textarea) return;
  282. // floatDiv的textarea始终自动计算高度,不受props.height影响
  283. // 重置高度以获得正确的scrollHeight
  284. textarea.style.height = "auto";
  285. // 计算新高度 - 统一限制为5行
  286. const lineHeight = parseInt(
  287. getComputedStyle(textarea).lineHeight,
  288. 10
  289. );
  290. const maxHeight = lineHeight * 5; // 统一为5行
  291. const newHeight = Math.min(textarea.scrollHeight, maxHeight);
  292. textarea.style.height = `${newHeight}px`;
  293. });
  294. };
  295. // 检查是否应该显示浮动窗口(需要同时满足:有焦点 + 内容超出)
  296. // 修复新增页面点击就出现floatdiv的问题 by xu 20251212
  297. const checkShouldShowFloatingDiv = () => {
  298. const textarea = inputRef.value;
  299. if (!textarea) return false;
  300. // 首先检查是否有内容,没有内容时不显示floatdiv by xu 20251212
  301. if (!inputValue.value || inputValue.value.toString().trim() === "") {
  302. console.log("[floatdiv] 内容为空,不显示floatdiv");
  303. return false;
  304. }
  305. // 判断内容是否超出 by xu 20251212
  306. // 同时检查横向和纵向溢出,任一方向溢出都应显示floatdiv
  307. // 纵向溢出需要加容差值,避免padding/border导致的误判 by xu 20251212
  308. const verticalTolerance = 5; // 容差值5px
  309. const isHorizontalOverflow =
  310. textarea.scrollWidth > textarea.clientWidth;
  311. const isVerticalOverflow =
  312. textarea.scrollHeight > textarea.clientHeight + verticalTolerance;
  313. const isOverflow = isHorizontalOverflow || isVerticalOverflow;
  314. console.log(
  315. "[floatdiv] 溢出检测 - scrollWidth:",
  316. textarea.scrollWidth,
  317. "clientWidth:",
  318. textarea.clientWidth,
  319. "horizontalOverflow:",
  320. isHorizontalOverflow
  321. );
  322. console.log(
  323. "[floatdiv] 溢出检测 - scrollHeight:",
  324. textarea.scrollHeight,
  325. "clientHeight:",
  326. textarea.clientHeight,
  327. "tolerance:",
  328. verticalTolerance,
  329. "verticalOverflow:",
  330. isVerticalOverflow
  331. );
  332. const shouldShow = isFocused.value && isOverflow;
  333. console.log(
  334. "[floatdiv] 最终判断 - isFocused:",
  335. isFocused.value,
  336. "isOverflow:",
  337. isOverflow,
  338. "shouldShow:",
  339. shouldShow
  340. );
  341. // 需要同时满足:有焦点 + 内容超出
  342. return shouldShow;
  343. };
  344. // 定义事件处理函数
  345. const onInput = (event) => {
  346. const newValue = event.target.value;
  347. inputValue.value = newValue;
  348. emit("update:modelValue", newValue);
  349. validate(); // 输入时验证
  350. nextTick(() => {
  351. // 检查是否需要显示浮动div
  352. contentFloatingDiv.value = checkShouldShowFloatingDiv();
  353. // 如果需要显示floatdiv,计算其位置
  354. if (contentFloatingDiv.value) {
  355. calculateFloatingDivPosition();
  356. }
  357. });
  358. adjustHeight();
  359. };
  360. const onFocus = (event) => {
  361. // 设置焦点状态为true
  362. isFocused.value = true;
  363. adjustHeight();
  364. // 检查是否应该显示浮动窗口
  365. nextTick(() => {
  366. contentFloatingDiv.value = checkShouldShowFloatingDiv();
  367. if (contentFloatingDiv.value) {
  368. calculateFloatingDivPosition();
  369. }
  370. });
  371. };
  372. // 失去焦点时进行验证
  373. const onBlur = (event) => {
  374. emit("blur", event.target);
  375. validate(); // 失焦时验证
  376. nextTick(() => {
  377. // 如果焦点不在 textarea 上,则隐藏浮动 div
  378. if (!document.activeElement.classList.contains("input-control")) {
  379. isFocused.value = false;
  380. contentFloatingDiv.value = false;
  381. }
  382. });
  383. };
  384. const onChange = (event) => {
  385. inputValue.value = event.target.value || "";
  386. emit("change", inputValue.value);
  387. };
  388. const onMouseover = (event) => {
  389. nextTick(() => {
  390. // setTimeout(contentFloatingDiv.value = true, 500)
  391. });
  392. };
  393. const onMouseleave = (event) => {
  394. // contentFloatingDiv.value = false
  395. };
  396. // 功能说明:传了 height 视为多行允许回车;未传 height 拦截回车(单行表现) by xu 20260204
  397. const onKeydown = (event) => {
  398. const allowMultiline =
  399. typeof props.height === "string" && props.height.trim() !== "";
  400. if (!allowMultiline && event.key === "Enter") {
  401. event.preventDefault();
  402. }
  403. };
  404. // 附件按钮点击处理(从 SsEditor 搬运)
  405. const onAttachmentClick = (e) => {
  406. e.preventDefault();
  407. if (!props.param || !props.param.button) {
  408. console.warn("未配置 param 参数");
  409. return;
  410. }
  411. console.log("附件点击了");
  412. console.log("param", props.param);
  413. console.log("cmsAddUrl", props.param.button.cmsAddUrl);
  414. // 如果 fjid 为空,先调用 cmsAddUrl 创建
  415. if (fjid.value == null || fjid.value == "") {
  416. $.ajax({
  417. type: "post",
  418. url: props.param.button.cmsAddUrl,
  419. async: false,
  420. data: {
  421. name: "fjid",
  422. ssNrObjName: "sh",
  423. ssNrObjId: "",
  424. },
  425. success: function (_fjid) {
  426. console.log("cmsAddUrl success", _fjid);
  427. fjid.value = _fjid;
  428. },
  429. });
  430. }
  431. // 构建参数字符串
  432. var str =
  433. "&nrid=T-" +
  434. fjid.value +
  435. "&objectId=" +
  436. fjid.value +
  437. "&objectName=" +
  438. fjName +
  439. "&callback=" +
  440. (window["fjidCallbackName"] || "");
  441. console.log("str", str);
  442. // 打开附件编辑对话框
  443. SS.openDialog({
  444. src: props.param.button.cmsUpdUrl + str,
  445. headerTitle: "编辑",
  446. width: 900,
  447. high: 664,
  448. zIndex: 51,
  449. });
  450. };
  451. return {
  452. errMsg,
  453. inputValue,
  454. showRequired,
  455. onInput,
  456. onBlur,
  457. onChange,
  458. onMouseover,
  459. onMouseleave,
  460. onKeydown, // 新增:键盘事件处理 by xu 20251212
  461. contentFloatingDiv,
  462. floatingDivPosition,
  463. getFloatingDivTop,
  464. inputRef,
  465. textareaRef,
  466. onFocus,
  467. onAttachmentClick,
  468. fjid, // 附件 ID,用于隐藏字段
  469. };
  470. },
  471. render() {
  472. const { resolveComponent, h } = Vue;
  473. const SsIcon = resolveComponent("ss-icon");
  474. const SsEditorIcon = resolveComponent("SsEditorIcon");
  475. // 构建主textarea的样式
  476. const mainTextareaStyle = {};
  477. if (this.height) {
  478. mainTextareaStyle.height = "auto";
  479. // mainTextareaStyle.paddingTop = '5px'; // 有高度时加上padding-top
  480. // mainTextareaStyle.paddingBottom = '5px'; // 有高度时加上padding-bottom
  481. } else {
  482. // 没有指定height时,固定为单行高度
  483. mainTextareaStyle.height = "20px"; // 行高20px
  484. mainTextareaStyle.lineHeight = "20px"; // 确保单行垂直居中
  485. mainTextareaStyle.display = "flex";
  486. mainTextareaStyle.marginBottom = "5px";
  487. }
  488. // 如果有附件按钮,为按钮留出空间
  489. if (this.fj || (this.param && this.param.button)) {
  490. //加上&&this.param.button条件 Ben(20251221)
  491. mainTextareaStyle.paddingRight = "75px";
  492. }
  493. const mainTextareaRows = this.height
  494. ? Math.floor(parseFloat("80px") / 20)
  495. : 1;
  496. return h("div", { class: "input" }, [
  497. h("div", { class: "input-container" }, [
  498. h("div", { class: "input", style: "padding:5px 0" }, [
  499. h("textarea", {
  500. ref: "inputRef",
  501. class: "input-control",
  502. name: this.name,
  503. value: this.inputValue,
  504. onInput: this.onInput,
  505. onFocus: this.onFocus,
  506. onBlur: this.onBlur,
  507. onChange: this.onChange,
  508. onKeydown: this.onKeydown, // 新增:禁止回车换行 by xu 20251212
  509. placeholder: this.placeholder,
  510. onMouseover: this.onMouseover, // 监听鼠标悬停
  511. onMouseleave: this.onMouseleave, // 监听鼠标离开
  512. rows: mainTextareaRows,
  513. ...this.$attrs,
  514. style: mainTextareaStyle,
  515. autocomplete: "off",
  516. }),
  517. // 附件按钮(优先使用 param,兼容旧的 fj)
  518. this.fj || (this.param && this.param.button) //加上&&this.param.button条件 Ben(20251221)
  519. ? h(
  520. "button",
  521. {
  522. type: "button",
  523. class: "fj-button",
  524. onClick: this.param
  525. ? this.onAttachmentClick
  526. : (e) => {
  527. e.preventDefault();
  528. console.log("附件配置:", this.fj);
  529. },
  530. },
  531. [
  532. h(SsEditorIcon, {
  533. class: "editor-icon-link",
  534. }),
  535. h("span", { class: "fj-button-text" }, "附件"),
  536. ]
  537. )
  538. : null,
  539. ]),
  540. this.contentFloatingDiv || ""
  541. ? h(
  542. "div",
  543. {
  544. class: "floating-div",
  545. style:
  546. this.floatingDivPosition === "bottom"
  547. ? {
  548. // 向下展开: 覆盖原输入框,top对齐首行
  549. top: this.getFloatingDivTop,
  550. bottom: "auto",
  551. }
  552. : {
  553. // 向上展开: 同样覆盖原输入框,但从底部开始计算
  554. top: "auto",
  555. bottom: this.height ? "5px" : "6px", // 对齐到原textarea的底部padding位置
  556. },
  557. },
  558. [
  559. h("textarea", {
  560. ref: "textareaRef",
  561. class: "input-control",
  562. value: this.inputValue,
  563. onInput: this.onInput,
  564. onBlur: this.onBlur,
  565. onFocus: this.onFocus,
  566. onKeydown: this.onKeydown, // 新增:禁止回车换行 by xu 20251212
  567. onMouseover: this.onMouseover, // 监听鼠标悬停
  568. onMouseleave: this.onMouseleave, // 监听鼠标离开
  569. autocomplete: "off",
  570. onVnodeMounted: (vnode) => {
  571. vnode.el.focus();
  572. },
  573. }),
  574. ]
  575. )
  576. : null,
  577. // this.errMsg ? h(SsValidate, { errMsg: this.errMsg }) : null,
  578. ]),
  579. // 附件相关的隐藏字段(仅在有 param 时才渲染)
  580. this.param && [
  581. // fjid 隐藏字段(只有当 fjid 有值时才渲染)
  582. this.fjid &&
  583. this.fjid.value &&
  584. h("input", {
  585. type: "hidden",
  586. name: "fjid",
  587. value: this.fjid.value,
  588. }),
  589. // 其他隐藏字段根据 name 生成
  590. /* 去掉。文本框不需要(这是富文本才有的) Ben 20251205
  591. h("input", {
  592. type: "hidden",
  593. name: this.name.replace(/wj$/, "") + "Edit",
  594. value: this.inputValue
  595. }),
  596. */
  597. // h("input", {
  598. // type: "hidden",
  599. // name: this.name.replace(/wj$/, "") + "wj",
  600. // value: this.inputValue
  601. // }),
  602. /* 去掉。文本框不需要(这是富文本才有的) Ben 20251205
  603. h("input", {
  604. type: "hidden",
  605. name: "ueditorpath",
  606. value: this.name
  607. }),
  608. */
  609. ],
  610. ]);
  611. },
  612. };
  613. // ss-normal-input 登录输入
  614. const SsLoginInput = {
  615. name: "SsLoginInput",
  616. inheritAttrs: false,
  617. props: {
  618. errTip: {
  619. type: String,
  620. },
  621. type: {
  622. type: String,
  623. default: "text",
  624. },
  625. required: {
  626. type: Boolean,
  627. default: false,
  628. },
  629. placeholder: {
  630. type: String,
  631. default: "请输入",
  632. },
  633. name: {
  634. type: String,
  635. default: "",
  636. },
  637. defaultValue: [String, Number],
  638. modelValue: [String, Number],
  639. },
  640. emits: ["update:modelValue", "input", "blur", "change"], // 允许更新 v-model 绑定的值
  641. setup(props, { emit }) {
  642. const errMsg = ref("");
  643. const inputRef = ref(null);
  644. const textareaRef = ref(null);
  645. const inputValue = ref(props.modelValue || props.defaultValue || "");
  646. // 使用 watch 监听 props.errTip 和 props.modelValue 的变化
  647. watch(
  648. () => props.errTip,
  649. (newVal) => {
  650. errMsg.value = newVal;
  651. },
  652. { immediate: true }
  653. );
  654. watch(
  655. () => props.modelValue,
  656. (newVal) => {
  657. inputValue.value = newVal;
  658. }
  659. );
  660. // 挂载时的逻辑
  661. onMounted(() => {
  662. errMsg.value = props.errTip;
  663. inputValue.value = props.modelValue || props.defaultValue || "";
  664. });
  665. // 定义事件处理函数
  666. const onInput = (event) => {
  667. const newValue = event.target.value;
  668. inputValue.value = newValue;
  669. emit("update:modelValue", newValue);
  670. };
  671. return { inputValue, onInput, inputRef, textareaRef };
  672. },
  673. render() {
  674. return h("div", { class: "input" }, [
  675. h("div", { class: "input-container" }, [
  676. h("div", { class: "input" }, [
  677. h("input", {
  678. ref: "inputRef",
  679. class: "input-control",
  680. name: this.name,
  681. value: this.inputValue,
  682. onInput: this.onInput,
  683. type: this.type,
  684. placeholder: this.placeholder,
  685. required: this.required,
  686. ...this.$attrs,
  687. autocomplete: "off",
  688. }),
  689. ]),
  690. ]),
  691. ]);
  692. },
  693. };
  694. // ss-login-button
  695. const SsLoginButton = {
  696. name: "SsLoginButton",
  697. inheritAttrs: false,
  698. props: {
  699. class: {
  700. type: String,
  701. default: "",
  702. },
  703. text: {
  704. type: String,
  705. default: "",
  706. },
  707. type: {
  708. type: String,
  709. default: "button",
  710. },
  711. },
  712. emits: ["click"],
  713. setup(props, { emit }) {
  714. // 定义事件处理函数
  715. const onClick = (event) => {
  716. // 发射一个 'click' 事件,你可以传递所需的参数
  717. emit("click", event);
  718. };
  719. return { props, onClick };
  720. },
  721. render() {
  722. const SsIcon = resolveComponent("ss-icon");
  723. const SsLoginIcon = resolveComponent("ss-login-icon");
  724. return h(
  725. "button",
  726. { class: "login-button", type: this.type, onClick: this.onClick },
  727. [
  728. h("span", [h(SsLoginIcon, { class: this.class })]),
  729. h("span", {}, this.text),
  730. ]
  731. );
  732. },
  733. };
  734. // ss-objp 下拉选择
  735. const SsObjp = {
  736. name: "SsObjp",
  737. inheritAttrs: false,
  738. props: {
  739. onchange: {
  740. //在此属性传入onChange的window全局回调函数,函数唯一参数是当前选中值 Ben(20251217)
  741. type: String,
  742. required: false,
  743. },
  744. filter: {
  745. type: String,
  746. required: false,
  747. },
  748. filterfield: {
  749. type: String,
  750. required: false,
  751. },
  752. // filterField: {
  753. // //此属性为页面表单元素的name用逗号分隔,作用为在向后台查询下拉菜单选项时,会带上这些name的表单元素的value值 Ben(20260313)
  754. // type: String,
  755. // required: false,
  756. // },
  757. cb: {
  758. type: String,
  759. required: true,
  760. },
  761. url: {
  762. type: String,
  763. required: true,
  764. },
  765. name: {
  766. type: String,
  767. required: true,
  768. },
  769. width: {
  770. type: String,
  771. default: "100%",
  772. },
  773. placeholder: {
  774. type: String,
  775. default: "请选择",
  776. },
  777. inp: {
  778. type: Boolean,
  779. default: false,
  780. },
  781. opt: {
  782. type: Array,
  783. default: () => [],
  784. },
  785. errTip: String,
  786. defaultValue: [String, Number],
  787. modelValue: [String, Number],
  788. direction: {
  789. type: String,
  790. default: "bottom",
  791. },
  792. },
  793. emits: ["update:modelValue", "input", "blur", "change"],
  794. setup(props, { emit }) {
  795. const canInput = props.inp;
  796. const errMsg = Vue.ref(props.errTip);
  797. const selectItem = Vue.ref({});
  798. let inputText = Vue.ref(""); // 用于存储输入框的文本
  799. const popupWinVisible = Vue.ref(false);
  800. const filteredOptions = Vue.ref(props.opt);
  801. const popupDirection = Vue.ref("bottom");
  802. const popupMaxHeight = Vue.ref("none"); // popup最大高度,用于空间不足时限制高度并出滚动条 by xu 20251212
  803. const popupContentAreaMaxHeight = Vue.computed(() => {
  804. if (!popupMaxHeight.value || popupMaxHeight.value === "none")
  805. return null;
  806. const maxHeightNum = Number.parseFloat(popupMaxHeight.value);
  807. if (!Number.isFinite(maxHeightNum)) return null;
  808. // 功能说明:滚动条统一落在 .content-area(CSS 默认如此),因此需要扣掉 popup-win padding-top(10) 与 popup-content padding(15*2) by xu 20260126
  809. const contentAreaMaxHeight = Math.max(60, maxHeightNum - 40);
  810. return `${contentAreaMaxHeight}px`;
  811. });
  812. // 修复表格内下拉弹层被 overflow 截断:popup 使用 Teleport 到 body + fixed 定位 by xu 20260126
  813. const selectContainerRef = Vue.ref(null);
  814. const popupRef = Vue.ref(null);
  815. const teleportRootStyle = Vue.ref({
  816. position: "fixed",
  817. left: "0",
  818. top: "0",
  819. width: "0",
  820. height: "0",
  821. zIndex: 9999,
  822. pointerEvents: "none",
  823. });
  824. const popupLayerStyle = Vue.ref({
  825. position: "fixed",
  826. left: "0",
  827. top: "0",
  828. bottom: "auto", // 功能说明:覆盖 .popup-win.top 的 bottom 定位,避免 fixed 场景被撑高/错位 by xu 20260126
  829. minWidth: "0",
  830. zIndex: 9999,
  831. pointerEvents: "auto",
  832. });
  833. // const showRequired = Vue.computed(() => {
  834. // const hasValidationRule = window.ssVm?.validations?.has(props.name);
  835. // if (!hasValidationRule) return false;
  836. // if (errMsg.value) return true;
  837. // if (!selectItem.value?.value) return true;
  838. // return false;
  839. // });
  840. const validate = () => {
  841. if (window.ssVm) {
  842. const result = window.ssVm.validateField(props.name);
  843. // console.log("validate", window.ssVm.validateField(props.name));
  844. errMsg.value = result.valid ? "" : result.message;
  845. }
  846. };
  847. //在objPicker界面,选中value对应的项
  848. const updateSelectItem = () => {
  849. // console.log(props.opt);
  850. const item = props.opt.find((it) => it.value === props.modelValue);
  851. if (item) {
  852. selectItem.value = item;
  853. inputText.value = item.label;
  854. } else {
  855. selectItem.value = { label: "", value: "" };
  856. inputText.value = "";
  857. }
  858. // validate();
  859. };
  860. Vue.watch(
  861. () => props.errTip,
  862. (newVal) => {
  863. errMsg.value = newVal;
  864. }
  865. );
  866. Vue.watch(() => props.modelValue, updateSelectItem, { immediate: true });
  867. Vue.watch(
  868. () => props.opt,
  869. (newVal) => {
  870. updateSelectItem();
  871. filteredOptions.value = [...newVal];
  872. // console.log("filteredOptions", filteredOptions.value);
  873. }
  874. );
  875. //初始化objPicker在页面刚打开时的默认值
  876. async function initDefaultValue() {
  877. try {
  878. if (props.url && props.cb && props.modelValue) {
  879. let objectPickerParam;
  880. let url = props.url;
  881. //如果有定义过滤器
  882. if (props.filter) {
  883. //包含HTML实体的JSON字符串转为JSON对象,如原字符串是{&quot;dwid&quot;:&quot;88&quot;},注意key也必需用单引号包着
  884. // const decodedString = props.filter.replace(/&quot;/g, '"'); // 转换为: {"dwid":"88"}
  885. // objectPickerParam = JSON.parse(decodedString); // 转为json对象
  886. const filterObj = props.filter; // 转为json对象
  887. for (let k in filterObj) {
  888. let v = filterObj[k];
  889. url += "&" + k + "=" + v;
  890. }
  891. objectPickerParam = props.filter; // 转为json对象
  892. objectPickerParam["input"] = props.inp;
  893. objectPickerParam["codebook"] = props.cb;
  894. // alert(url);
  895. } else {
  896. objectPickerParam = { input: props.inp, codebook: props.cb };
  897. }
  898. const objectPickerParamStr = JSON.stringify(objectPickerParam);
  899. const params = new URLSearchParams();
  900. params.append("objectpickerparam", objectPickerParamStr);
  901. params.append("objectpickertype", "2");
  902. params.append("objectpickervalue", props.modelValue); //需回显的值
  903. // alert("1params:"+JSON.stringify(params));
  904. axios
  905. .post(props.url, params, {
  906. headers: {
  907. "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
  908. },
  909. })
  910. .then((response) => {
  911. // alert(JSON.stringify(response.data));
  912. if ("timeout" == response.data.statusText) {
  913. alert("网络超时!");
  914. return;
  915. }
  916. if (response.data.result) {
  917. const keys = Object.keys(response.data.result);
  918. if (keys.length === 1) {
  919. let code = keys[0];
  920. let desc = response.data.result[keys[0]];
  921. if (props.opt)
  922. props.opt.length = 0; //通过修改数组的length属性,直接清空数组元素,内存会被自动释放。这是性能最优的方式
  923. else {
  924. props.opt = [];
  925. }
  926. props.opt.push({ label: desc, value: code });
  927. updateSelectItem();
  928. // alert('props.opt:'+JSON.stringify(props.opt));
  929. }
  930. }
  931. });
  932. }
  933. } catch (error) {
  934. // callback(null, error.message); // 失败回调,传递错误
  935. }
  936. }
  937. // Vue.onMounted(updateSelectItem);
  938. const doSelectItem = (item) => {
  939. emit("update:modelValue", item.value);
  940. selectItem.value = item;
  941. inputText.value = item.label;
  942. hidePopup();
  943. nextTick(() => {
  944. console.log(item.value + "@@@props.modelValue:" + props.modelValue);
  945. validate();
  946. if (window.ssVm) {
  947. // 遍历所有验证规则,找到依赖当前字段的规则
  948. for (const [field, rules] of window.ssVm.validations.entries()) {
  949. for (const rule of rules) {
  950. if (rule.opt?.relField === props.name) {
  951. // console.log("Found dependent field:", field); // 调试日志
  952. window.ssVm.validateField(field);
  953. }
  954. }
  955. }
  956. }
  957. });
  958. callGlobalOnchg(item.value, item.label); // 值变化时尝试调用全局onchange回调函数 Ben(20251217)
  959. };
  960. // 用于调用全局onchange回调函数 Ben(20251217)
  961. const callGlobalOnchg = (value, desc) => {
  962. // 检查 onchange 属性是否提供了有效的函数名
  963. if (props.onchange && typeof props.onchange === "string") {
  964. // 检查 window 对象上是否存在该函数
  965. if (
  966. typeof window !== "undefined" &&
  967. window[props.onchange] &&
  968. typeof window[props.onchange] === "function"
  969. ) {
  970. try {
  971. window[props.onchange](value, desc); // 调用全局函数,并传入当前选中值
  972. } catch (error) {
  973. console.error(`调用全局函数 ${props.onchange} 时出错:`, error);
  974. }
  975. } else {
  976. console.warn(`全局函数 ${props.onchange} 未定义或不是一个函数。`);
  977. }
  978. }
  979. };
  980. //可录入的objPicker,更新下拉菜单选项
  981. async function updateOptionBYInputText(inpTxt) {
  982. try {
  983. let objectPickerParam;
  984. let url = props.url;
  985. if (props.url && props.cb) {
  986. //如果有定义过滤器
  987. if (props.filter || props.filterfield) {
  988. let filterObj = props.filter;
  989. if (!props.filter) filterObj = {};
  990. if (props.filter) {
  991. const filterObj = props.filter; // 转为json对象
  992. for (let k in filterObj) {
  993. let v = filterObj[k];
  994. url += "&" + k + "=" + v;
  995. }
  996. }
  997. if (props.filterfield) {
  998. //加上filterfield的值过滤
  999. let filterfieldArr = props.filterfield.split(",");
  1000. for (var i = 0; i < filterfieldArr.length; i++) {
  1001. let fieldName = filterfieldArr[i];
  1002. let fields = document.getElementsByName(fieldName);
  1003. if (!fields || fields.length < 1) {
  1004. alert("下拉菜单配置的过滤条件" + fieldName + "不存在!");
  1005. continue;
  1006. }
  1007. let v = null;
  1008. for (let j = 0; j < fields.length; j++) {
  1009. if (fields[j].value) {
  1010. v = fields[j].value;
  1011. break;
  1012. }
  1013. }
  1014. // let field = document.getElementsByName(fieldName)[0];
  1015. // let v = field.value;
  1016. if (v) {
  1017. url += "&" + fieldName + "=" + v;
  1018. filterObj[fieldName] = v;
  1019. }
  1020. }
  1021. console.log("filterfield url:" + url);
  1022. }
  1023. //包含HTML实体的JSON字符串转为JSON对象,如原字符串是{&quot;dwid&quot;:&quot;88&quot;},注意key也必需用单引号包着
  1024. // const decodedString = props.filter.replace(/&quot;/g, '"'); // 转换为: {"dwid":"88"}
  1025. // objectPickerParam = JSON.parse(decodedString); // 转为json对象
  1026. objectPickerParam = filterObj;
  1027. objectPickerParam["input"] = props.inp;
  1028. objectPickerParam["codebook"] = props.cb;
  1029. // alert(url);
  1030. } else {
  1031. objectPickerParam = { input: props.inp, codebook: props.cb };
  1032. }
  1033. const objectPickerParamStr = JSON.stringify(objectPickerParam);
  1034. const params = new URLSearchParams();
  1035. params.append("objectpickerparam", objectPickerParamStr);
  1036. params.append("objectpickertype", "1");
  1037. if (props.inp && props.inp === true) {
  1038. //把"true"改为true Ben(20251209)
  1039. params.append("objectpickersearchAll", 0); //只查录入的值
  1040. params.append("objectpickerinput", inpTxt); //录入的值
  1041. } else {
  1042. params.append("objectpickersearchAll", 1);
  1043. }
  1044. axios
  1045. .post(url, params, {
  1046. headers: {
  1047. "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
  1048. },
  1049. })
  1050. .then((response) => {
  1051. if ("timeout" == response.data.statusText) {
  1052. alert("网络超时!");
  1053. return;
  1054. }
  1055. // 先清空选项 by xu 20251212
  1056. if (props.opt) {
  1057. props.opt.length = 0;
  1058. } else {
  1059. props.opt = [];
  1060. }
  1061. if (response.data.result) {
  1062. const keys = Object.keys(response.data.result);
  1063. // console.log("params:"+params+"@@response.data:"+JSON.stringify(response.data));
  1064. if (keys.length > 0) {
  1065. for (let k in response.data.result) {
  1066. props.opt.push({
  1067. label: response.data.result[k],
  1068. value: k,
  1069. });
  1070. }
  1071. // console.log('###inpTxt:'+inpTxt+';');
  1072. if (
  1073. props.inp &&
  1074. props.inp === true && //把"true"改为true Ben(20251209)
  1075. inpTxt.length > 0
  1076. ) {
  1077. //对于可录入的,用已录入的值作过滤
  1078. filteredOptions.value = props.opt.filter((option) =>
  1079. option.label
  1080. .toLowerCase()
  1081. .includes(inputText.value.toLowerCase())
  1082. );
  1083. // 可录入的objPicker,当搜索结果只有一项时,自动选中这一项 by xu 20251212
  1084. if (filteredOptions.value.length === 1) {
  1085. const autoSelectItem = filteredOptions.value[0];
  1086. console.log(
  1087. "[objp] 搜索结果只有一项,自动选中:",
  1088. autoSelectItem
  1089. );
  1090. doSelectItem(autoSelectItem);
  1091. return; // 自动选中后直接返回,不需要显示popup
  1092. }
  1093. filteredOptions.value.unshift({ label: "", value: "" });
  1094. // console.log('###做了过滤:'+inputText.value.toLowerCase()+';');
  1095. } else {
  1096. filteredOptions.value = props.opt;
  1097. filteredOptions.value.unshift({ label: "", value: "" });
  1098. }
  1099. console.log("props.opt11:" + JSON.stringify(props.opt));
  1100. } else {
  1101. // 没有数据时,清空过滤选项 by xu 20251212
  1102. filteredOptions.value = [];
  1103. console.log("[objp] 接口返回空数据");
  1104. }
  1105. } else {
  1106. // result不存在时,清空过滤选项 by xu 20251212
  1107. filteredOptions.value = [];
  1108. console.log("[objp] 接口返回无result");
  1109. }
  1110. // 无论是否有数据,都显示popup by xu 20251212
  1111. openPopup(); // Teleport 场景下统一打开并重定位 by xu 20260126
  1112. });
  1113. }
  1114. } catch (error) {
  1115. // callback(null, error.message); // 失败回调,传递错误
  1116. }
  1117. }
  1118. // 计算弹出方向和最大高度的方法 by xu 20251212
  1119. // 当空间不足时限制popup高度并显示滚动条
  1120. const calculatePopupDirection = () => {
  1121. const triggerEl =
  1122. selectContainerRef.value?.querySelector(".input") ||
  1123. selectContainerRef.value;
  1124. if (!triggerEl) return;
  1125. const selectRect = triggerEl.getBoundingClientRect();
  1126. const viewportHeight = window.innerHeight;
  1127. // 3. 计算上下可用空间
  1128. const spaceBelow = viewportHeight - selectRect.bottom - 10; // 减10px留边距
  1129. const spaceAbove = selectRect.top - 10; // 减10px留边距
  1130. // 4. popup预估高度(假设每项36px,最多显示8项 + padding)
  1131. const estimatedPopupHeight = 300;
  1132. const minPopupHeight = 100; // 最小高度
  1133. console.log(
  1134. "[popup] 空间计算 - spaceAbove:",
  1135. spaceAbove,
  1136. "spaceBelow:",
  1137. spaceBelow,
  1138. "estimatedHeight:",
  1139. estimatedPopupHeight
  1140. );
  1141. // 5. 判断方向和最大高度 by xu 20251212
  1142. if (spaceBelow >= estimatedPopupHeight) {
  1143. // 下方空间足够,向下展开,不限制高度
  1144. popupDirection.value = "bottom";
  1145. popupMaxHeight.value = "none";
  1146. console.log("[popup] 向下展开,空间充足");
  1147. } else if (spaceAbove >= estimatedPopupHeight) {
  1148. // 上方空间足够,向上展开,不限制高度
  1149. popupDirection.value = "top";
  1150. popupMaxHeight.value = "none";
  1151. console.log("[popup] 向上展开,空间充足");
  1152. } else {
  1153. // 上下空间都不足,选择空间大的方向,并限制高度出滚动条
  1154. if (spaceBelow >= spaceAbove) {
  1155. popupDirection.value = "bottom";
  1156. popupMaxHeight.value = Math.max(spaceBelow, minPopupHeight) + "px";
  1157. console.log(
  1158. "[popup] 向下展开,空间不足,限制高度:",
  1159. popupMaxHeight.value
  1160. );
  1161. } else {
  1162. popupDirection.value = "top";
  1163. popupMaxHeight.value = Math.max(spaceAbove, minPopupHeight) + "px";
  1164. console.log(
  1165. "[popup] 向上展开,空间不足,限制高度:",
  1166. popupMaxHeight.value
  1167. );
  1168. }
  1169. }
  1170. };
  1171. // Teleport popup 的定位(fixed) by xu 20260126
  1172. const updatePopupPosition = () => {
  1173. const triggerEl =
  1174. selectContainerRef.value?.querySelector(".input") ||
  1175. selectContainerRef.value;
  1176. if (!triggerEl) return;
  1177. const triggerRect = triggerEl.getBoundingClientRect();
  1178. const margin = 10;
  1179. const viewportWidth = window.innerWidth;
  1180. const viewportHeight = window.innerHeight;
  1181. const popupGap = 0; // 功能说明:定位不再做 -10 重叠,让 padding-top 自然形成间距 by xu 20260126
  1182. // 先给一个初始位置,确保下一帧可以测量弹层尺寸 by xu 20260126
  1183. popupLayerStyle.value = {
  1184. position: "fixed",
  1185. left: `${Math.max(margin, triggerRect.left)}px`,
  1186. top: `${Math.max(margin, triggerRect.bottom + popupGap)}px`, // 功能说明:与输入框底部对齐 by xu 20260126
  1187. bottom: "auto", // 功能说明:fixed 场景显式取消 bottom,避免与 top 同时生效 by xu 20260126
  1188. minWidth: `${Math.max(0, triggerRect.width)}px`,
  1189. zIndex: 9999,
  1190. pointerEvents: "auto",
  1191. };
  1192. Vue.nextTick(() => {
  1193. const popupEl = popupRef.value;
  1194. if (!popupEl) return;
  1195. const popupRect = popupEl.getBoundingClientRect();
  1196. const maxLeft = viewportWidth - popupRect.width - margin;
  1197. const left = Math.min(
  1198. Math.max(margin, triggerRect.left),
  1199. Math.max(margin, maxLeft)
  1200. );
  1201. let top;
  1202. if (popupDirection.value === "top") {
  1203. top = triggerRect.top - popupRect.height - popupGap; // 功能说明:向上展开时与输入框顶部对齐 by xu 20260126
  1204. top = Math.max(margin, top);
  1205. } else {
  1206. top = triggerRect.bottom + popupGap; // 功能说明:向下展开时与输入框底部对齐 by xu 20260126
  1207. if (top + popupRect.height > viewportHeight - margin) {
  1208. top = Math.max(
  1209. margin,
  1210. viewportHeight - popupRect.height - margin
  1211. );
  1212. }
  1213. }
  1214. popupLayerStyle.value = {
  1215. ...popupLayerStyle.value,
  1216. left: `${left}px`,
  1217. top: `${top}px`,
  1218. bottom: "auto", // 功能说明:无论 top/bottom 展开都用 top 定位,禁用 bottom by xu 20260126
  1219. };
  1220. });
  1221. };
  1222. const openPopup = () => {
  1223. if (!popupWinVisible.value) popupWinVisible.value = true;
  1224. Vue.nextTick(() => {
  1225. calculatePopupDirection();
  1226. updatePopupPosition();
  1227. });
  1228. };
  1229. // Teleport 场景下:滚动/缩放重定位 by xu 20260126
  1230. const handleViewportChange = () => {
  1231. if (!popupWinVisible.value) return;
  1232. calculatePopupDirection();
  1233. updatePopupPosition();
  1234. };
  1235. // Teleport 场景下:点击外部关闭 by xu 20260126
  1236. const onDocPointerDown = (event) => {
  1237. if (!popupWinVisible.value) return;
  1238. const target = event.target;
  1239. if (selectContainerRef.value?.contains(target)) return;
  1240. if (popupRef.value?.contains(target)) return;
  1241. hidePopup();
  1242. };
  1243. //点击下拉菜单的文本区域时,会触发的方法
  1244. function togglePopup() {
  1245. // 可录入的 objPicker,更新下拉菜单选项
  1246. updateOptionBYInputText(inputText.value);
  1247. // popupWinVisible.value = !popupWinVisible.value;
  1248. Vue.nextTick(() => {
  1249. calculatePopupDirection();
  1250. updatePopupPosition(); // Teleport 场景下同步定位 by xu 20260126
  1251. });
  1252. }
  1253. const hidePopup = () => {
  1254. popupWinVisible.value = false;
  1255. };
  1256. //点击下拉菜单的三角形时,会触发的方法
  1257. // 添加toggle逻辑,点击时切换显示/隐藏 by xu 20251212
  1258. const suffixClick = () => {
  1259. // 如果popup已显示,则关闭 by xu 20251212
  1260. if (popupWinVisible.value) {
  1261. hidePopup();
  1262. console.log("[objp] 点三角关闭popup");
  1263. return;
  1264. }
  1265. //可录入的objPicker,更新下拉菜单选项
  1266. updateOptionBYInputText("");
  1267. Vue.nextTick(() => {
  1268. calculatePopupDirection();
  1269. updatePopupPosition(); // Teleport 场景下同步定位 by xu 20260126
  1270. });
  1271. console.log("[objp] 点三角打开popup");
  1272. };
  1273. //可录入的objPicker,录入项变化时,会触发
  1274. async function handleInputChange(event) {
  1275. inputText.value = event.target.value;
  1276. if (!inputText.value) {
  1277. inputText.value = "";
  1278. }
  1279. //可录入的objPicker,更新下拉菜单选项
  1280. updateOptionBYInputText(inputText.value);
  1281. // filteredOptions.value = props.opt.filter((option) =>
  1282. // option.label.toLowerCase().includes(inputText.value.toLowerCase())
  1283. // );
  1284. // if (!popupWinVisible.value) {
  1285. // popupWinVisible.value = true; // 确保下拉框在输入时打开
  1286. // }
  1287. }
  1288. Vue.onMounted(() => {
  1289. initDefaultValue();
  1290. // Teleport 场景下:滚动/缩放需要重定位(scroll 用 capture 捕获容器滚动) by xu 20260126
  1291. window.addEventListener("resize", handleViewportChange);
  1292. window.addEventListener("scroll", handleViewportChange, true);
  1293. // 点击外部关闭(原本依赖 mouseleave,Teleport 后会误关) by xu 20260126
  1294. document.addEventListener("pointerdown", onDocPointerDown, true);
  1295. });
  1296. Vue.onUnmounted(() => {
  1297. window.removeEventListener("resize", handleViewportChange);
  1298. window.removeEventListener("scroll", handleViewportChange, true);
  1299. document.removeEventListener("pointerdown", onDocPointerDown, true);
  1300. });
  1301. return {
  1302. errMsg,
  1303. selectItem,
  1304. inputText,
  1305. canInput,
  1306. filteredOptions,
  1307. popupWinVisible,
  1308. popupDirection,
  1309. popupMaxHeight, // 添加popup最大高度 by xu 20251212
  1310. popupContentAreaMaxHeight,
  1311. selectContainerRef,
  1312. popupRef,
  1313. teleportRootStyle,
  1314. popupLayerStyle,
  1315. suffixClick,
  1316. togglePopup,
  1317. hidePopup,
  1318. doSelectItem,
  1319. handleInputChange,
  1320. };
  1321. },
  1322. template: `
  1323. <div class="input" style="position: relative" :style="{width: width}">
  1324. <div class="select-container" ref="selectContainerRef">
  1325. <div class="input" @click="togglePopup">
  1326. <input
  1327. type="hidden"
  1328. :name="name"
  1329. :value="selectItem.value"
  1330. .value="selectItem.value"
  1331. />
  1332. <input
  1333. v-model="inputText"
  1334. @input="handleInputChange"
  1335. v-if="canInput"
  1336. :placeholder="placeholder"
  1337. />
  1338. <input
  1339. v-else
  1340. :placeholder="placeholder"
  1341. :value="selectItem.label"
  1342. disabled
  1343. style="pointer-events: none;"
  1344. />
  1345. <div class="suffix" @click.stop="suffixClick">
  1346. <ss-form-icon :class="popupWinVisible ? 'form-icon-transform-select select' : 'form-icon-select'" />
  1347. </div>
  1348. </div>
  1349. <!-- 修复表格内弹层被截断:popup Teleport 到 body by xu 20260126 -->
  1350. <teleport to="body">
  1351. <div v-show="popupWinVisible" class="select-container ss-objp-teleport-root" :style="teleportRootStyle">
  1352. <!-- popup弹出层,添加maxHeight和overflowY支持空间不足时滚动 by xu 20251212 -->
  1353. <div ref="popupRef" class="popup-win" :class="popupDirection" :style="[popupLayerStyle, { maxHeight: popupMaxHeight !== 'none' ? popupMaxHeight : 'none', overflowY: 'visible' }]">
  1354. <div v-if="opt && opt.length && filteredOptions.length > 0" class="popup-content">
  1355. <div class="content-area" :style="popupContentAreaMaxHeight ? { maxHeight: popupContentAreaMaxHeight, overflowY: 'auto' } : null">
  1356. <div v-for="(item, index) in filteredOptions" :key="index" @click="doSelectItem(item)" :class="{ active: item.value === selectItem.value }">
  1357. <span class="check-icon">
  1358. <ss-form-icon class="form-icon-select-checked" />
  1359. </span>
  1360. <span>{{ item.label }}</span>
  1361. </div>
  1362. </div>
  1363. </div>
  1364. <div v-else class="popup-content"><div class="content-area"><div class="content-area"> <span>无选项</span></div></div></div>
  1365. </div>
  1366. </div>
  1367. </teleport>
  1368. </div>
  1369. </div>
  1370. `,
  1371. };
  1372. // ss-hidden 隐藏字段组件
  1373. const SsHidden = {
  1374. name: "SsHidden",
  1375. props: {
  1376. modelValue: String,
  1377. name: {
  1378. type: String,
  1379. required: true,
  1380. },
  1381. rule: {
  1382. type: String,
  1383. required: true,
  1384. },
  1385. param: {
  1386. type: String,
  1387. required: true,
  1388. },
  1389. url: {
  1390. type: String,
  1391. required: true,
  1392. },
  1393. },
  1394. emits: ["update:modelValue"],
  1395. setup(props, { emit }) {
  1396. const errMsg = Vue.ref("");
  1397. const validate = () => {
  1398. if (window.ssVm) {
  1399. const result = window.ssVm.validateField(props.name);
  1400. console.log("validate", window.ssVm.validateField(props.name));
  1401. errMsg.value = result.valid ? "" : result.message;
  1402. }
  1403. };
  1404. Vue.onMounted(() => {
  1405. /**
  1406. * 初始化级联菜单值初始值思路:
  1407. * 1. 带隐藏字段(即带编码规则)的级联菜单
  1408. * 在隐藏字段这,可以取到要回显的值和编码规则,从而计算出各级下拉菜单要回显的值。
  1409. * 然后通过ajax取各级级联菜单的值回显。
  1410. * 2. 不带隐藏字段的级联,只能在各个下拉菜单的setup事件中通过ajax取回显值回显
  1411. */
  1412. // 当同组级联下拉菜单选中值变化时,会调用本隐藏字段下面这方法设置隐藏字段值
  1413. window.addEventListener(
  1414. "cascader-setHiddenVal-" + props.name,
  1415. (event) => {
  1416. const { value } = event.detail;
  1417. emit("update:modelValue", value);
  1418. console.log(value);
  1419. setTimeout(() => {
  1420. validate();
  1421. }, 50);
  1422. }
  1423. );
  1424. // 如果有初始值,触发回显过程
  1425. if (props.modelValue) {
  1426. console.log("级联隐藏字段,开始回显,初始值:", props.modelValue);
  1427. triggerCascaderEcho(props.modelValue);
  1428. validate();
  1429. }
  1430. });
  1431. // 触发级联回显
  1432. const triggerCascaderEcho = (code) => {
  1433. /**
  1434. * 开始回显,初始值: 440304
  1435. * 解析后的所有值: Array(3)0: "440000"1: "440300"2: "440304"length: 3[[Prototype]]: Array(0)
  1436. */
  1437. const values = parseHiddenCodeForAll(code, props.rule);
  1438. console.log("解析后的所有值:", values);
  1439. // 转换为 JSON 对象
  1440. // const paramObj = JSON.parse(props.param);
  1441. const paramObj = props.param;
  1442. let selectArr = paramObj.fieldOrd; //保存本组级联菜单项的数组,如:['hksheng','hkshi','hkxian']
  1443. if (selectArr.length != values.length) {
  1444. // alert('属性'+props.name+'的值'+code+'与级联菜单中下拉菜单的数目不匹配!');
  1445. return;
  1446. }
  1447. // 按顺序触发回显,并增加延迟确保数据加载
  1448. /**
  1449. * 通过隐藏字段的setup事件,
  1450. * 循环遍历各级下拉菜单,并触发定义在下拉菜单中的'cascader-echo'事件,
  1451. * 在此事件中完成每个下拉菜单回显值操作(只取当前要回显的键值对显示,
  1452. * 下拉菜单所有的值,在点击下拉菜单时,才通过ajax取)。
  1453. */
  1454. values.forEach((value, index) => {
  1455. if (value) {
  1456. setTimeout(() => {
  1457. let upperVal = undefined;
  1458. if (index != 0) {
  1459. upperVal = values[index - 1];
  1460. }
  1461. const echoEvent = new CustomEvent(
  1462. "cascader-echo-" + selectArr[index],
  1463. {
  1464. detail: {
  1465. name: props.name,
  1466. value: value,
  1467. // level: index + 1,
  1468. isAuto: true, // 标记为自动回显
  1469. upperVal: upperVal,
  1470. },
  1471. }
  1472. );
  1473. console.log(props.name + "--upperValue:" + upperVal);
  1474. window.dispatchEvent(echoEvent);
  1475. }, index * 500); // 每级增加500ms延迟
  1476. }
  1477. });
  1478. };
  1479. // 解析所有级别的代码
  1480. const parseHiddenCodeForAll = (code, rule) => {
  1481. if (!code || !rule) return [];
  1482. // 获取规则中每段的长度
  1483. const segments = [];
  1484. let currentChar = rule[0];
  1485. let currentLength = 1;
  1486. for (let i = 1; i < rule.length; i++) {
  1487. if (rule[i] === currentChar) {
  1488. currentLength++;
  1489. } else {
  1490. segments.push(currentLength);
  1491. currentChar = rule[i];
  1492. currentLength = 1;
  1493. }
  1494. }
  1495. segments.push(currentLength);
  1496. // 解析每一级的值
  1497. const values = [];
  1498. let position = 0;
  1499. segments.forEach((length, index) => {
  1500. const value = code
  1501. .substring(0, position + length)
  1502. .padEnd(rule.length, "0");
  1503. values.push(value);
  1504. position += length;
  1505. });
  1506. return values;
  1507. };
  1508. watchEffect(() => {});
  1509. return {};
  1510. },
  1511. template: `<input type="hidden" :name="name" :value="modelValue">`,
  1512. };
  1513. // ss-cascader 级联选择器
  1514. const SsCcp = {
  1515. name: "SsCcp",
  1516. inheritAttrs: false,
  1517. props: {
  1518. modelValue: String,
  1519. name: {
  1520. type: String,
  1521. required: true,
  1522. },
  1523. level: {
  1524. type: Number,
  1525. required: true,
  1526. },
  1527. opt: {
  1528. type: Array,
  1529. default: () => [],
  1530. },
  1531. placeholder: {
  1532. type: String,
  1533. default: "请选择",
  1534. },
  1535. width: {
  1536. type: String,
  1537. default: "150px",
  1538. },
  1539. direction: {
  1540. type: String,
  1541. default: "bottom",
  1542. },
  1543. mode: {
  1544. type: String,
  1545. default: "1",
  1546. },
  1547. //级联菜单配置参数,如果是数组,则代表本下拉菜单是多套级联菜单共用的第一级菜单。如果是对象,则只有一套级联菜单用此下拉菜单。
  1548. param: {
  1549. type: String,
  1550. required: true,
  1551. },
  1552. //向后台拿数据的url
  1553. url: {
  1554. type: String,
  1555. required: true,
  1556. },
  1557. },
  1558. emits: ["update:modelValue", "change"],
  1559. setup(props, { emit }) {
  1560. // alert('级联菜单初始化:'+props.name+':--:'+props.modelValue);
  1561. const selectItem = Vue.ref({ label: props.placeholder, value: "" });
  1562. const popupWinVisible = Vue.ref(false);
  1563. const isAutoEcho = Vue.ref(false); // 用于标记是否是自动回显
  1564. const upperValue = Vue.ref(""); //上级下拉菜单当前值,在初始化下拉菜单默认值时,和上级下拉菜单的值变化时,修改此upperValue变量
  1565. const popupDirection = Vue.ref("bottom");
  1566. const popupMaxHeight = Vue.ref("none"); // popup最大高度,用于空间不足时限制高度并出滚动条 by xu 20251212
  1567. //有隐藏字段的下拉菜单,加载菜单项并展开事件
  1568. // 被上级下拉菜单选中值后,触发本下拉菜单刷新菜单项并弹出显示
  1569. window.addEventListener("cascader-open-" + props.name, async (event) => {
  1570. const { upperVal } = event.detail;
  1571. upperValue.value = upperVal;
  1572. console.log(
  1573. "22props.name:" +
  1574. props.name +
  1575. ",22props.upperValue:" +
  1576. upperValue.value
  1577. );
  1578. selectItem.value = ""; //清除本下拉菜单当前选中的值
  1579. emit("update:modelValue", ""); //通知父级
  1580. //清空下拉菜单,并设置第一项的值为placeholder
  1581. clearAndInit1stOpt();
  1582. //下个下拉菜单名
  1583. let nextSelName = getNextSel(props.name, props.param.fieldOrd);
  1584. if (nextSelName) {
  1585. //清下个下拉菜单选中值和选项
  1586. event = new CustomEvent("cascader-cleanOpt-" + nextSelName, {
  1587. detail: {},
  1588. });
  1589. window.dispatchEvent(event);
  1590. }
  1591. showPopup();
  1592. });
  1593. //设置mode2的下级下拉菜单的上级菜单当前值
  1594. function setNextSelectUpperValue() {
  1595. //设置下级菜单的上级菜单当前值upperValue
  1596. let paramArr = undefined;
  1597. if (Array.isArray(props.param)) {
  1598. paramArr = props.param;
  1599. } else {
  1600. paramArr = [];
  1601. paramArr.push(props.param);
  1602. }
  1603. for (const oneParam of paramArr) {
  1604. //下个下拉菜单名
  1605. const nextSelName = getNextSel(props.name, oneParam.fieldOrd);
  1606. if (nextSelName) {
  1607. setTimeout(() => {
  1608. const openNextEvent = new CustomEvent(
  1609. "cascade-setUpperVal-" + nextSelName,
  1610. {
  1611. detail: {
  1612. upperVal: props.modelValue,
  1613. },
  1614. }
  1615. );
  1616. window.dispatchEvent(openNextEvent);
  1617. }, 100);
  1618. }
  1619. } // end for
  1620. }
  1621. // 把上级 级联下拉菜单的值,设置进本组件的事件
  1622. window.addEventListener("cascade-setUpperVal-" + props.name, (event) => {
  1623. // alert('props.name:'+props.name+',props.upperValue:'+event.detail.upperVal);
  1624. const { upperVal } = event.detail;
  1625. upperValue.value = upperVal;
  1626. // console.log('props.name:'+props.name+',props.upperValue:'+upperValue.value);
  1627. });
  1628. //清空下拉菜单,并设置第一项的值为空
  1629. function clearAndInit1stOpt() {
  1630. if (props.opt)
  1631. props.opt.length = 0; //通过修改数组的length属性,直接清空数组元素,内存会被自动释放。这是性能最优的方式
  1632. else {
  1633. props.opt = [];
  1634. }
  1635. props.opt.push({ label: "", value: "" });
  1636. }
  1637. //获取下一级下拉菜单,如果下一级下拉菜单不存在,则返回undefined
  1638. function getNextSel(selName, selNameArr) {
  1639. // 检查参数有效性
  1640. if (!Array.isArray(selNameArr) || selNameArr.length === 0) {
  1641. return undefined;
  1642. }
  1643. // 查找当前元素的索引
  1644. const currentIndex = selNameArr.indexOf(selName);
  1645. // 如果元素不存在或已经是最后一个元素,返回undefined
  1646. if (currentIndex === -1 || currentIndex === selNameArr.length - 1) {
  1647. return undefined;
  1648. }
  1649. // 返回下一个元素
  1650. return selNameArr[currentIndex + 1];
  1651. }
  1652. const validate = () => {
  1653. if (window.ssVm) {
  1654. return window.ssVm.validateField(props.name);
  1655. }
  1656. return { valid: true };
  1657. };
  1658. // 处理选择事件
  1659. const doSelectItem = (item) => {
  1660. selectItem.value = item;
  1661. emit("update:modelValue", item.value); //修改本下拉菜单在vue中保存的值
  1662. // alert('item.value:'+item.value);
  1663. if (props.mode === "1") {
  1664. // mode 1 模式:修改隐藏字段值
  1665. let event = new CustomEvent(
  1666. "cascader-setHiddenVal-" + props.param.combField,
  1667. {
  1668. detail: {
  1669. value: item.value,
  1670. },
  1671. }
  1672. );
  1673. window.dispatchEvent(event);
  1674. }
  1675. emit("change", item.value); //触发配置的change方法
  1676. nextTick(() => {
  1677. validate();
  1678. if (window.ssVm) {
  1679. for (const [field, rules] of window.ssVm.validations.entries()) {
  1680. for (const rule of rules) {
  1681. if (rule.options?.relField) {
  1682. const relFields = String(rule.options.relField)
  1683. .split(",")
  1684. .map((name) => name.trim())
  1685. .filter(Boolean);
  1686. if (relFields.includes(props.name)) {
  1687. window.ssVm.validateField(field);
  1688. }
  1689. }
  1690. }
  1691. }
  1692. }
  1693. });
  1694. //设置下级菜单的上级菜单当前值upperValue
  1695. let paramArr = undefined;
  1696. if (Array.isArray(props.param)) {
  1697. paramArr = props.param;
  1698. } else {
  1699. paramArr = [];
  1700. paramArr.push(props.param);
  1701. }
  1702. for (const oneParam of paramArr) {
  1703. //下个下拉菜单名
  1704. const nextSelName = getNextSel(props.name, oneParam.fieldOrd);
  1705. if (nextSelName) {
  1706. setTimeout(() => {
  1707. const openNextEvent = new CustomEvent(
  1708. "cascader-open-" + nextSelName,
  1709. {
  1710. detail: {
  1711. upperVal: item.value,
  1712. },
  1713. }
  1714. );
  1715. window.dispatchEvent(openNextEvent);
  1716. }, 100);
  1717. }
  1718. } // end for
  1719. hidePopup();
  1720. //下个下拉菜单名
  1721. // let nextSelName = getNextSel(props.name, props.param.fieldOrd);
  1722. // if(nextSelName){
  1723. // // //设置下一级下拉菜单中保存的本下拉菜单值(upperValue)
  1724. // // event = new CustomEvent('cascade-setUpperVal-'+nextSelName, {
  1725. // // detail: {
  1726. // // value: item.value
  1727. // // }
  1728. // // });
  1729. // // window.dispatchEvent(event);
  1730. //
  1731. // //触发下一级下拉菜单,重新初始化下拉菜单项并弹出显示
  1732. // event = new CustomEvent('cascader-open-' +nextSelName, {
  1733. // detail: {
  1734. // upperVal: item.value
  1735. // }
  1736. // });
  1737. // window.dispatchEvent(event);
  1738. // }
  1739. // 只在手动选择时自动展开下一级
  1740. // if (!isAutoEcho.value) {
  1741. // const nextLevel = props.level + 1;
  1742. // setTimeout(() => {
  1743. // const openNextEvent = new CustomEvent('open-next-cascader', {
  1744. // detail: {
  1745. // name: props.name,
  1746. // level: nextLevel
  1747. // }
  1748. // });
  1749. // window.dispatchEvent(openNextEvent);
  1750. // }, 100);
  1751. // }
  1752. };
  1753. // 监听下一级展开事件 (仅 mode 2)
  1754. window.addEventListener("cascade-open", (event) => {
  1755. if (props.mode === "2") {
  1756. const { level } = event.detail;
  1757. if (level === props.level) {
  1758. popupWinVisible.value = true;
  1759. }
  1760. }
  1761. });
  1762. if (props.mode === "1") {
  1763. //如果是有隐藏字段的下拉菜单
  1764. // 监听回显事件
  1765. window.addEventListener(
  1766. "cascader-echo-" + props.name,
  1767. async (event) => {
  1768. const { name, value, isAuto, upperVal } = event.detail;
  1769. // level,
  1770. if (upperVal) {
  1771. upperValue.value = upperVal;
  1772. console.log(
  1773. "value:" +
  1774. value +
  1775. ",upperValue:" +
  1776. upperValue +
  1777. ",初始化级联组件时props.name:" +
  1778. props.name
  1779. );
  1780. }
  1781. // if (name === props.name && level === props.level) {
  1782. // 设置自动回显标记
  1783. isAutoEcho.value = true;
  1784. // if (props.opt.length === 0) {
  1785. // const loadDataEvent = new CustomEvent('cascader-load-data', {
  1786. // detail: {
  1787. // name: props.name,
  1788. // level: props.level,
  1789. // value: value
  1790. // }
  1791. // });
  1792. // window.dispatchEvent(loadDataEvent);
  1793. //下面的代码只用于页面刚打开时,初始化级联菜单的回显值。
  1794. //Vue.watch用于监听数据的变化,并在数据变化时执行特定的回调函数。
  1795. //这段代码使用了 Vue.js 的 watch API 来监听 props.opt 的变化,如果props.opt有变化,则自动
  1796. // const unwatch = Vue.watch(
  1797. // () => props.opt, // 监听的数据源(props.opt)
  1798. // (newOptions) => { // 回调函数
  1799. // if (newOptions.length > 0) { // 条件判断
  1800. // matchAndSelect(value); // 执行逻辑
  1801. // unwatch(); // 停止监听
  1802. // }
  1803. // },
  1804. // { immediate: true } // 配置:立即触发一次
  1805. // );
  1806. // } else {
  1807. // matchAndSelect(value);
  1808. // }
  1809. // 初始化级联菜单在页面刚打开时的默认值
  1810. async function initDefaultValue(value) {
  1811. try {
  1812. // alert(1);
  1813. if (
  1814. props.url &&
  1815. props.param
  1816. // && props.modelValue 对于有rule编码规则的级联菜单(即mode=1),modelValue一定是空的,所以注释掉,修复mode=1的级联菜单无法回显问题。Ben(20251124)
  1817. ) {
  1818. // alert(2);
  1819. /**
  1820. * let objectPickerParam=
  1821. * {"objectpickerparam":"{\"input\":\"false\",\"cascadingLevel\":\"hksheng,hkshi,hkxian\"," +
  1822. * "\"name\":\"hksheng\"," +
  1823. * "\"cascadingName\":\"dq\",\"cascadingInputsName\":\"hkdqm\"," +
  1824. * "\"codebook\":\"sheng\"}",
  1825. * "objectpickertype":2,
  1826. * "objectpickervalue":"440000"
  1827. * };
  1828. */
  1829. const objectPickerParam = {
  1830. input: "false",
  1831. cascadingLevel: props.param.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian"
  1832. name: props.name, //本下拉菜单名
  1833. cascadingName: props.param.name, //级联菜单名
  1834. cascadingInputsName: props.param.combField, //对象属性,即隐藏字段名,如:hkdqm
  1835. codebook: props.param.codebook,
  1836. };
  1837. const objectPickerParamStr =
  1838. JSON.stringify(objectPickerParam);
  1839. const params = new URLSearchParams();
  1840. params.append("objectpickerparam", objectPickerParamStr);
  1841. params.append("objectpickertype", "2");
  1842. params.append("objectpickervalue", value); //需回显的值
  1843. axios
  1844. .post(props.url, params, {
  1845. headers: {
  1846. "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
  1847. },
  1848. })
  1849. .then((response) => {
  1850. // alert(JSON.stringify(response.data));
  1851. if ("timeout" == response.data.statusText) {
  1852. alert("网络超时!");
  1853. return;
  1854. }
  1855. if (response.data.result) {
  1856. const keys = Object.keys(response.data.result);
  1857. if (keys.length === 1) {
  1858. let code = keys[0];
  1859. let desc = response.data.result[keys[0]];
  1860. clearAndInit1stOpt();
  1861. props.opt.push({ label: desc, value: code });
  1862. if (value) matchAndSelect(value);
  1863. // updateSelectItem();
  1864. // alert('props.opt:'+JSON.stringify(props.opt));
  1865. }
  1866. }
  1867. });
  1868. }
  1869. } catch (error) {
  1870. alert(error);
  1871. // callback(null, error.message); // 失败回调,传递错误
  1872. }
  1873. }
  1874. //下面的代码只用于页面刚打开时,初始化级联菜单的回显值。
  1875. initDefaultValue(value);
  1876. // 延迟重置自动回显标记
  1877. setTimeout(() => {
  1878. isAutoEcho.value = false;
  1879. }, 500);
  1880. }
  1881. );
  1882. // 被上级下拉菜单触发的,清除选中值和下拉菜单选项
  1883. window.addEventListener(
  1884. "cascader-cleanOpt-" + props.name,
  1885. async (event) => {
  1886. upperValue.value = "";
  1887. selectItem.value = ""; //清除本下拉菜单当前选中的值
  1888. emit("update:modelValue", ""); //通知父级
  1889. //清空所有下拉菜单项
  1890. if (props.opt) {
  1891. props.opt.length = 0;
  1892. } else {
  1893. props.opt = [];
  1894. }
  1895. //下个下拉菜单名
  1896. let nextSelName = getNextSel(props.name, props.param.fieldOrd);
  1897. // alert('nextSelName:'+nextSelName+'--,props.name:'+props.name);
  1898. if (nextSelName) {
  1899. //清下个下拉菜单选中值和选项
  1900. event = new CustomEvent("cascader-cleanOpt-" + nextSelName, {
  1901. detail: {},
  1902. });
  1903. window.dispatchEvent(event);
  1904. }
  1905. }
  1906. );
  1907. } else if (props.mode === "2") {
  1908. //没隐藏字段的下拉菜单,在这初始化默认值
  1909. let needInitParam = undefined;
  1910. if (Array.isArray(props.param)) {
  1911. needInitParam = props.param[props.param.length - 1]; //只初始化数组最后一项
  1912. console.log("needInitParam最后一项:" + JSON.stringify(needInitParam));
  1913. } else {
  1914. needInitParam = props.param;
  1915. }
  1916. // 初始化级联菜单在页面刚打开时的默认值
  1917. async function initDefaultValue(value, param) {
  1918. try {
  1919. // alert(1);
  1920. if (props.url && param && props.modelValue) {
  1921. // alert(2);
  1922. /**
  1923. * let param=
  1924. * {"objectpickerparam":"{\"input\":\"false\",\"cascadingLevel\":\"rylbm,gwid\"," +
  1925. * "\"name\":\"gwid\",\"cascadingName\":\"rylb_gw\"," +
  1926. * "\"codebook\":\"gwByRylb\"}",
  1927. * "objectpickertype":2,
  1928. * "objectpickervalue":"102121"};
  1929. */
  1930. // alert('props.name:'+props.name+',props.param.fieldOrd:'+props.param.fieldOrd);
  1931. const objectPickerParam = {
  1932. input: "false",
  1933. cascadingLevel: param.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian"
  1934. name: props.name, //本下拉菜单名
  1935. cascadingName: param.name, //级联菜单名
  1936. codebook: param.codebook,
  1937. };
  1938. const objectPickerParamStr = JSON.stringify(objectPickerParam);
  1939. const sendParams = new URLSearchParams();
  1940. sendParams.append("objectpickerparam", objectPickerParamStr);
  1941. sendParams.append("objectpickertype", "2");
  1942. sendParams.append("objectpickervalue", value); //需回显的值
  1943. axios
  1944. .post(props.url, sendParams, {
  1945. headers: {
  1946. "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
  1947. },
  1948. })
  1949. .then((response) => {
  1950. // alert(JSON.stringify(response.data));
  1951. if ("timeout" == response.data.statusText) {
  1952. alert("网络超时!");
  1953. return;
  1954. }
  1955. if (response.data.result) {
  1956. const keys = Object.keys(response.data.result);
  1957. console.log(
  1958. "name:" +
  1959. props.name +
  1960. ",@@级联初始化默认值value:" +
  1961. value +
  1962. "--param:" +
  1963. JSON.stringify(param) +
  1964. "--objectPickerParamStr:" +
  1965. objectPickerParamStr +
  1966. "--response.data:" +
  1967. JSON.stringify(response.data)
  1968. );
  1969. if (keys.length === 1) {
  1970. let code = keys[0];
  1971. let desc = response.data.result[keys[0]];
  1972. if (props.opt)
  1973. props.opt.length = 0; //通过修改数组的length属性,直接清空数组元素,内存会被自动释放。这是性能最优的方式
  1974. else {
  1975. props.opt = [];
  1976. }
  1977. props.opt.push({ label: desc, value: code });
  1978. if (value) matchAndSelect(value);
  1979. console.log(
  1980. "GOOD mode2回显的默认值:" +
  1981. JSON.stringify({ label: desc, value: code }) +
  1982. "--props.param:" +
  1983. JSON.stringify(param)
  1984. );
  1985. // updateSelectItem();
  1986. // alert('props.opt:'+JSON.stringify(props.opt));
  1987. }
  1988. }
  1989. });
  1990. }
  1991. } catch (error) {
  1992. alert(error);
  1993. // callback(null, error.message); // 失败回调,传递错误
  1994. }
  1995. // 重置自动回显标记
  1996. isAutoEcho.value = false;
  1997. }
  1998. // 初始化级联菜单在页面刚打开时的默认值
  1999. initDefaultValue(props.modelValue, needInitParam);
  2000. //设置mode2的下级下拉菜单的上级菜单当前值
  2001. setNextSelectUpperValue();
  2002. }
  2003. //选中要回显的默认值
  2004. const matchAndSelect = (value) => {
  2005. const matchedOption = props.opt.find((opt) => opt.value === value);
  2006. if (matchedOption) {
  2007. selectItem.value = matchedOption;
  2008. emit("update:modelValue", value);
  2009. emit("change", value);
  2010. }
  2011. };
  2012. // 计算弹出方向和最大高度的方法 by xu 20251212
  2013. // 当空间不足时限制popup高度并显示滚动条
  2014. const calculatePopupDirection = () => {
  2015. // 1. 获取select容器元素
  2016. const selectEl = document.querySelector(
  2017. `[name="${props.name}"]`
  2018. )?.nextElementSibling;
  2019. console.log("selectEl:" + selectEl, props.name);
  2020. if (!selectEl) return;
  2021. // 2. 获取位置信息
  2022. const selectRect = selectEl.getBoundingClientRect();
  2023. const viewportHeight = window.innerHeight;
  2024. // 3. 计算上下可用空间 by xu 20251212
  2025. const spaceBelow = viewportHeight - selectRect.bottom - 10; // 减10px留边距
  2026. const spaceAbove = selectRect.top - 10; // 减10px留边距
  2027. // 4. popup预估高度(假设每项36px,最多显示8项 + padding)
  2028. const estimatedPopupHeight = 300;
  2029. const minPopupHeight = 100; // 最小高度
  2030. console.log(
  2031. "[popup] 空间计算 - spaceAbove:",
  2032. spaceAbove,
  2033. "spaceBelow:",
  2034. spaceBelow,
  2035. "estimatedHeight:",
  2036. estimatedPopupHeight
  2037. );
  2038. // 5. 判断方向和最大高度 by xu 20251212
  2039. if (spaceBelow >= estimatedPopupHeight) {
  2040. // 下方空间足够,向下展开,不限制高度
  2041. popupDirection.value = "bottom";
  2042. popupMaxHeight.value = "none";
  2043. console.log("[popup] 向下展开,空间充足");
  2044. } else if (spaceAbove >= estimatedPopupHeight) {
  2045. // 上方空间足够,向上展开,不限制高度
  2046. popupDirection.value = "top";
  2047. popupMaxHeight.value = "none";
  2048. console.log("[popup] 向上展开,空间充足");
  2049. } else {
  2050. // 上下空间都不足,选择空间大的方向,并限制高度出滚动条
  2051. if (spaceBelow >= spaceAbove) {
  2052. popupDirection.value = "bottom";
  2053. popupMaxHeight.value = Math.max(spaceBelow, minPopupHeight) + "px";
  2054. console.log(
  2055. "[popup] 向下展开,空间不足,限制高度:",
  2056. popupMaxHeight.value
  2057. );
  2058. } else {
  2059. popupDirection.value = "top";
  2060. popupMaxHeight.value = Math.max(spaceAbove, minPopupHeight) + "px";
  2061. console.log(
  2062. "[popup] 向上展开,空间不足,限制高度:",
  2063. popupMaxHeight.value
  2064. );
  2065. }
  2066. }
  2067. };
  2068. //级联菜单点击事件
  2069. const togglePopup = () => {
  2070. if (!popupWinVisible.value) {
  2071. //如果当前下拉菜单是隐藏的,先ajax重新加载下拉菜单项,再显示。
  2072. showPopup();
  2073. } else {
  2074. hidePopup();
  2075. }
  2076. };
  2077. //显示下拉菜单,在此之前先清除下拉菜单项
  2078. const showPopup = () => {
  2079. //清空下拉菜单,并设置第一项的值为空
  2080. clearAndInit1stOpt();
  2081. Vue.nextTick(() => {
  2082. calculatePopupDirection();
  2083. });
  2084. let url = props.url;
  2085. let filterObj = props.param.filter;
  2086. if (filterObj) {
  2087. for (let k in filterObj) {
  2088. let v = filterObj[k];
  2089. url += "&" + k + "=" + v;
  2090. }
  2091. }
  2092. if (props.mode === "1") {
  2093. //如果是有隐藏字段的下拉菜单
  2094. console.log("666url:" + url);
  2095. // alert('url:'+url);
  2096. // 获取级联菜单所有下拉菜单项
  2097. async function getSelectItems(value) {
  2098. try {
  2099. // alert(1);
  2100. if (props.url && props.param) {
  2101. // alert(2);
  2102. /**
  2103. * param={"objectpickerparam":"{\"input\":\"false\",\"cascadingLevel\":\"hksheng,hkshi,hkxian\"," +
  2104. * "\"name\":\"hksheng\",\"cascadingName\":\"dq\"," +
  2105. * "\"cascadingInputsName\":\"hkdqm\",\"codebook\":\"sheng\"}",
  2106. * "objectpickertype":1,//2表示获取要回显的一项,1表示获取所有下拉菜单项
  2107. * "upperValue":"440000"
  2108. * };
  2109. */
  2110. const objectPickerParam = {
  2111. input: "false",
  2112. cascadingLevel: props.param.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian"
  2113. name: props.name, //本下拉菜单名
  2114. cascadingName: props.param.name, //级联菜单名
  2115. cascadingInputsName: props.param.combField, //对象属性,即隐藏字段名,如:hkdqm
  2116. codebook: props.param.codebook,
  2117. };
  2118. console.log("mode1 upperValue.value:" + upperValue.value);
  2119. const objectPickerParamStr = JSON.stringify(objectPickerParam);
  2120. const params = new URLSearchParams();
  2121. params.append("objectpickerparam", objectPickerParamStr);
  2122. params.append("objectpickertype", "1");
  2123. if (upperValue.value) {
  2124. params.append("upperValue", upperValue.value);
  2125. }
  2126. // params.append('objectpickervalue', value); //需回显的值
  2127. axios
  2128. .post(url, params, {
  2129. headers: {
  2130. "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
  2131. },
  2132. })
  2133. .then((response) => {
  2134. if ("timeout" == response.data.statusText) {
  2135. alert("网络超时!");
  2136. return;
  2137. }
  2138. if (response.data.result) {
  2139. const keys = Object.keys(response.data.result);
  2140. console.log(
  2141. "params:" +
  2142. params +
  2143. "@@response.data:" +
  2144. JSON.stringify(response.data)
  2145. );
  2146. if (keys.length > 0) {
  2147. for (let k in response.data.result) {
  2148. props.opt.push({
  2149. label: response.data.result[k],
  2150. value: k,
  2151. });
  2152. }
  2153. console.log("props.opt11:" + JSON.stringify(props.opt));
  2154. } else {
  2155. // 没有数据时打印日志 by xu 20251212
  2156. console.log("[ccp mode1] 接口返回空数据");
  2157. }
  2158. } else {
  2159. // result不存在时打印日志 by xu 20251212
  2160. console.log("[ccp mode1] 接口返回无result");
  2161. }
  2162. // 无论是否有数据,都显示popup by xu 20251212
  2163. if (!popupWinVisible.value) {
  2164. popupWinVisible.value = true;
  2165. }
  2166. });
  2167. }
  2168. } catch (error) {
  2169. alert(error);
  2170. // callback(null, error.message); // 失败回调,传递错误
  2171. }
  2172. }
  2173. getSelectItems(props.modelValue);
  2174. } else if (props.mode === "2") {
  2175. //没隐藏字段的下拉菜单
  2176. let needInitParam = undefined;
  2177. if (Array.isArray(props.param)) {
  2178. needInitParam = props.param[props.param.length - 1]; //只初始化数组最后一项
  2179. console.log(
  2180. "needInitParam最后一项:" + JSON.stringify(needInitParam)
  2181. );
  2182. } else {
  2183. needInitParam = props.param;
  2184. }
  2185. // 获取级联菜单所有下拉菜单项
  2186. async function getSelectItems(value, sendParam) {
  2187. try {
  2188. // alert(1);
  2189. if (props.url && sendParam) {
  2190. // alert(2);
  2191. /**
  2192. * param="{\"input\":\"false\",\"cascadingLevel\":\"dwid,sjryid\",
  2193. * \"ryid\":\"111121\",\"name\":\"sjryid\",
  2194. * \"cascadingName\":\"dw_sjry\",\"codebook\":\"sjryByDw\"}"
  2195. */
  2196. const objectPickerParam = {
  2197. input: "false",
  2198. cascadingLevel: sendParam.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian"
  2199. name: props.name, //本下拉菜单名
  2200. cascadingName: sendParam.name, //级联菜单名
  2201. codebook: sendParam.codebook,
  2202. };
  2203. console.log("mode2 upperValue.value:" + upperValue.value);
  2204. const objectPickerParamStr = JSON.stringify(objectPickerParam);
  2205. const params = new URLSearchParams();
  2206. params.append("objectpickerparam", objectPickerParamStr);
  2207. params.append("objectpickertype", "1");
  2208. if (upperValue.value) {
  2209. params.append("upperValue", upperValue.value);
  2210. }
  2211. // params.append('objectpickervalue', value); //需回显的值
  2212. axios
  2213. .post(url, params, {
  2214. headers: {
  2215. "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
  2216. },
  2217. })
  2218. .then((response) => {
  2219. if ("timeout" == response.data.statusText) {
  2220. alert("网络超时!");
  2221. return;
  2222. }
  2223. if (response.data.result) {
  2224. const keys = Object.keys(response.data.result);
  2225. console.log(
  2226. "params:" +
  2227. params +
  2228. "@@response.data:" +
  2229. JSON.stringify(response.data)
  2230. );
  2231. if (keys.length > 0) {
  2232. for (let k in response.data.result) {
  2233. props.opt.push({
  2234. label: response.data.result[k],
  2235. value: k,
  2236. });
  2237. }
  2238. console.log("props.opt11:" + JSON.stringify(props.opt));
  2239. } else {
  2240. // 没有数据时打印日志 by xu 20251212
  2241. console.log("[ccp mode2] 接口返回空数据");
  2242. }
  2243. } else {
  2244. // result不存在时打印日志 by xu 20251212
  2245. console.log("[ccp mode2] 接口返回无result");
  2246. }
  2247. // 无论是否有数据,都显示popup by xu 20251212
  2248. if (!popupWinVisible.value) {
  2249. popupWinVisible.value = true;
  2250. }
  2251. });
  2252. }
  2253. } catch (error) {
  2254. alert(error);
  2255. // callback(null, error.message); // 失败回调,传递错误
  2256. }
  2257. }
  2258. getSelectItems(props.modelValue, needInitParam);
  2259. // popupWinVisible.value = !popupWinVisible.value;
  2260. }
  2261. };
  2262. const hidePopup = () => {
  2263. popupWinVisible.value = false;
  2264. };
  2265. // 合并所有的 onMounted 逻辑
  2266. Vue.onMounted(() => {
  2267. window.addEventListener("resize", calculatePopupDirection);
  2268. // 1. 监听展开下一级事件
  2269. window.addEventListener("open-next-cascader", (event) => {
  2270. const { name, level } = event.detail;
  2271. if (name === props.name && level === props.level) {
  2272. popupWinVisible.value = true;
  2273. }
  2274. });
  2275. // 2. 监听级联事件
  2276. window.addEventListener("cascader-change", (event) => {
  2277. const { name, level, value } = event.detail;
  2278. if (name === props.name && level < props.level) {
  2279. selectItem.value = { label: "", value: "" };
  2280. emit("update:modelValue", "");
  2281. if (ssHidden) {
  2282. ssHidden.updateValue(value);
  2283. }
  2284. }
  2285. });
  2286. });
  2287. Vue.onUnmounted(() => {
  2288. window.removeEventListener("resize", calculatePopupDirection);
  2289. });
  2290. // 监听值变化,处理回显 (mode 2)
  2291. Vue.watch(
  2292. () => props.modelValue,
  2293. (newVal) => {
  2294. if (props.mode === "2" && newVal) {
  2295. // 使用 watchEffect 替代嵌套的 watch
  2296. Vue.watchEffect(() => {
  2297. if (props.opt.length > 0) {
  2298. const matchedOption = props.opt.find(
  2299. (opt) => opt.value === newVal
  2300. );
  2301. if (matchedOption) {
  2302. selectItem.value = matchedOption;
  2303. }
  2304. }
  2305. });
  2306. } else {
  2307. // 原有的值变化处理
  2308. const item = props.opt.find((it) => it.value === newVal);
  2309. if (item) {
  2310. selectItem.value = item;
  2311. } else {
  2312. selectItem.value = { label: "", value: "" };
  2313. }
  2314. }
  2315. },
  2316. { immediate: true }
  2317. );
  2318. // 监听选项变化,当数据加载完成时进行匹配
  2319. Vue.watch(
  2320. () => props.opt,
  2321. (newOptions) => {
  2322. if (newOptions.length > 0) {
  2323. const matchedOption = newOptions.find(
  2324. (opt) => opt.value === selectItem.value.value
  2325. );
  2326. if (matchedOption) {
  2327. selectItem.value = matchedOption;
  2328. emit("update:modelValue", matchedOption.value);
  2329. emit("change", matchedOption.value);
  2330. }
  2331. }
  2332. }
  2333. );
  2334. return {
  2335. selectItem,
  2336. popupWinVisible,
  2337. popupDirection,
  2338. popupMaxHeight, // 添加popup最大高度 by xu 20251212
  2339. togglePopup,
  2340. hidePopup,
  2341. doSelectItem,
  2342. };
  2343. },
  2344. template: `
  2345. <div class="input ss-ccp-container" style="position: relative" :style="{width: width}">
  2346. <input type="hidden" :name="name" :value="modelValue">
  2347. <div class="select-container" @mouseleave="hidePopup">
  2348. <div class="input" @click="togglePopup">
  2349. <input
  2350. type="hidden"
  2351. :name="name"
  2352. :value="selectItem.value"
  2353. />
  2354. <input
  2355. :placeholder="placeholder"
  2356. :value="selectItem.label"
  2357. disabled
  2358. style="pointer-events: none;"
  2359. />
  2360. <div class="suffix">
  2361. <ss-form-icon :class="popupWinVisible ? 'form-icon-transform-select select' : 'form-icon-select'" />
  2362. </div>
  2363. </div>
  2364. <!-- popup弹出层,添加maxHeight和overflowY支持空间不足时滚动 by xu 20251212 -->
  2365. <div v-show="popupWinVisible" class="popup-win " :class="popupDirection" :style="{ maxHeight: popupMaxHeight, overflowY: popupMaxHeight !== 'none' ? 'auto' : 'visible' }">
  2366. <div v-if="opt && opt.length > 0" class="popup-content">
  2367. <div class="content-area">
  2368. <div
  2369. v-for="(item, index) in opt"
  2370. :key="index"
  2371. @click="doSelectItem(item)"
  2372. :class="{ active: item.value === selectItem.value }"
  2373. >
  2374. <span class="check-icon">
  2375. <ss-form-icon class="form-icon-select-checked" />
  2376. </span>
  2377. <span>{{ item.label }}</span>
  2378. </div>
  2379. </div>
  2380. </div>
  2381. <div v-else class="popup-content">
  2382. <div class="content-area">
  2383. <div class="content-area">
  2384. <span>无选项</span>
  2385. </div>
  2386. </div>
  2387. </div>
  2388. </div>
  2389. </div>
  2390. </div>
  2391. `,
  2392. };
  2393. // ss-date-picker 日期时间选择器组件
  2394. const SsDatePicker = {
  2395. name: "SsDatePicker",
  2396. props: {
  2397. modelValue: {
  2398. type: [String, Number, Date],
  2399. default: "",
  2400. },
  2401. name: {
  2402. type: String,
  2403. required: true,
  2404. },
  2405. type: {
  2406. type: String,
  2407. default: "date",
  2408. validator: (value) => ["date", "datetime", "time"].includes(value),
  2409. },
  2410. fmt: {
  2411. type: String,
  2412. default: null,
  2413. },
  2414. placeholder: {
  2415. type: String,
  2416. default: "",
  2417. },
  2418. width: {
  2419. type: String,
  2420. default: "100%",
  2421. },
  2422. },
  2423. emits: ["update:modelValue"],
  2424. setup(props, { emit }) {
  2425. const errMsg = ref("");
  2426. const validate = () => {
  2427. if (window.ssVm) {
  2428. const result = window.ssVm.validateField(props.name);
  2429. console.log("validate", window.ssVm.validateField(props.name));
  2430. errMsg.value = result.valid ? "" : result.message;
  2431. }
  2432. };
  2433. // 根据type确定默认格式
  2434. const defaultFormat = computed(() => {
  2435. switch (props.type) {
  2436. case "datetime":
  2437. return "YYYY-MM-DD HH:mm:ss";
  2438. case "date":
  2439. return "YYYY-MM-DD";
  2440. case "time":
  2441. return "HH:mm:ss";
  2442. }
  2443. });
  2444. const convertJavaFormatToElement = (javaFormat) => {
  2445. if (!javaFormat) return null;
  2446. return javaFormat
  2447. .replace("yyyy", "YYYY")
  2448. .replace("MM", "MM")
  2449. .replace("dd", "DD")
  2450. .replace("HH", "HH")
  2451. .replace("mm", "mm")
  2452. .replace("ss", "ss");
  2453. };
  2454. const finalFormat = computed(() => {
  2455. if (props.fmt) {
  2456. return convertJavaFormatToElement(props.fmt);
  2457. }
  2458. return defaultFormat.value;
  2459. });
  2460. // 使用 resolveComponent 获取组件
  2461. const ElDatePicker = resolveComponent("ElDatePicker");
  2462. const ElTimePicker = resolveComponent("ElTimePicker");
  2463. const SsFormIcon = resolveComponent("SsFormIcon");
  2464. const ElIcon = resolveComponent("ElIcon");
  2465. const handleValueUpdate = (val) => {
  2466. emit("update:modelValue", val);
  2467. emit("change", val); // 同时触发 change 事件
  2468. setTimeout(() => {
  2469. validate();
  2470. }, 50);
  2471. };
  2472. const dateType = computed(() => {
  2473. const fmt = props.fmt || "";
  2474. if (fmt.includes("HH:mm:ss")) {
  2475. return "datetime";
  2476. } else if (fmt.includes("HH:mm")) {
  2477. return "datetime";
  2478. } else if (fmt.includes("mm:ss")) {
  2479. return "time";
  2480. }
  2481. return "date";
  2482. });
  2483. let useTimePicker = true;
  2484. //"yyyy-MM-dd HH:mm:ss"; "日期字符串格式在java的写法",传到本组件fmt属性也是按这个格式
  2485. if (props.fmt) {
  2486. //有fmt属性,则以fmt属性优先判断类型
  2487. if (/[dMy]/.test(props.fmt)) {
  2488. //如果有传入日期格式,且含年月日
  2489. useTimePicker = false;
  2490. } else {
  2491. useTimePicker = true;
  2492. }
  2493. } else if (props.type !== "time") {
  2494. useTimePicker = false;
  2495. }
  2496. return () =>
  2497. h("div", { class: "ss-date-picker", style: { width: props.width } }, [
  2498. h("input", {
  2499. type: "hidden",
  2500. name: props.name,
  2501. value: props.modelValue,
  2502. }),
  2503. // 选择组件
  2504. h(useTimePicker ? ElTimePicker : ElDatePicker, {
  2505. modelValue: props.modelValue,
  2506. "onUpdate:modelValue": handleValueUpdate,
  2507. type: dateType.value,
  2508. format: finalFormat.value,
  2509. "value-format": finalFormat.value,
  2510. clearable: true,
  2511. placeholder: props.placeholder,
  2512. class: "custom-date-picker", // 用于自定义样式
  2513. "time-arrow-control": props.type === "datetime", // 修改这里
  2514. size: "large", // 添加这一行,改为 large 尺寸
  2515. style: { width: "100%" },
  2516. "prefix-icon": h(SsFormIcon, { class: "form-icon-time" }),
  2517. }),
  2518. ]);
  2519. },
  2520. };
  2521. // ss-icon 图标
  2522. // v3.0 增加 class 属性分支:有 class 走新逻辑,否则走 v1.0 逻辑 by xu 20251212
  2523. // v3.0 用法: <ss-icon class="icon-obj-ry menu-icon" />
  2524. // v1.0 用法: <ss-icon name="setting" size="20px" />
  2525. const SsIcon = {
  2526. name: "SsIcon",
  2527. // v3.0 禁用 class 透传,手动处理 by xu 20251215
  2528. inheritAttrs: false,
  2529. props: {
  2530. // v1.0: 以下为旧属性
  2531. name: { type: String },
  2532. size: { type: [Number, String], default: 16 },
  2533. unit: { type: String, default: "px" },
  2534. color: String,
  2535. type: {
  2536. type: String,
  2537. default: ssIcon.name,
  2538. validator: function (value) {
  2539. return [ssIcon, commonIcon].some((icon) => icon.name === value);
  2540. },
  2541. },
  2542. },
  2543. emits: ["update:modelValue", "input", "blur", "change"],
  2544. setup(props, { emit, attrs }) {
  2545. // v3.0 分支:有 class 属性时直接渲染(从 attrs 获取) by xu 20251215
  2546. if (attrs.class) {
  2547. return () =>
  2548. h("i", { ...attrs, class: attrs.class + " icon-container" });
  2549. }
  2550. // v1.0 分支:原有逻辑
  2551. const useIconType = computed(() => {
  2552. return [ssIcon, commonIcon].find(
  2553. (iconConfig) => iconConfig.name === props.type
  2554. );
  2555. });
  2556. const iconName = computed(() => {
  2557. const iconConfig = useIconType.value; // 注意:使用 .value 来访问响应式引用的值
  2558. if (!iconConfig) {
  2559. console.error(`Icon type "${props.type}" not found.`);
  2560. return "";
  2561. }
  2562. const iconType = iconConfig.types[props.name];
  2563. if (!iconType) {
  2564. console.error(
  2565. `Icon name "${props.name}" not found in type "${props.type}".`
  2566. );
  2567. return "";
  2568. }
  2569. return `${iconConfig.prefix}${iconType}`;
  2570. });
  2571. // 类似地,你可以计算 fontFamily 和 style
  2572. const fontFamily = computed(() => {
  2573. return useIconType.value ? useIconType.value.family : "";
  2574. });
  2575. // console.log(iconName.value,fontFamily.value)
  2576. const style = computed(() => {
  2577. const sizeStyle = isNum(props.size)
  2578. ? `${props.size}${props.unit}`
  2579. : props.size;
  2580. const styleObj = {
  2581. fontSize: sizeStyle,
  2582. color: props.color || "",
  2583. };
  2584. return toStyleStr(styleObj);
  2585. });
  2586. // 使用渲染函数定义模板逻辑
  2587. return () =>
  2588. h("i", {
  2589. class: ["icon-container", iconName.value, fontFamily.value],
  2590. style: style.value,
  2591. });
  2592. },
  2593. };
  2594. // 通用icon
  2595. const SsCommonIcon = {
  2596. name: "SsCommonIcon",
  2597. props: {
  2598. class: {
  2599. type: String,
  2600. required: true,
  2601. },
  2602. },
  2603. setup(props) {
  2604. return () =>
  2605. h("i", {
  2606. class: props.class + " common-icon",
  2607. });
  2608. },
  2609. };
  2610. // 登录页icon
  2611. const SsLoginIcon = {
  2612. name: "SsLoginIcon",
  2613. props: {
  2614. class: {
  2615. type: String,
  2616. required: true,
  2617. },
  2618. },
  2619. setup(props) {
  2620. return () =>
  2621. h("div", {
  2622. class: props.class + " login-icon",
  2623. });
  2624. },
  2625. };
  2626. // 弹窗icon
  2627. const SsDialogIcon = {
  2628. name: "SsDialogIcon",
  2629. props: {
  2630. class: {
  2631. type: String,
  2632. required: true,
  2633. },
  2634. },
  2635. setup(props) {
  2636. return () =>
  2637. h("i", {
  2638. class: props.class + " dialog-icon",
  2639. });
  2640. },
  2641. };
  2642. // 全局左侧导航图标组件
  2643. const SsNavIcon = {
  2644. name: "SsNavIcon",
  2645. props: {
  2646. class: {
  2647. type: String,
  2648. required: true,
  2649. },
  2650. },
  2651. setup(props) {
  2652. return () =>
  2653. h("div", {
  2654. class: props.class + " nav-icon",
  2655. });
  2656. },
  2657. };
  2658. // 顶部工具栏图标组件
  2659. const SsHeaderIcon = {
  2660. name: "SsHeaderIcon",
  2661. props: {
  2662. class: {
  2663. type: String,
  2664. required: true,
  2665. },
  2666. },
  2667. setup(props) {
  2668. return () =>
  2669. h("div", {
  2670. class: props.class + " header-icon",
  2671. });
  2672. },
  2673. };
  2674. // 全局菜单图标组件
  2675. const SsGolbalMenuIcon = {
  2676. name: "SsGolbalMenuIcon",
  2677. props: {
  2678. class: {
  2679. type: String,
  2680. required: true,
  2681. },
  2682. },
  2683. setup(props) {
  2684. return () =>
  2685. h("div", {
  2686. class: props.class + " global-menu-icon",
  2687. });
  2688. },
  2689. };
  2690. // 全局查询列表卡片图标
  2691. const SsCartListIcon = {
  2692. name: "SsCartListIcon",
  2693. props: {
  2694. class: {
  2695. type: String,
  2696. required: true,
  2697. },
  2698. },
  2699. setup(props) {
  2700. return () =>
  2701. h("div", {
  2702. class: props.class + " cart-list-icon",
  2703. });
  2704. },
  2705. };
  2706. // 全局底部工具栏图标组件
  2707. const SsQuickIcon = {
  2708. name: "SsQuickIcon",
  2709. props: {
  2710. class: {
  2711. type: String,
  2712. required: true,
  2713. },
  2714. },
  2715. setup(props) {
  2716. return () =>
  2717. h("div", {
  2718. class: props.class + " quick-icon",
  2719. });
  2720. },
  2721. };
  2722. // 表单组件icon
  2723. const SsFormIcon = {
  2724. name: "SsFormIcon",
  2725. props: {
  2726. class: {
  2727. type: String,
  2728. required: true,
  2729. },
  2730. },
  2731. setup(props) {
  2732. return () =>
  2733. h("div", {
  2734. class: props.class + " form-icon",
  2735. });
  2736. },
  2737. };
  2738. // 弹窗底部按钮icon
  2739. const SsBottomDivIcon = {
  2740. name: "SsBottomDivIcon",
  2741. props: {
  2742. class: {
  2743. type: String,
  2744. required: true,
  2745. },
  2746. },
  2747. setup(props) {
  2748. return () =>
  2749. h("div", {
  2750. class: props.class + " bottom-div-icon",
  2751. });
  2752. },
  2753. };
  2754. // editor组件icon
  2755. const SsEditorIcon = {
  2756. name: "SsEditorIcon",
  2757. props: {
  2758. class: {
  2759. type: String,
  2760. required: true,
  2761. },
  2762. },
  2763. setup(props) {
  2764. return () =>
  2765. h("i", {
  2766. class: props.class + " editor-icon",
  2767. });
  2768. },
  2769. };
  2770. // ss-validate校验器
  2771. const SsValidate = {
  2772. name: "SsValidate",
  2773. props: {
  2774. errMsg: { type: String },
  2775. textAlign: { type: String, default: "left" },
  2776. style: { type: Object, default: () => ({}) },
  2777. },
  2778. template: `<div class="validate-vline"></div>
  2779. <div class="validate-tip" :style="style">
  2780. <div class="tip" :style="{ textAlign: textAlign }">{{ errMsg }}</div>
  2781. <div class="tip-more" :style="{ textAlign: textAlign }">{{ errMsg }}</div>
  2782. </div>`,
  2783. };
  2784. // ss-onoff-array 多选按钮 数组形式
  2785. const SsonoffArray = {
  2786. name: "SsonoffArray",
  2787. props: {
  2788. name: {
  2789. type: String,
  2790. required: true,
  2791. },
  2792. opt: {
  2793. type: Array,
  2794. default: () => [],
  2795. },
  2796. defaultValue: [String, Number, Array],
  2797. modelValue: [String, Number, Array],
  2798. multiple: {
  2799. // 新增多选模式属性
  2800. type: Boolean,
  2801. default: false,
  2802. },
  2803. // 是否允许一项都不选,默认true允许 by xu 20251212
  2804. null: {
  2805. type: Boolean,
  2806. default: true,
  2807. },
  2808. },
  2809. emits: ["update:modelValue"], // 允许更新 v-model 绑定的值
  2810. setup(props, { emit }) {
  2811. console.log("多选按钮", props.opt);
  2812. // 使用数组来存储选中值
  2813. const checkedValue = ref(
  2814. props.multiple
  2815. ? Array.isArray(props.defaultValue)
  2816. ? props.defaultValue
  2817. : []
  2818. : props.defaultValue
  2819. );
  2820. const errMsg = ref(props.errTip);
  2821. // 生成icon名字
  2822. const genIconName = (itemValue) => {
  2823. if (props.multiple) {
  2824. return checkedValue.value.includes(itemValue)
  2825. ? "form-icon-onoff-checked"
  2826. : "form-icon-onoff-unchecked";
  2827. }
  2828. return checkedValue.value === itemValue
  2829. ? "form-icon-onoff-checked"
  2830. : "form-icon-onoff-unchecked";
  2831. };
  2832. // 选中项
  2833. const selectItem = (value) => {
  2834. if (props.multiple) {
  2835. // 多选模式
  2836. const index = checkedValue.value.indexOf(value);
  2837. if (index === -1) {
  2838. checkedValue.value = [...checkedValue.value, value];
  2839. } else {
  2840. // 取消选中当前项
  2841. const newValue = checkedValue.value.filter((v) => v !== value);
  2842. // 如果不允许为空且取消后为空,则阻止取消操作 by xu 20251212
  2843. if (!props.null && newValue.length === 0) {
  2844. return; // 阻止取消最后一项
  2845. }
  2846. checkedValue.value = newValue;
  2847. }
  2848. } else {
  2849. // 单选模式
  2850. // 如果点击的是当前已选中的项,判断是否允许取消 by xu 20251212
  2851. if (checkedValue.value === value) {
  2852. if (!props.null) {
  2853. return; // 不允许为空时,阻止取消
  2854. }
  2855. checkedValue.value = ""; // 允许为空时,取消选中
  2856. } else {
  2857. checkedValue.value = value;
  2858. }
  2859. }
  2860. emit("update:modelValue", checkedValue.value);
  2861. nextTick(() => {
  2862. // 触发验证
  2863. if (window.ssVm) {
  2864. window.ssVm.validateField(props.name);
  2865. }
  2866. });
  2867. };
  2868. return { checkedValue, genIconName, selectItem };
  2869. },
  2870. // 使用渲染函数定义模板逻辑
  2871. render() {
  2872. const SsFormIcon = resolveComponent("ss-form-icon");
  2873. return h("div", { class: "radio-container" }, [
  2874. // 根据情况创建 input
  2875. this.multiple
  2876. ? this.checkedValue.length
  2877. ? // 多选且有选中值:为选中项创建 input
  2878. this.checkedValue.map((value) =>
  2879. h("input", {
  2880. type: "checkbox",
  2881. name: this.name,
  2882. value: value,
  2883. checked: true,
  2884. style: { display: "none" },
  2885. })
  2886. )
  2887. : // 多选但没有选中值:创建一个空值 input
  2888. h("input", {
  2889. type: "hidden",
  2890. name: this.name,
  2891. value: "",
  2892. })
  2893. : // 单选模式:创建一个 input
  2894. h("input", {
  2895. type: "hidden",
  2896. name: this.name,
  2897. value: this.checkedValue || "",
  2898. }),
  2899. this.opt.map((item, i) =>
  2900. h(
  2901. "div",
  2902. {
  2903. key: i,
  2904. class: {
  2905. checked: this.multiple
  2906. ? this.checkedValue.includes(item.value)
  2907. : this.checkedValue === item.value,
  2908. },
  2909. style: { width: item.width },
  2910. onClick: () => this.selectItem(item.value),
  2911. },
  2912. [
  2913. h("span", null, item.label),
  2914. h("div", { class: "mark" }, [
  2915. h(SsFormIcon, {
  2916. class: this.genIconName(item.value),
  2917. }),
  2918. ]),
  2919. ]
  2920. )
  2921. ),
  2922. ]);
  2923. },
  2924. };
  2925. // ss-onoff 一个按钮
  2926. const Ssonoff = {
  2927. name: "Ssonoff",
  2928. props: {
  2929. name: {
  2930. type: String,
  2931. required: true,
  2932. },
  2933. label: {
  2934. type: String,
  2935. required: true,
  2936. },
  2937. value: {
  2938. type: [String, Number],
  2939. required: true,
  2940. },
  2941. width: {
  2942. type: String,
  2943. default: "",
  2944. },
  2945. onchange: {
  2946. // 在此属性传入 onChange 的 window 全局回调函数,第一参数是当前整组值 by Ben/xu(20260320)
  2947. type: String,
  2948. required: false,
  2949. },
  2950. modelValue: [String, Number, Array],
  2951. multiple: {
  2952. type: Boolean,
  2953. default: false,
  2954. },
  2955. null: {
  2956. type: Boolean,
  2957. default: true,
  2958. },
  2959. },
  2960. emits: ["update:modelValue", "change"],
  2961. setup(props, { emit }) {
  2962. const parseModelValue = (val) => {
  2963. if (Array.isArray(val)) {
  2964. return val
  2965. .map((item) => (item == null ? "" : item.toString()))
  2966. .filter(Boolean);
  2967. }
  2968. if (val == null || val === "") return [];
  2969. // 如果以逗号开头,去掉开头的逗号
  2970. const cleanValue = val.toString().replace(/^,+/, "");
  2971. if (!cleanValue) return [];
  2972. if (cleanValue.includes("|")) {
  2973. return cleanValue.split("|").filter(Boolean);
  2974. }
  2975. if (cleanValue.includes(",")) {
  2976. return cleanValue.split(",").filter(Boolean);
  2977. }
  2978. return [cleanValue];
  2979. };
  2980. const callGlobalOnchg = (groupValue) => {
  2981. if (props.onchange && typeof props.onchange === "string") {
  2982. if (
  2983. typeof window !== "undefined" &&
  2984. window[props.onchange] &&
  2985. typeof window[props.onchange] === "function"
  2986. ) {
  2987. try {
  2988. window[props.onchange](groupValue, props.value, props.label);
  2989. } catch (error) {
  2990. console.error(`调用全局函数 ${props.onchange} 时出错:`, error);
  2991. }
  2992. } else {
  2993. console.warn(`全局函数 ${props.onchange} 未定义或不是一个函数。`);
  2994. }
  2995. }
  2996. };
  2997. // 判断当前按钮是否选中
  2998. const isChecked = computed(() => {
  2999. if (props.multiple) {
  3000. const currentValue = parseModelValue(props.modelValue);
  3001. return currentValue.includes(props.value.toString());
  3002. }
  3003. return props.modelValue + "" === props.value + ""; //强转为字符串类型再比较(改之前是数字类型和字符串类型作比较,永远为false) Ben 20251206
  3004. });
  3005. // 切换选中状态
  3006. const toggleSelect = () => {
  3007. let newModelValue;
  3008. if (props.multiple) {
  3009. const currentValue = parseModelValue(props.modelValue);
  3010. const currentButtonValue = props.value.toString();
  3011. const index = currentValue.indexOf(currentButtonValue);
  3012. let newValue;
  3013. if (index === -1) {
  3014. // 选中当前项
  3015. newValue = [...currentValue, currentButtonValue];
  3016. } else {
  3017. // 取消选中当前项
  3018. const filteredValue = currentValue.filter(
  3019. (value) => value !== currentButtonValue
  3020. );
  3021. // 如果不允许为空且取消后为空,则阻止取消操作
  3022. if (!props.null && filteredValue.length === 0) {
  3023. return; // 阻止取消最后一项
  3024. }
  3025. newValue = filteredValue;
  3026. }
  3027. newModelValue = newValue.join(",");
  3028. } else {
  3029. // 单选模式
  3030. const currentValue = parseModelValue(props.modelValue);
  3031. const isCurrentlySelected = currentValue.includes(
  3032. props.value.toString()
  3033. );
  3034. if (!isCurrentlySelected) {
  3035. // 选中当前项
  3036. newModelValue = props.value;
  3037. } else {
  3038. // 取消选中当前项
  3039. // 如果不允许为空且当前只有这一项被选中,则阻止取消操作
  3040. if (!props.null && currentValue.length === 1) {
  3041. return; // 阻止取消唯一选中项
  3042. }
  3043. newModelValue = "";
  3044. }
  3045. }
  3046. emit("update:modelValue", newModelValue);
  3047. emit("change", newModelValue, props.value, props.label);
  3048. callGlobalOnchg(newModelValue);
  3049. nextTick(() => {
  3050. // 触发验证
  3051. if (window.ssVm) {
  3052. window.ssVm.validateField(props.name);
  3053. }
  3054. });
  3055. };
  3056. return { isChecked, toggleSelect };
  3057. },
  3058. render() {
  3059. const SsFormIcon = resolveComponent("ss-form-icon");
  3060. return h("div", { class: "radio-container2" }, [
  3061. // 隐藏的表单元素
  3062. this.multiple
  3063. ? h("input", {
  3064. type: "hidden",
  3065. name: `${this.name}`, // 多选模式下使用数组形式的 name
  3066. value: this.isChecked ? this.value : "",
  3067. })
  3068. : this.isChecked &&
  3069. h("input", {
  3070. // 只有当前按钮被选中时才创建 input
  3071. type: "hidden",
  3072. name: this.name,
  3073. value: this.value,
  3074. }),
  3075. // 按钮显示
  3076. h(
  3077. "div",
  3078. {
  3079. class: { checked: this.isChecked },
  3080. style: { width: this.width },
  3081. onClick: this.toggleSelect,
  3082. },
  3083. [
  3084. h("span", null, this.label),
  3085. h("div", { class: "mark" }, [
  3086. h(SsFormIcon, {
  3087. class: this.isChecked
  3088. ? "form-icon-onoff-checked"
  3089. : "form-icon-onoff-unchecked",
  3090. }),
  3091. ]),
  3092. ]
  3093. ),
  3094. ]);
  3095. },
  3096. };
  3097. // ss-textarea
  3098. // ss-textarea
  3099. const SsTextarea = {
  3100. name: "SsTextarea",
  3101. props: {
  3102. name: {
  3103. type: String,
  3104. required: true,
  3105. },
  3106. placeholder: {
  3107. type: String,
  3108. default: "请输入",
  3109. },
  3110. defaultValue: [String, Number],
  3111. modelValue: [String, Number],
  3112. },
  3113. emits: ["update:modelValue"],
  3114. setup(props, { emit }) {
  3115. const inputValue = ref(props.modelValue || props.defaultValue || "");
  3116. // 监听 modelValue 变化
  3117. watch(
  3118. () => props.modelValue,
  3119. (newVal) => {
  3120. inputValue.value = newVal;
  3121. }
  3122. );
  3123. // 输入事件处理
  3124. const onInput = (event) => {
  3125. const newValue = event.target.value;
  3126. inputValue.value = newValue;
  3127. emit("update:modelValue", newValue);
  3128. // 触发验证
  3129. if (window.ssVm) {
  3130. window.ssVm.validateField(props.name);
  3131. }
  3132. };
  3133. // 失焦时验证
  3134. const onBlur = () => {
  3135. if (window.ssVm) {
  3136. window.ssVm.validateField(props.name);
  3137. }
  3138. };
  3139. return { inputValue, onInput, onBlur };
  3140. },
  3141. render() {
  3142. return h("div", { class: "textarea-container" }, [
  3143. h("div", { class: "textarea" }, [
  3144. h("input", {
  3145. type: "hidden",
  3146. name: this.name,
  3147. value: this.inputValue || "",
  3148. }),
  3149. h("textarea", {
  3150. placeholder: this.placeholder,
  3151. value: this.inputValue,
  3152. onInput: this.onInput,
  3153. onBlur: this.onBlur,
  3154. }),
  3155. ]),
  3156. ]);
  3157. },
  3158. };
  3159. // ss-editor 富文本编辑器 基于Jodit
  3160. const SsEditor = {
  3161. name: "SsEditor",
  3162. props: {
  3163. modelValue: {
  3164. type: String,
  3165. default: "",
  3166. },
  3167. html: {
  3168. type: String,
  3169. default: "",
  3170. },
  3171. name: {
  3172. type: String,
  3173. default: "",
  3174. },
  3175. url: {
  3176. type: String,
  3177. default: "",
  3178. },
  3179. height: {
  3180. type: [Number, String],
  3181. default: 400,
  3182. },
  3183. placeholder: {
  3184. type: String,
  3185. default: "请输入内容",
  3186. },
  3187. mode: {
  3188. type: String,
  3189. default: "edit", // 'edit' | 'play'
  3190. },
  3191. uploadUrl: {
  3192. type: String,
  3193. default: "/ulByHttp", //原值为“upload” Ben(20251205)
  3194. },
  3195. param: {
  3196. type: Object,
  3197. default: () => ({}),
  3198. },
  3199. customButtons: {
  3200. type: Array,
  3201. default: () => [],
  3202. },
  3203. },
  3204. emits: ["update:modelValue", "ready", "change"],
  3205. setup(props, { emit }) {
  3206. const editorRef = ref(null);
  3207. const editorContent = ref("");
  3208. const currentModelValue = ref(props.modelValue || "");
  3209. const uniqueId = "editor-" + Date.now();
  3210. const errMsg = Vue.ref("");
  3211. // button 支持对象(旧格式,单个附件按钮)或数组(新格式,多个按钮)by xu 20250331
  3212. const buttonConfigRaw = props.param && props.param.button ? props.param.button : null;
  3213. const buttonArray = Array.isArray(buttonConfigRaw)
  3214. ? buttonConfigRaw
  3215. : buttonConfigRaw
  3216. ? [buttonConfigRaw]
  3217. : [];
  3218. // 找到附件按钮配置(有 cmsAddUrl 的)
  3219. const fjButton = buttonArray.find(btn => btn.cmsAddUrl && btn.cmsUpdUrl) || {};
  3220. const fjid = ref(fjButton.val || null);
  3221. const fjName = fjButton.desc || "附件";
  3222. // 筛选出自定义按钮(有 func 的)
  3223. const customButtonsFromParam = buttonArray.filter(btn => btn.func).map(btn => ({
  3224. name: btn.name,
  3225. text: btn.desc || btn.text,
  3226. tooltip: btn.desc || btn.tooltip,
  3227. icon: btn.icon || 'editor-icon-file',
  3228. onClick: btn.func // 函数名字符串
  3229. }));
  3230. const mode = props.param ? props.param.mode : null;
  3231. const normalizeHtml = (rawHtml) => {
  3232. if (rawHtml == null) {
  3233. return "";
  3234. }
  3235. const html = String(rawHtml).trim();
  3236. if (!html) {
  3237. return "";
  3238. }
  3239. if (
  3240. /<html[\s>]/i.test(html) ||
  3241. /<body[\s>]/i.test(html) ||
  3242. /<!doctype/i.test(html)
  3243. ) {
  3244. try {
  3245. const doc = new DOMParser().parseFromString(html, "text/html");
  3246. if (doc && doc.body) {
  3247. return doc.body.innerHTML || "";
  3248. }
  3249. } catch (e) {
  3250. console.warn("ss-editor 解析完整HTML失败,回退原始内容:", e);
  3251. }
  3252. }
  3253. return html;
  3254. };
  3255. const syncHiddenContentInput = (html) => {
  3256. const contentElements = document.getElementsByName(
  3257. props.name.replace(/wj$/, "") + "Edit"
  3258. );
  3259. if (contentElements.length > 0) {
  3260. contentElements[0].value = html;
  3261. }
  3262. };
  3263. const setEditorHtml = (html) => {
  3264. const normalizedHtml = normalizeHtml(html);
  3265. editorContent.value = normalizedHtml;
  3266. syncHiddenContentInput(normalizedHtml);
  3267. if (editorRef.value && editorRef.value.value !== normalizedHtml) {
  3268. editorRef.value.value = normalizedHtml;
  3269. }
  3270. };
  3271. const resolveWindowFunction = (handlerName) => {
  3272. if (!handlerName || typeof handlerName !== "string") {
  3273. return null;
  3274. }
  3275. return handlerName.split(".").reduce((current, key) => {
  3276. return current && current[key] != null ? current[key] : null;
  3277. }, window);
  3278. };
  3279. const getCustomButtonKey = (button, index) => {
  3280. const baseName =
  3281. (button &&
  3282. (button.key || button.name || button.text || button.tooltip)) ||
  3283. `button${index + 1}`;
  3284. return `ssCustomButton_${String(baseName).replace(
  3285. /[^a-zA-Z0-9_-]/g,
  3286. "_"
  3287. )}_${index}`;
  3288. };
  3289. const normalizeCustomButtons = (buttons = []) => {
  3290. return Array.isArray(buttons)
  3291. ? buttons.filter(Boolean).map((button, index) => ({
  3292. ...button,
  3293. _buttonKey: getCustomButtonKey(button, index),
  3294. }))
  3295. : [];
  3296. };
  3297. const renderButtonIcon = (icon) => {
  3298. if (!icon) {
  3299. return null;
  3300. }
  3301. if (typeof icon === "string" && icon.trim().startsWith("<")) {
  3302. return icon;
  3303. }
  3304. return `<span class="editor-icon ${icon}"></span>`;
  3305. };
  3306. const buildCustomButtonRegistry = (buttons = []) => {
  3307. const controls = {};
  3308. const buttonNames = [];
  3309. const iconMap = {};
  3310. normalizeCustomButtons(buttons).forEach((button) => {
  3311. const buttonKey = button._buttonKey;
  3312. const handler =
  3313. button.onClick ||
  3314. button.click ||
  3315. button.handler ||
  3316. button.exec ||
  3317. null;
  3318. const tooltip =
  3319. button.tooltip ||
  3320. button.text ||
  3321. button.label ||
  3322. button.name ||
  3323. "自定义按钮";
  3324. buttonNames.push(buttonKey);
  3325. if (button.icon) {
  3326. iconMap[buttonKey] = button.icon;
  3327. }
  3328. controls[buttonKey] = {
  3329. name: button.name || buttonKey,
  3330. tooltip,
  3331. exec: function (editor) {
  3332. const context = {
  3333. editor,
  3334. button,
  3335. props,
  3336. value:
  3337. editor && typeof editor.value !== "undefined"
  3338. ? editor.value
  3339. : "",
  3340. setValue: (value) => setEditorHtml(value),
  3341. getValue: () =>
  3342. editor && typeof editor.value !== "undefined"
  3343. ? editor.value
  3344. : "",
  3345. emit,
  3346. currentModelValue: currentModelValue.value,
  3347. fjid: fjid.value,
  3348. };
  3349. if (typeof handler === "function") {
  3350. return handler(context);
  3351. }
  3352. if (typeof handler === "string") {
  3353. const windowHandler = resolveWindowFunction(handler);
  3354. if (typeof windowHandler === "function") {
  3355. return windowHandler(context);
  3356. }
  3357. console.warn("ss-editor 自定义按钮未找到点击事件:", handler);
  3358. return undefined;
  3359. }
  3360. console.warn("ss-editor 自定义按钮缺少点击事件配置:", button);
  3361. return undefined;
  3362. },
  3363. };
  3364. });
  3365. return {
  3366. controls,
  3367. buttonNames,
  3368. iconMap,
  3369. };
  3370. };
  3371. const validate = () => {
  3372. if (window.ssVm) {
  3373. const result = window.ssVm.validateField(props.name);
  3374. console.log("validate", window.ssVm.validateField(props.name));
  3375. errMsg.value = result.valid ? "" : result.message;
  3376. }
  3377. };
  3378. onMounted(() => {
  3379. validate();
  3380. // 只读模式下不初始化 Jodit,直接设置内容 by xu 20250402
  3381. if (props.mode === "play") {
  3382. if (props.html) {
  3383. setEditorHtml(props.html);
  3384. } else if (props.url) {
  3385. const params = new URLSearchParams();
  3386. if (mode) params.append("mode", mode);
  3387. if (props.modelValue) params.append("path", props.modelValue);
  3388. axios
  3389. .post(props.url, params, {
  3390. headers: {
  3391. "Content-Type": "application/x-www-form-urlencoded",
  3392. },
  3393. })
  3394. .then((response) => {
  3395. if ("timeout" == response.data.statusText) {
  3396. alert("网络超时!");
  3397. return;
  3398. }
  3399. const content = response.data.content;
  3400. if (content != null) {
  3401. setEditorHtml(content);
  3402. }
  3403. const filePath = response.data.path;
  3404. if (filePath) {
  3405. currentModelValue.value = filePath;
  3406. emit("update:modelValue", filePath);
  3407. }
  3408. });
  3409. } else {
  3410. setEditorHtml(props.html);
  3411. }
  3412. return;
  3413. }
  3414. // 如果 prop 为空,尝试从各种来源读取按钮配置
  3415. let buttonsToUse = props.customButtons;
  3416. if (!buttonsToUse || buttonsToUse.length === 0) {
  3417. // 1. 优先从 param.button 数组中筛选(新格式)by xu 20250331
  3418. if (customButtonsFromParam && customButtonsFromParam.length > 0) {
  3419. buttonsToUse = customButtonsFromParam;
  3420. }
  3421. // 2. 尝试从 param.customButtons 读取(旧格式,兼容)
  3422. if (!buttonsToUse || buttonsToUse.length === 0) {
  3423. if (props.param && props.param.customButtons && props.param.customButtons.length > 0) {
  3424. buttonsToUse = props.param.customButtons;
  3425. }
  3426. }
  3427. // 3. 尝试通过桥接 API 读取
  3428. if (!buttonsToUse || buttonsToUse.length === 0) {
  3429. const vm = window.SS.dom.getVueApp({ scope: "chain" });
  3430. if (vm && vm.zwwjCustomButtons) {
  3431. buttonsToUse = vm.zwwjCustomButtons;
  3432. }
  3433. }
  3434. // 4. 从 formElemConfig 读取(兜底)
  3435. if (!buttonsToUse || buttonsToUse.length === 0) {
  3436. const zwwjConfig = window.SS.dom.formElemConfig?.zwwj;
  3437. if (zwwjConfig?.customButtons) {
  3438. buttonsToUse = zwwjConfig.customButtons;
  3439. } else {
  3440. buttonsToUse = window.SS.dom.formElemConfig?.zwwjCustomButtons;
  3441. }
  3442. }
  3443. }
  3444. const {
  3445. controls: customButtonControls,
  3446. buttonNames: customButtonNames,
  3447. iconMap: customButtonIconMap,
  3448. } = buildCustomButtonRegistry(buttonsToUse);
  3449. console.log(
  3450. "[SsEditor] 解析后的按钮:",
  3451. customButtonNames,
  3452. customButtonControls
  3453. );
  3454. const appendCustomButtons = (buttons = []) => {
  3455. if (!customButtonNames.length) {
  3456. return buttons;
  3457. }
  3458. const toolbarButtons = [...buttons];
  3459. const insertIndex = toolbarButtons.includes("dots")
  3460. ? toolbarButtons.indexOf("dots")
  3461. : toolbarButtons.includes("print")
  3462. ? toolbarButtons.indexOf("print")
  3463. : -1;
  3464. if (insertIndex >= 0) {
  3465. toolbarButtons.splice(insertIndex, 0, ...customButtonNames);
  3466. return toolbarButtons;
  3467. }
  3468. return [...toolbarButtons, ...customButtonNames];
  3469. };
  3470. const editor = Jodit.make(`#${uniqueId}`, {
  3471. height: props.height,
  3472. placeholder: props.placeholder,
  3473. readonly: props.mode === "play",
  3474. language: "zh_cn",
  3475. i18n: {
  3476. zh_cn: {
  3477. Link: "链接",
  3478. URL: "链接",
  3479. "No follow": "无跟踪",
  3480. "Class name": "类名",
  3481. Image: "图片",
  3482. File: "文件",
  3483. "Line height": "行高",
  3484. Alternative: "描述",
  3485. "Alternative text": "描述",
  3486. "Lower Alpha": "小写字母",
  3487. "Upper Alpha": "大写字母",
  3488. "Upper Roman": "大写罗马数字",
  3489. "Lower Roman": "小写罗马数字",
  3490. "Lower Greek": "小写希腊字母",
  3491. "Lower Letter": "小写字母",
  3492. "Upper Letter": "大写字母",
  3493. },
  3494. },
  3495. showXPathInStatusbar: false,
  3496. showCharsCounter: false,
  3497. showWordsCounter: false,
  3498. allowResizeY: false,
  3499. toolbarSticky: false,
  3500. statusbar: false,
  3501. uploader: {
  3502. url: props.uploadUrl,
  3503. format: "json",
  3504. method: "POST",
  3505. filesVariableName: function (i) {
  3506. return "imgs[" + i + "]";
  3507. },
  3508. headers: {
  3509. Accept: "application/json",
  3510. },
  3511. prepareData: function (formData) {
  3512. return formData;
  3513. },
  3514. isSuccess: function (resp) {
  3515. console.log("isSuccess resp:", resp);
  3516. return resp.code === 0;
  3517. },
  3518. getMessage: function (resp) {
  3519. console.log("getMessage resp:", resp);
  3520. return resp.msg || "上传失败";
  3521. },
  3522. process: function (resp) {
  3523. console.log("process resp:", resp);
  3524. return resp.data.url;
  3525. },
  3526. error: function (e) {
  3527. console.error("上传失败:", e.message);
  3528. },
  3529. defaultHandlerSuccess: function (resp) {
  3530. console.log("上传成功:", resp);
  3531. },
  3532. defaultHandlerError: function (err) {
  3533. console.error("上传错误:", err);
  3534. },
  3535. contentType: function () {
  3536. return false;
  3537. },
  3538. },
  3539. controls: {
  3540. font: {
  3541. list: {
  3542. Arial: "Arial",
  3543. SimSun: "宋体",
  3544. SimHei: "黑体",
  3545. "Microsoft YaHei": "微软雅黑",
  3546. KaiTi: "楷体",
  3547. FangSong: "仿宋",
  3548. "Times New Roman": "Times New Roman",
  3549. "Courier New": "Courier New",
  3550. },
  3551. },
  3552. customLinkButton: {
  3553. name: "link",
  3554. tooltip: "附件",
  3555. exec: function () {
  3556. if (!fjButton.cmsAddUrl || !fjButton.cmsUpdUrl) {
  3557. console.warn("ss-editor 未配置附件按钮地址,忽略附件操作");
  3558. return;
  3559. }
  3560. if (fjid.value == null || fjid.value === "") {
  3561. $.ajax({
  3562. type: "post",
  3563. url: fjButton.cmsAddUrl,
  3564. async: false,
  3565. data: {
  3566. name: "fjid",
  3567. ssNrObjName: "sh",
  3568. ssNrObjId: "",
  3569. },
  3570. success: function (_fjid) {
  3571. console.log("cmsAddUrl success", _fjid);
  3572. fjid.value = _fjid;
  3573. },
  3574. });
  3575. }
  3576. var str =
  3577. "&nrid=T-" +
  3578. fjid.value +
  3579. "&objectId=" +
  3580. fjid.value +
  3581. "&objectName=" +
  3582. fjName +
  3583. "&callback=" +
  3584. (window["fjidCallbackName"] || "");
  3585. console.log("str", str);
  3586. SS.openDialog({
  3587. src: buttonConfig.cmsUpdUrl + str,
  3588. headerTitle: "编辑",
  3589. width: 900,
  3590. high: 664,
  3591. zIndex: 51,
  3592. });
  3593. },
  3594. },
  3595. ...customButtonControls,
  3596. },
  3597. toolbarAdaptive: true,
  3598. // 调试:记录最终按钮配置
  3599. buttons: (() => {
  3600. const finalButtons = appendCustomButtons([
  3601. "fullsize",
  3602. "bold",
  3603. "italic",
  3604. "underline",
  3605. "strikethrough",
  3606. "eraser",
  3607. "|",
  3608. "font",
  3609. "fontsize",
  3610. "brush",
  3611. "paragraph",
  3612. "|",
  3613. "left",
  3614. "center",
  3615. "right",
  3616. "justify",
  3617. "|",
  3618. "ul",
  3619. "ol",
  3620. "indent",
  3621. "outdent",
  3622. "|",
  3623. "image",
  3624. "table",
  3625. "customLinkButton",
  3626. "print",
  3627. "|",
  3628. "undo",
  3629. "redo",
  3630. "find",
  3631. ]);
  3632. console.log("[SsEditor] 最终 buttons 配置:", finalButtons);
  3633. return finalButtons;
  3634. })(),
  3635. buttonsMD: appendCustomButtons([
  3636. "fullsize",
  3637. "bold",
  3638. "italic",
  3639. "underline",
  3640. "strikethrough",
  3641. "eraser",
  3642. "|",
  3643. "font",
  3644. "fontsize",
  3645. "brush",
  3646. "paragraph",
  3647. "|",
  3648. "font",
  3649. "fontsize",
  3650. "|",
  3651. "left",
  3652. "center",
  3653. "right",
  3654. "justify",
  3655. "|",
  3656. "image",
  3657. "customLinkButton",
  3658. "|",
  3659. "dots",
  3660. ]),
  3661. buttonsSM: appendCustomButtons([
  3662. "fullsize",
  3663. "bold",
  3664. "italic",
  3665. "|",
  3666. "image",
  3667. "|",
  3668. "dots",
  3669. ]),
  3670. buttonsXS: appendCustomButtons(["fullsize", "bold", "|", "dots"]),
  3671. sizeLG: 1024,
  3672. sizeMD: 768,
  3673. sizeSM: 576,
  3674. getIcon: function (name, clearName) {
  3675. const iconMap = {
  3676. bold: "editor-icon-bold",
  3677. italic: "editor-icon-italic",
  3678. underline: "editor-icon-underline",
  3679. strikethrough: "editor-icon-strikethrough",
  3680. eraser: "editor-icon-eraser",
  3681. copyformat: "editor-icon-copyformat",
  3682. font: "editor-icon-font",
  3683. fontsize: "editor-icon-fontsize",
  3684. brush: "editor-icon-brush",
  3685. paragraph: "editor-icon-paragraph",
  3686. left: "editor-icon-align-left",
  3687. center: "editor-icon-align-center",
  3688. right: "editor-icon-align-right",
  3689. justify: "editor-icon-align-justify",
  3690. ul: "editor-icon-ul",
  3691. ol: "editor-icon-ol",
  3692. indent: "editor-icon-indent",
  3693. outdent: "editor-icon-outdent",
  3694. image: "editor-icon-image",
  3695. file: "editor-icon-file",
  3696. video: "editor-icon-video",
  3697. table: "editor-icon-table",
  3698. link: "editor-icon-link",
  3699. source: "editor-icon-source",
  3700. eye: "editor-icon-preview",
  3701. fullsize: "editor-icon-fullsize",
  3702. shrink: "editor-icon-fullsize-exit",
  3703. print: "editor-icon-print",
  3704. undo: "editor-icon-undo",
  3705. redo: "editor-icon-redo",
  3706. search: "editor-icon-find",
  3707. selectall: "editor-icon-selectall",
  3708. };
  3709. const customIcon =
  3710. customButtonIconMap[clearName] || customButtonIconMap[name];
  3711. if (customIcon) {
  3712. return renderButtonIcon(customIcon);
  3713. }
  3714. const iconClass = iconMap[clearName] || iconMap[name];
  3715. if (iconClass) {
  3716. return renderButtonIcon(iconClass);
  3717. }
  3718. return null;
  3719. },
  3720. });
  3721. editorRef.value = editor;
  3722. editor.events.on("change", () => {
  3723. const html = editor.value || "";
  3724. editorContent.value = html;
  3725. syncHiddenContentInput(html);
  3726. emit("change", html);
  3727. setTimeout(() => {
  3728. validate();
  3729. }, 50);
  3730. });
  3731. emit("ready", editor);
  3732. if (props.html) {
  3733. setEditorHtml(props.html);
  3734. } else if (props.url) {
  3735. const params = new URLSearchParams();
  3736. if (mode) params.append("mode", mode);
  3737. if (props.modelValue) params.append("path", props.modelValue);
  3738. axios
  3739. .post(props.url, params, {
  3740. headers: {
  3741. "Content-Type": "application/x-www-form-urlencoded",
  3742. },
  3743. })
  3744. .then((response) => {
  3745. if ("timeout" == response.data.statusText) {
  3746. alert("网络超时!");
  3747. return;
  3748. }
  3749. const content = response.data.content;
  3750. if (content != null) {
  3751. setEditorHtml(content);
  3752. }
  3753. const filePath = response.data.path;
  3754. if (filePath) {
  3755. currentModelValue.value = filePath;
  3756. emit("update:modelValue", filePath);
  3757. }
  3758. });
  3759. } else {
  3760. setEditorHtml(props.html);
  3761. }
  3762. });
  3763. watch(
  3764. () => props.html,
  3765. (newValue) => {
  3766. setEditorHtml(newValue);
  3767. }
  3768. );
  3769. watch(
  3770. () => props.modelValue,
  3771. (newValue) => {
  3772. currentModelValue.value = newValue || "";
  3773. }
  3774. );
  3775. watch(
  3776. () => props.mode,
  3777. (newMode) => {
  3778. if (editorRef.value) {
  3779. editorRef.value.setReadOnly(newMode === "play");
  3780. }
  3781. }
  3782. );
  3783. // 监听 customButtons 变化,支持运行时动态更新 by xu 20250331
  3784. watch(
  3785. () => props.customButtons,
  3786. (newButtons) => {
  3787. if (!editorRef.value) return;
  3788. if (!newButtons || newButtons.length === 0) return;
  3789. console.log("[SsEditor] customButtons 变化:", newButtons);
  3790. const { controls, buttonNames } = buildCustomButtonRegistry(newButtons);
  3791. if (buttonNames.length === 0) return;
  3792. // 注册新控件到 Jodit
  3793. buttonNames.forEach((btnKey) => {
  3794. const control = controls[btnKey];
  3795. if (control && control.exec) {
  3796. // 使用 Jodit 的 register 方法注册新按钮
  3797. editorRef.value.controls.register(btnKey, control);
  3798. console.log("[SsEditor] 注册新按钮:", btnKey);
  3799. }
  3800. });
  3801. // 更新工具栏(需要重新构建)
  3802. const toolbar = editorRef.value.toolbar;
  3803. if (toolbar && toolbar.build) {
  3804. try {
  3805. // 获取当前配置并重新构建工具栏
  3806. const currentButtons = editorRef.value.options.buttons || [];
  3807. const newToolbarButtons = [...currentButtons, ...buttonNames];
  3808. editorRef.value.options.buttons = newToolbarButtons;
  3809. toolbar.build(newToolbarButtons, toolbar.container);
  3810. console.log("[SsEditor] 工具栏已更新,新按钮:", buttonNames);
  3811. } catch (e) {
  3812. console.warn("[SsEditor] 工具栏更新失败:", e);
  3813. }
  3814. }
  3815. },
  3816. { deep: true, immediate: false }
  3817. );
  3818. onBeforeUnmount(() => {
  3819. if (editorRef.value) {
  3820. editorRef.value.destruct();
  3821. }
  3822. });
  3823. // 只读模式:打开附件弹窗 by xu 20250402
  3824. const openAttachmentDialog = () => {
  3825. if (!fjButton.cmsAddUrl || !fjButton.cmsUpdUrl) {
  3826. console.warn("ss-editor 未配置附件按钮地址,忽略附件操作");
  3827. return;
  3828. }
  3829. if (fjid.value == null || fjid.value === "") {
  3830. $.ajax({
  3831. type: "post",
  3832. url: fjButton.cmsAddUrl,
  3833. async: false,
  3834. data: {
  3835. name: "fjid",
  3836. ssNrObjName: "sh",
  3837. ssNrObjId: "",
  3838. },
  3839. success: function (_fjid) {
  3840. console.log("cmsAddUrl success", _fjid);
  3841. fjid.value = _fjid;
  3842. },
  3843. });
  3844. }
  3845. var str =
  3846. "&nrid=T-" +
  3847. fjid.value +
  3848. "&objectId=" +
  3849. fjid.value +
  3850. "&objectName=" +
  3851. fjName +
  3852. "&callback=" +
  3853. (window["fjidCallbackName"] || "");
  3854. console.log("str", str);
  3855. SS.openDialog({
  3856. src: fjButton.cmsUpdUrl + str,
  3857. headerTitle: "编辑",
  3858. width: 900,
  3859. high: 664,
  3860. zIndex: 51,
  3861. });
  3862. };
  3863. return () => {
  3864. // 只读模式:不初始化 Jodit,直接渲染 HTML + 附件按钮 by xu 20250402
  3865. if (props.mode === "play") {
  3866. return h("div", { class: "ss-editor-container ss-editor-readonly" }, [
  3867. fjid.value &&
  3868. h("input", {
  3869. type: "hidden",
  3870. name: "fjid",
  3871. value: fjid.value,
  3872. }),
  3873. h("input", {
  3874. type: "hidden",
  3875. name: props.name.replace(/wj$/, "") + "Edit",
  3876. value: editorContent.value,
  3877. }),
  3878. h("input", {
  3879. type: "hidden",
  3880. name: props.name.replace(/wj$/, "") + "wj",
  3881. value: currentModelValue.value,
  3882. }),
  3883. h("input", {
  3884. type: "hidden",
  3885. name: "ueditorpath",
  3886. value: "mswj",
  3887. }),
  3888. // 附件按钮(只读模式下仍显示)
  3889. fjButton.cmsAddUrl && fjButton.cmsUpdUrl &&
  3890. h("div", {
  3891. class: "ss-editor-attach-btn",
  3892. onClick: openAttachmentDialog,
  3893. }, [
  3894. h("span", { class: "editor-icon editor-icon-file" }),
  3895. h("span", { class: "ss-editor-attach-text" }, "附件")
  3896. ]),
  3897. // HTML 内容展示
  3898. h("div", {
  3899. class: "ss-editor-readonly-content",
  3900. innerHTML: editorContent.value,
  3901. }),
  3902. ]);
  3903. }
  3904. // 正常编辑模式:渲染 textarea 供 Jodit 初始化
  3905. return h("div", { class: "ss-editor-container" }, [
  3906. fjid.value &&
  3907. h("input", {
  3908. type: "hidden",
  3909. name: "fjid",
  3910. value: fjid.value,
  3911. }),
  3912. h("input", {
  3913. type: "hidden",
  3914. name: props.name.replace(/wj$/, "") + "Edit",
  3915. value: editorContent.value,
  3916. }),
  3917. h("input", {
  3918. type: "hidden",
  3919. name: props.name.replace(/wj$/, "") + "wj",
  3920. value: currentModelValue.value,
  3921. }),
  3922. h("input", {
  3923. type: "hidden",
  3924. name: "ueditorpath",
  3925. value: "mswj",
  3926. }),
  3927. h("textarea", { id: uniqueId }),
  3928. ]);
  3929. };
  3930. },
  3931. };
  3932. // 弹窗右边图标
  3933. const SsFullStyleHeader = {
  3934. name: "SsFullStyleHeader",
  3935. props: {
  3936. title: {
  3937. type: String,
  3938. default: "标题",
  3939. },
  3940. },
  3941. emits: ["close"],
  3942. setup(props, { emit }) {
  3943. // console.log(props.title)
  3944. const onClose = () => {
  3945. emit("close");
  3946. };
  3947. const SsIcon = resolveComponent("ss-icon");
  3948. return () =>
  3949. h("div", { class: "header-container" }, [
  3950. h("div", { class: "title" }, props.title),
  3951. h("div", { class: "handle-bar" }, [
  3952. h("div", { class: "left-bar" }, [
  3953. h(SsDialogIcon, { class: "dialog-icon-download" }),
  3954. h(SsDialogIcon, { class: "dialog-icon-print" }),
  3955. h(SsDialogIcon, { class: "dialog-icon-setting" }),
  3956. h(SsDialogIcon, { class: "dialog-icon-collect" }),
  3957. h(SsDialogIcon, { class: "dialog-icon-help" }),
  3958. h(SsDialogIcon, { class: "dialog-icon-full-screen" }),
  3959. h(SsDialogIcon, { class: "dialog-icon-lock" }),
  3960. ]),
  3961. h("div", { class: "close-bar", onClick: onClose }, [
  3962. h(SsDialogIcon, { class: "dialog-icon-close" }),
  3963. ]),
  3964. ]),
  3965. ]);
  3966. },
  3967. };
  3968. // ss-dialog弹窗
  3969. const SsDialog = {
  3970. name: "SsDialog",
  3971. props: {
  3972. src: {
  3973. type: String,
  3974. },
  3975. headerTitle: {
  3976. type: String,
  3977. // required: true,
  3978. default: "弹窗",
  3979. },
  3980. width: {
  3981. type: String,
  3982. default: "1400",
  3983. },
  3984. height: {
  3985. type: String,
  3986. default: "600",
  3987. },
  3988. params: {
  3989. type: Object,
  3990. default: () => ({}),
  3991. },
  3992. zIndex: {
  3993. type: Number,
  3994. default: 1000,
  3995. },
  3996. },
  3997. emits: ["close"],
  3998. setup(props, { slots, emit }) {
  3999. // 关闭窗口方法
  4000. const onClose = () => {
  4001. emit("close");
  4002. };
  4003. const showHeader = ref(true);
  4004. const headerVisible = ref(false);
  4005. const popupHieght = ref(props.height);
  4006. // 状态:存储位置信息
  4007. const position = reactive({
  4008. // 页面居中
  4009. x: (window.innerWidth - props.width) / 2,
  4010. y: (window.innerHeight - popupHieght.value) / 2,
  4011. isDragging: false,
  4012. offsetX: 0,
  4013. offsetY: 0,
  4014. });
  4015. // 鼠标按下时设置起始坐标并开始拖拽
  4016. const startDrag = (event) => {
  4017. position.isDragging = true;
  4018. position.offsetX = event.clientX - position.x;
  4019. position.offsetY = event.clientY - position.y;
  4020. };
  4021. // 鼠标移动时更新位置
  4022. const onDrag = (event) => {
  4023. if (position.isDragging) {
  4024. position.x = event.clientX - position.offsetX;
  4025. position.y = event.clientY - position.offsetY;
  4026. }
  4027. };
  4028. // 鼠标放开时结束拖拽
  4029. const endDrag = () => {
  4030. position.isDragging = false;
  4031. };
  4032. // 监听来自 iframe 的消息
  4033. const handleMessage = (event) => {
  4034. // 顶天立地
  4035. if (event.data && typeof event.data.hasScrollBar !== "undefined") {
  4036. if (event.data.hasScrollBar) {
  4037. // console.log(event);
  4038. position.y = 10;
  4039. showHeader.value = false;
  4040. headerVisible.value = true;
  4041. popupHieght.value = window.innerHeight - 20;
  4042. // console.log(popupHieght.value);
  4043. document.querySelector(".body").style.height = "100%";
  4044. document.querySelector(".body").style.paddingTop = "30px";
  4045. document.querySelector(".header-container ").style.position =
  4046. "absolute";
  4047. document.querySelector(".header-container ").style.zIndex = "10";
  4048. }
  4049. }
  4050. };
  4051. // 鼠标移入关闭按钮区域时显示头部
  4052. const onMouseEnterCloseButton = () => {
  4053. headerVisible.value = false;
  4054. };
  4055. // 鼠标移出关闭按钮区域时隐藏头部
  4056. const onMouseLeaveCloseButton = () => {
  4057. headerVisible.value = true;
  4058. };
  4059. // 在组件挂载时添加全局事件监听器
  4060. Vue.onMounted(() => {
  4061. // 如果传过来的高度大于窗口高度,则设置为窗口高度减去20 否则保持传过来的高度
  4062. popupHieght.value =
  4063. popupHieght.value > window.innerHeight
  4064. ? window.innerHeight - 20
  4065. : popupHieght.value;
  4066. const container = document.querySelector(".header-container");
  4067. if (container) {
  4068. container.addEventListener("mousedown", startDrag);
  4069. }
  4070. document.addEventListener("mousemove", onDrag);
  4071. document.addEventListener("mouseup", endDrag);
  4072. window.addEventListener("message", handleMessage);
  4073. });
  4074. // 在组件卸载时移除全局事件监听器
  4075. Vue.onUnmounted(() => {
  4076. document.removeEventListener("mousemove", onDrag);
  4077. document.removeEventListener("mouseup", endDrag);
  4078. window.removeEventListener("message", handleMessage);
  4079. });
  4080. const SsMark = resolveComponent("ss-mark");
  4081. const SsFullStyleHeader = resolveComponent("ss-full-style-header");
  4082. // render函数定义组件结构
  4083. return () =>
  4084. h(
  4085. Teleport,
  4086. { to: "body" }, // 使用 Teleport 将弹窗内容挂载到 body
  4087. h(SsMark, {}, [
  4088. h(
  4089. "div",
  4090. {
  4091. class: "popup-container",
  4092. style: {
  4093. position: "absolute",
  4094. left: `${position.x}px`,
  4095. top: `${position.y}px`,
  4096. width: props.width + "px",
  4097. height: popupHieght.value + "px",
  4098. zIndex: props.zIndex, // 确保弹窗在最上层
  4099. },
  4100. },
  4101. [
  4102. h(SsFullStyleHeader, {
  4103. class: "header",
  4104. title: props.headerTitle,
  4105. onClose: onClose,
  4106. onMousedown: startDrag, // 绑定拖动事件
  4107. onMouseUp: endDrag,
  4108. ...(!showHeader.value && {
  4109. onMouseenter: onMouseEnterCloseButton,
  4110. onMouseleave: onMouseLeaveCloseButton,
  4111. }),
  4112. style: {
  4113. cursor: position.isDragging ? "grabbing" : "grab",
  4114. visibility: headerVisible.value ? "hidden" : "visible",
  4115. },
  4116. }),
  4117. h(
  4118. "div",
  4119. {
  4120. class: "body",
  4121. style: {},
  4122. },
  4123. [
  4124. h("iframe", {
  4125. src: props.src,
  4126. frameborder: 0,
  4127. style: { width: "100%", height: "100%" },
  4128. }),
  4129. ]
  4130. ),
  4131. headerVisible.value &&
  4132. h("div", {
  4133. class: "close-button",
  4134. onMouseenter: onMouseEnterCloseButton,
  4135. onMouseleave: onMouseLeaveCloseButton,
  4136. style: {
  4137. position: "absolute",
  4138. top: "0",
  4139. right: "0",
  4140. // background: 'black',
  4141. width: "60px",
  4142. height: "60px",
  4143. cursor: "pointer",
  4144. },
  4145. }),
  4146. ]
  4147. ),
  4148. ])
  4149. );
  4150. },
  4151. };
  4152. // ss-mark遮罩层
  4153. const SsMark = {
  4154. name: "SsMark",
  4155. setup(props, { slots, emit }) {
  4156. return () =>
  4157. h("div", { class: "dialog-container" }, [
  4158. h("div", { class: "mark-content" }, [
  4159. h("div", { class: "dialog-contianer" }, [
  4160. slots.default ? slots.default() : "",
  4161. ]),
  4162. ]),
  4163. ]);
  4164. },
  4165. };
  4166. // ss-bottom-button 底部按钮
  4167. // 修改支持更多按钮 by xu 20251211
  4168. const SsBottomButton = {
  4169. name: "SsBottomButton",
  4170. props: {
  4171. text: {
  4172. type: String,
  4173. required: false,
  4174. },
  4175. type: {
  4176. type: String,
  4177. default: "button",
  4178. },
  4179. iconClass: {
  4180. type: String,
  4181. },
  4182. class: {
  4183. type: String,
  4184. default: "",
  4185. },
  4186. onclick: {
  4187. type: [Function, String],
  4188. default: null,
  4189. },
  4190. // 修改支持更多按钮 by xu 20251211
  4191. more: {
  4192. type: [Boolean, String],
  4193. default: false,
  4194. },
  4195. },
  4196. setup(props, { emit }) {
  4197. const SsBottomDivIcon = Vue.resolveComponent("ss-bottom-div-icon");
  4198. const showDropdown = Vue.ref(false);
  4199. // 修改支持更多按钮 by xu 20251211
  4200. const moreKey = Vue.computed(() => {
  4201. const val = props.more;
  4202. if (val === false || val === null || typeof val === "undefined") {
  4203. return null;
  4204. }
  4205. if (val === true || val === "" || val === "true") {
  4206. return "moreChg";
  4207. }
  4208. return val;
  4209. });
  4210. // 从配置中读取按钮信息和下拉选项
  4211. const config = Vue.computed(() => {
  4212. if (
  4213. moreKey.value &&
  4214. window.ss &&
  4215. window.ss.dom &&
  4216. window.ss.dom.btnElemConfig
  4217. ) {
  4218. return window.ss.dom.btnElemConfig[moreKey.value] || {};
  4219. }
  4220. return {};
  4221. });
  4222. const buttonText = Vue.computed(() => {
  4223. return props.text || config.value.desc || "";
  4224. });
  4225. const dropOptions = Vue.computed(() => {
  4226. return config.value.dropOptions || [];
  4227. });
  4228. const hasDropdown = Vue.computed(() => {
  4229. return dropOptions.value.length > 0;
  4230. });
  4231. const handleMouseEnter = () => {
  4232. if (hasDropdown.value) {
  4233. showDropdown.value = true;
  4234. }
  4235. };
  4236. const handleMouseLeave = () => {
  4237. showDropdown.value = false;
  4238. };
  4239. const handleDropItemClick = (option) => {
  4240. if (option.callback && typeof option.callback === "function") {
  4241. option.callback();
  4242. }
  4243. showDropdown.value = false;
  4244. };
  4245. return () =>
  4246. h(
  4247. "div",
  4248. {
  4249. class: "ss-bottom-button-wrapper",
  4250. onMouseenter: handleMouseEnter,
  4251. onMouseleave: handleMouseLeave,
  4252. },
  4253. [
  4254. h(
  4255. "button",
  4256. {
  4257. class: props.class,
  4258. onClick: (e) => {
  4259. e.stopPropagation();
  4260. emit("click", e);
  4261. if (props.onclick) {
  4262. // 如果是函数直接调用
  4263. if (typeof props.onclick === "function") {
  4264. props.onclick(e);
  4265. } else if (typeof props.onclick === "string") {
  4266. // 如果是字符串,使用直接的方法执行
  4267. // 临时存储按钮元素到全局变量
  4268. window.__ss_current_button = e.currentTarget;
  4269. // 直接执行代码,使用eval以保留原始上下文
  4270. try {
  4271. eval(props.onclick);
  4272. } finally {
  4273. // 清理全局变量
  4274. delete window.__ss_current_button;
  4275. }
  4276. }
  4277. }
  4278. },
  4279. type: props.type,
  4280. },
  4281. [
  4282. h("span", null, [
  4283. h(SsBottomDivIcon, {
  4284. class: props.iconClass,
  4285. }),
  4286. ]),
  4287. h("span", null, buttonText.value),
  4288. ]
  4289. ),
  4290. // 渲染下拉菜单
  4291. hasDropdown.value && showDropdown.value
  4292. ? h(
  4293. "div",
  4294. {
  4295. class: "ss-bottom-button-dropdown",
  4296. },
  4297. dropOptions.value.map((option) =>
  4298. h(
  4299. "div",
  4300. {
  4301. class: "ss-bottom-button-dropdown-item",
  4302. onClick: (e) => {
  4303. e.stopPropagation();
  4304. handleDropItemClick(option);
  4305. },
  4306. },
  4307. option.desc
  4308. )
  4309. )
  4310. )
  4311. : null,
  4312. ]
  4313. );
  4314. },
  4315. };
  4316. // ss-search搜索框
  4317. const SsSearch = {
  4318. name: "SsSearch",
  4319. props: {
  4320. theme: {
  4321. type: String,
  4322. default: "light",
  4323. validator: function (value) {
  4324. return ["dark", "light"].includes(value);
  4325. },
  4326. },
  4327. placeholder: {
  4328. type: String,
  4329. default: "请输入搜索条件",
  4330. },
  4331. },
  4332. setup(props, { emit }) {
  4333. const onClick = () => {
  4334. console.log("Search clicked");
  4335. emit("click");
  4336. };
  4337. const SsIcon = Vue.resolveComponent("ss-icon");
  4338. return () =>
  4339. Vue.h(
  4340. "div",
  4341. {
  4342. class: ["search-container", props.theme],
  4343. onClick: onClick,
  4344. },
  4345. [
  4346. Vue.h("input", {
  4347. placeholder: props.placeholder,
  4348. disabled: true,
  4349. }),
  4350. Vue.h(SsIcon, {
  4351. name: "search-result",
  4352. size: "20px",
  4353. }),
  4354. ]
  4355. );
  4356. },
  4357. };
  4358. // ss-cart-item 菜单页面的卡片 左右结构
  4359. const SsCartItem = {
  4360. name: "SsCartItem",
  4361. props: {
  4362. active: Boolean,
  4363. item: {
  4364. type: Object,
  4365. default: () => ({
  4366. thumb: "images/example/project-img.png",
  4367. title: "广州(国际)科技成果转化天河基地专",
  4368. description: "佳能中国广州分公司",
  4369. all: 50,
  4370. finish: 5,
  4371. }),
  4372. },
  4373. },
  4374. setup(props, { emit }) {
  4375. const item = props.item;
  4376. const itemWidth = Vue.computed(() => {
  4377. const containerWidth =
  4378. document.body.clientWidth || document.body.scrollWidth - 520;
  4379. const halfWidth = containerWidth / 2;
  4380. if (halfWidth < 480) {
  4381. return Math.min(containerWidth, 702) + "px";
  4382. } else {
  4383. return Math.min(halfWidth, 702) + "px";
  4384. }
  4385. });
  4386. const onItemClick = (e) => {
  4387. emit("click", e);
  4388. };
  4389. return {
  4390. item,
  4391. itemWidth,
  4392. onItemClick,
  4393. };
  4394. },
  4395. render() {
  4396. const SsIcon = Vue.resolveComponent("ss-icon");
  4397. return Vue.h(
  4398. "div",
  4399. {
  4400. class: { "item-container": true, active: this.active },
  4401. onClick: this.onItemClick,
  4402. style: { width: this.itemWidth },
  4403. },
  4404. [
  4405. Vue.h("div", { class: "header" }, [
  4406. Vue.h(SsIcon, { name: "setting", size: "20px" }),
  4407. ]),
  4408. Vue.h("div", { class: "body" }, [
  4409. Vue.h("div", { class: "left" }, [
  4410. Vue.h("img", {
  4411. src: this.item.thumb,
  4412. alt: "Thumbnail",
  4413. class: "imgUnHandle",
  4414. style: { "object-fit": "cover", width: "100%", height: "100%" },
  4415. }),
  4416. ]),
  4417. Vue.h("div", { class: "right" }, [
  4418. Vue.h("div", { class: "title" }, this.item.title),
  4419. Vue.h("div", { class: "desc" }, this.item.description),
  4420. Vue.h("div", { class: "progress" }, [
  4421. Vue.h(
  4422. "div",
  4423. {
  4424. style: {
  4425. width: `${(this.item.finish / this.item.all) * 100}%`,
  4426. },
  4427. },
  4428. [Vue.h("div", `${this.item.finish}/${this.item.all}`)]
  4429. ),
  4430. ]),
  4431. ]),
  4432. ]),
  4433. ]
  4434. );
  4435. },
  4436. };
  4437. // ss-cart-item2 菜单页面的卡片 上下结构
  4438. const SsCartItem2 = {
  4439. name: "SsCartItem2",
  4440. props: {
  4441. active: Boolean,
  4442. item: {
  4443. type: Object,
  4444. default: () => ({
  4445. thumb: "images/example/project-img.png",
  4446. title: "广州(国际)科技成果转化天河基地专",
  4447. description: "佳能中国广州分公司",
  4448. all: 50,
  4449. finish: 5,
  4450. }),
  4451. },
  4452. },
  4453. setup(props, { emit }) {
  4454. const item = props.item;
  4455. const itemWidth = Vue.computed(() => {
  4456. const containerWidth =
  4457. document.body.clientWidth || document.body.scrollWidth - 520;
  4458. const halfWidth = containerWidth / 2;
  4459. if (halfWidth < 480) {
  4460. return Math.min(containerWidth, 702) + "px";
  4461. } else {
  4462. return Math.min(halfWidth, 702) + "px";
  4463. }
  4464. });
  4465. const onItemClick = (e) => {
  4466. emit("click", e);
  4467. };
  4468. return {
  4469. item,
  4470. itemWidth,
  4471. onItemClick,
  4472. };
  4473. },
  4474. render() {
  4475. const SsIcon = Vue.resolveComponent("ss-icon");
  4476. return Vue.h(
  4477. "div",
  4478. {
  4479. class: { "item-container2": true, active: this.active },
  4480. onClick: this.onItemClick,
  4481. style: { width: this.itemWidth },
  4482. },
  4483. [
  4484. Vue.h("div", { class: "action-bar" }, [
  4485. Vue.h(SsIcon, { name: "setting", size: "20px" }),
  4486. ]),
  4487. Vue.h("div", { class: "header" }, [
  4488. Vue.h("div", { class: "title" }, `${this.item.title}`),
  4489. ]),
  4490. Vue.h("div", { class: "body" }, [
  4491. Vue.h("div", { class: "left" }, [
  4492. Vue.h("img", {
  4493. src: this.item.thumb,
  4494. alt: "Thumbnail",
  4495. class: "imgUnHandle",
  4496. style: { "object-fit": "cover", width: "100%", height: "100%" },
  4497. }),
  4498. ]),
  4499. Vue.h("div", { class: "right" }, [
  4500. Vue.h("div", { class: "content" }, this.item.description),
  4501. Vue.h("div", { class: "tip" }, [
  4502. Vue.h("div", { class: "progress" }, [
  4503. Vue.h(
  4504. "div",
  4505. {
  4506. style: {
  4507. width: `${(this.item.finish / this.item.all) * 100}%`,
  4508. },
  4509. },
  4510. [Vue.h("div", `${this.item.finish}/${this.item.all}`)]
  4511. ),
  4512. ]),
  4513. ]),
  4514. ]),
  4515. ]),
  4516. ]
  4517. );
  4518. },
  4519. };
  4520. /**
  4521. * SsListCard - 列表卡片组件
  4522. *
  4523. * @description 用于显示列表项的卡片组件,支持缩略图、标签、状态、操作按钮和选择功能
  4524. *
  4525. * @prop {Object} item - 卡片数据对象
  4526. * @prop {String} item.title - 卡片标题
  4527. * @prop {String} [item.thumb] - 缩略图 URL(可选)
  4528. * @prop {String} [item.thumbType] - 缩略图类型:'thumbnail'(缩略图)或默认(证件照)
  4529. * @prop {String} [item.status] - 卡片状态:'available'(可用-绿色)、'unavailable'(不可用-黄色)、'disabled'(禁用-红色)
  4530. * @prop {Array} item.tags - 标签数组,格式:[{键: 值}, ...]
  4531. * @prop {Function} item.onclick - 点击卡片的回调函数
  4532. * @prop {Array} [item.buttons] - 操作按钮数组(可选),显示在右上角齿轮
  4533. * @prop {Array} [item.statusIcons] - 状态图标数组(可选),显示在右上角,格式:[{class: '图标类名', title: '提示文字'}, ...]
  4534. *
  4535. * @example
  4536. * // 基础用法
  4537. * const item = {
  4538. * title: "卡片标题",
  4539. * tags: [
  4540. * { 类型: '文档' },
  4541. * { 状态: '进行中' }
  4542. * ],
  4543. * onclick: () => console.log('点击了卡片')
  4544. * };
  4545. *
  4546. * @example
  4547. * // 带缩略图和状态
  4548. * const item = {
  4549. * title: "场地预定",
  4550. * thumbType: 'thumbnail',
  4551. * thumb: "https://example.com/image.jpg",
  4552. * status: "available", // 绿色背景
  4553. * tags: [{ 容量: '50人' }],
  4554. * onclick: () => {}
  4555. * };
  4556. *
  4557. * @example
  4558. * // 带操作按钮和状态图标
  4559. * const item = {
  4560. * title: "会议室A",
  4561. * tags: [{ 楼层: '3F' }],
  4562. * onclick: () => {},
  4563. * // 右上角操作按钮(齿轮)
  4564. * buttons: [{
  4565. * class: 'cart-list-setting',
  4566. * title: '编辑',
  4567. * onclick: () => console.log('编辑')
  4568. * }],
  4569. * // 右上角状态图标(在齿轮右边)
  4570. * statusIcons: [{
  4571. * class: 'icon-emoji',
  4572. * title: '清洁中'
  4573. * }]
  4574. * };
  4575. *
  4576. * @features
  4577. * - 卡片选择:鼠标悬停右下角显示选择角标,点击切换选中状态,选中后显示底部深灰色线条
  4578. * - 状态颜色:根据 status 字段显示不同背景色(可用/不可用/禁用)
  4579. * - 图片类型:支持证件照(73×100px)和缩略图(180×100px)两种尺寸
  4580. * - 操作按钮:右上角齿轮,hover 显示,支持多个按钮下拉菜单
  4581. * - 状态图标:右上角显示状态图标,齿轮会根据图标数量自动左移
  4582. *
  4583. * @author xu
  4584. * @date 20260105
  4585. */
  4586. // 组件文档补全(JSDoc) by xu 20260108
  4587. /**
  4588. * SsListCard(左侧对象卡片)
  4589. *
  4590. * 用途:
  4591. * - 渲染左侧卡片(标题 + tags)
  4592. * - 右下角“角标”用于选中/取消选中(不会触发卡片 click)
  4593. *
  4594. * 调用示例:
  4595. * ```html
  4596. * <ss-list-card :item="item" @toggle-select="handleToggleSelect" @click="openDetail"></ss-list-card>
  4597. * ```
  4598. *
  4599. * Props:
  4600. * - `item`:卡片数据对象(建议含 `id/title/tags[]`;内部会读写 `item._ssSelected` 作为选中态)
  4601. *
  4602. * Emits:
  4603. * - `toggle-select`:点击角标触发,参数 `{ item, selected }`
  4604. * - `click`:点击卡片主体触发(用于打开详情等)
  4605. */
  4606. const SsListCard = {
  4607. name: "SsListCard",
  4608. props: {
  4609. ssObjName: { type: String, default: "" }, // 功能:业务对象名(用于默认缩略图 icon) by xu 20260109
  4610. cardClickAction: { type: String, default: "view" }, // 功能:卡片主体点击动作(view=查看;single=单选互斥) by xu 20260109
  4611. item: {
  4612. type: Object,
  4613. required: true,
  4614. },
  4615. },
  4616. emits: ["click", "change", "toggle-select"],
  4617. setup(props, { emit }) {
  4618. const item = props.item;
  4619. // 移除 itemWidth 计算属性,不再需要 by xu 20260105
  4620. // 判断卡片类型 by xu 20260105
  4621. const cardType = Vue.computed(() => {
  4622. // 支持“无图但保留缩略图占位”(同一业务列表图片形态一致) by xu 20260109
  4623. if (!item.thumb && !item.thumbType) return ""; // 业务列表“无图”形态
  4624. // 根据 thumbType 字段判断,如果没有则默认为证件照
  4625. return item.thumbType === "thumbnail" ? "card-thumbnail" : "card-photo";
  4626. });
  4627. // 判断状态类型 - 场地预定状态 by xu 20260105
  4628. const statusClass = Vue.computed(() => {
  4629. if (!item.status) return ""; // 无状态,默认白色
  4630. // 映射状态值到 CSS 类名
  4631. const statusMap = {
  4632. available: "status-available",
  4633. unavailable: "status-unavailable",
  4634. disabled: "status-disabled",
  4635. };
  4636. return statusMap[item.status] || "";
  4637. });
  4638. const onItemClick = (e) => {
  4639. // 清除所有类型卡片的 active 状态(卡片主体点击仅做 active 高亮) by xu 20260109
  4640. const allListCards = document.querySelectorAll(
  4641. ".knowledge-item-container"
  4642. );
  4643. const allFolderCards = document.querySelectorAll(".ss-folder-list");
  4644. allListCards.forEach((card) => card.classList.remove("active"));
  4645. allFolderCards.forEach((card) => card.classList.remove("active"));
  4646. // 设置当前项的 active 状态
  4647. e.currentTarget.classList.add("active");
  4648. // 卡片主体点击动作由页面级配置控制 by xu 20260109
  4649. if (props.cardClickAction === "view") {
  4650. props.item.onclick?.();
  4651. // 通知父级:卡片点击(查看) by xu 20260109
  4652. emit("click", props.item);
  4653. }
  4654. };
  4655. const onItemChange = (e, icon, index) => {
  4656. e.stopPropagation(); // 阻止事件冒泡到卡片
  4657. props.item.buttons[0].onclick();
  4658. // emit("change", { item: props.item, icon, index });
  4659. };
  4660. return {
  4661. item,
  4662. cardType,
  4663. statusClass,
  4664. onItemClick,
  4665. onItemChange,
  4666. };
  4667. },
  4668. data() {
  4669. return {
  4670. showButtons: false,
  4671. selected: false, // 选择状态 by xu 20260105
  4672. showTextPopover: false, // 功能:右侧文字区 hover 展示全量 by xu 20260108
  4673. textPopoverType: "", // second-summary / second-tags / third / third-full by xu 20260108
  4674. textPopoverBottom: 0, // 功能:popover 从当前省略行位置向上展开 by xu 20260108
  4675. hideTextPopoverTimer: null, // 功能:鼠标从省略行移到浮层的缓冲 by xu 20260108
  4676. textPopoverPayload: null, // { kind, text?, lines? } by xu 20260108
  4677. ellipsisVisible: {
  4678. // 功能:只在真实出现 ... 时才显示命中区/允许 goheight by xu 20260109
  4679. secondSummary: false,
  4680. secondTags: false,
  4681. third: false,
  4682. thirdFull: false,
  4683. },
  4684. };
  4685. },
  4686. methods: {
  4687. __allowSelect() {
  4688. // 功能说明:无 rbarObj 时禁用选中能力(隐藏右下角勾选并禁止 toggle) by xu 20260122
  4689. try {
  4690. if (this?.$root && this.$root.hasObjPanel === false) return false;
  4691. if (
  4692. typeof window !== "undefined" &&
  4693. window.__objListVm &&
  4694. window.__objListVm.hasObjPanel === false
  4695. )
  4696. return false;
  4697. } catch (_) {}
  4698. return true;
  4699. },
  4700. // 切换选择状态(对外 emit,支持方案A父级 state 中转) by xu 20260106
  4701. toggleSelect(e) {
  4702. e.stopPropagation();
  4703. if (!this.__allowSelect()) return; // 功能说明:无 rbarObj 时禁止选中 by xu 20260122
  4704. // 使用 item 上的状态,便于父级/右侧边栏反向同步 by xu 20260106
  4705. this.item._ssSelected = !this.item?._ssSelected;
  4706. this.$emit("toggle-select", {
  4707. item: this.item,
  4708. selected: !!this.item?._ssSelected,
  4709. });
  4710. },
  4711. // 卡片主体点击=单选互斥:只有“本次切到选中”才清理其他选中 by xu 20260109
  4712. toggleSelectExclusive(e) {
  4713. e?.stopPropagation?.();
  4714. if (!this.__allowSelect()) return; // 功能说明:无 rbarObj 时禁止选中 by xu 20260122
  4715. this.item._ssSelected = !this.item?._ssSelected;
  4716. const selected = !!this.item?._ssSelected;
  4717. this.$emit("toggle-select", {
  4718. item: this.item,
  4719. selected,
  4720. exclusive: true,
  4721. });
  4722. },
  4723. // 功能:无缩略图时,用业务对象 icon 做默认图(ss-icon + icon-obj-xx) by xu 20260109
  4724. getBizThumbIconClass() {
  4725. const name = String(
  4726. this.ssObjName ||
  4727. this.item?.ssObjName ||
  4728. this.$root?.ssObjName ||
  4729. window?.ss?.dom?.ssObjName ||
  4730. ""
  4731. ).trim();
  4732. if (!name) return "";
  4733. return `icon-obj-${name}`;
  4734. },
  4735. // 功能:构造右侧文字区 4 行(摘要/类目或标签/对象号) by xu 20260108
  4736. buildRightTextLines() {
  4737. const item = this.item || {};
  4738. const summary = String(item?.desc ?? "").trim(); // 后端字段后续映射 by xu 20260108
  4739. const objNum = String(item?.objNum ?? "").trim(); // 后端字段后续映射 by xu 20260108
  4740. const categoryArr = Array.isArray(item?.category) ? item.category : [];
  4741. const tagsArr = Array.isArray(item?.tags) ? item.tags : [];
  4742. const hasTags = tagsArr.length > 0;
  4743. const hasCategory = categoryArr.length > 0;
  4744. // 第二部分(L1-L3):摘要优先,其次物品参数 by xu 20260108
  4745. const secondKind = summary ? "summary" : hasTags ? "tags" : "";
  4746. const hasSecond = !!secondKind;
  4747. // 第三部分(L4):对象号优先,其次类目 by xu 20260108
  4748. const thirdKind = objNum ? "objNum" : hasCategory ? "category" : "";
  4749. const hasThird = !!thirdKind;
  4750. const thirdFull = !hasSecond && hasThird; // 第二部分为空则第三部分占满 4 行 by xu 20260108
  4751. const secondFull = hasSecond && !hasThird && secondKind === "tags"; // 仅标签时占满 4 行(不留空行) by xu 20260109
  4752. const toValueText = (obj) => {
  4753. // 功能说明:类目/物品参数回显展示 key: value(否则只显示值看不懂) by xu 20260114
  4754. const [k, v] = Object.entries(obj || {})[0] || ["", ""];
  4755. const key = k !== undefined && k !== null ? String(k).trim() : "";
  4756. const val = v !== undefined && v !== null ? String(v).trim() : "";
  4757. if (key && val) return `${key}:${val}`;
  4758. if (val) return val;
  4759. if (key) return key;
  4760. return "";
  4761. };
  4762. const toLineList = (arr) =>
  4763. (arr || [])
  4764. .map(toValueText)
  4765. .map((s) => String(s ?? "").trim())
  4766. .filter(Boolean);
  4767. const flat = (arr) => toLineList(arr).join(" ");
  4768. // 第二部分 tags:默认 3 行;仅标签时占满 4 行(避免底部空一行) by xu 20260109
  4769. const secondTagsMaxLines = secondFull ? 4 : 3;
  4770. const secondTagsLinesFull = toLineList(tagsArr);
  4771. const secondTagsHead = secondTagsLinesFull.slice(
  4772. 0,
  4773. Math.max(0, secondTagsMaxLines - 1)
  4774. );
  4775. const secondTagsTail = secondTagsLinesFull.slice(
  4776. Math.max(0, secondTagsMaxLines - 1)
  4777. );
  4778. const secondTagsLast =
  4779. secondTagsTail.length <= 1
  4780. ? secondTagsTail[0] || ""
  4781. : secondTagsTail.join(" "); // 功能:最后一行平铺剩余 by xu 20260108
  4782. // 第三部分类目:卡片上串成一行;goheight 展开时一条一行 by xu 20260109
  4783. const categoryLinesFull = toLineList(categoryArr);
  4784. const categoryLine = categoryLinesFull.join(" ");
  4785. const thirdText =
  4786. thirdKind === "objNum"
  4787. ? objNum
  4788. : thirdKind === "category"
  4789. ? categoryLine
  4790. : "";
  4791. return {
  4792. secondKind,
  4793. thirdKind,
  4794. hasSecond,
  4795. hasThird,
  4796. thirdFull,
  4797. secondFull,
  4798. summary,
  4799. secondTagsMaxLines,
  4800. secondTagsHead,
  4801. secondTagsLast,
  4802. secondTagsLinesFull,
  4803. objNum,
  4804. categoryLine,
  4805. categoryLinesFull,
  4806. thirdText,
  4807. };
  4808. },
  4809. measureTextOverflowByLines(text, maxLines, width) {
  4810. const w = Number(width) || 0;
  4811. if (!w || !text) return false;
  4812. const probe = document.createElement("div");
  4813. probe.style.position = "fixed";
  4814. probe.style.left = "-99999px";
  4815. probe.style.top = "0";
  4816. probe.style.width = w + "px";
  4817. probe.style.fontSize = "18px";
  4818. probe.style.lineHeight = "24px";
  4819. probe.style.whiteSpace = "normal";
  4820. probe.style.wordBreak = "break-word";
  4821. probe.style.visibility = "hidden";
  4822. probe.textContent = text;
  4823. document.body.appendChild(probe);
  4824. const h = probe.getBoundingClientRect().height || 0;
  4825. document.body.removeChild(probe);
  4826. return h > maxLines * 24 + 1;
  4827. },
  4828. measureSingleLineOverflow(text, width) {
  4829. const w = Number(width) || 0;
  4830. if (!w || !text) return false;
  4831. const probe = document.createElement("span");
  4832. probe.style.position = "fixed";
  4833. probe.style.left = "-99999px";
  4834. probe.style.top = "0";
  4835. probe.style.display = "inline-block";
  4836. probe.style.maxWidth = w + "px";
  4837. probe.style.fontSize = "18px";
  4838. probe.style.lineHeight = "24px";
  4839. probe.style.whiteSpace = "nowrap";
  4840. probe.style.visibility = "hidden";
  4841. probe.textContent = text;
  4842. document.body.appendChild(probe);
  4843. const overflow =
  4844. (probe.scrollWidth || 0) > (probe.clientWidth || w) + 1;
  4845. document.body.removeChild(probe);
  4846. return overflow;
  4847. },
  4848. // 功能:根据当前卡片宽度刷新「是否出现 ...」状态(用于控制命中区显示) by xu 20260109
  4849. refreshEllipsisVisible() {
  4850. try {
  4851. const right = this.$el?.querySelector?.(".right");
  4852. const rawWidth =
  4853. right?.getBoundingClientRect?.().width || right?.clientWidth || 0;
  4854. const width = Math.max(0, Math.round(rawWidth)); // 修复:内容区不再使用 padding-right 预留,测量按真实宽度 by xu 20260109
  4855. const model = this.buildRightTextLines();
  4856. const next = {
  4857. secondSummary: false,
  4858. secondTags: false,
  4859. third: false,
  4860. thirdFull: false,
  4861. };
  4862. if (model.secondKind === "summary" && model.summary) {
  4863. next.secondSummary = this.measureTextOverflowByLines(
  4864. model.summary,
  4865. 3,
  4866. width
  4867. );
  4868. }
  4869. if (model.secondKind === "tags") {
  4870. // 功能说明:tags 采用「最后一行平铺剩余」策略,是否出现 ... 仅取决于最后一行是否溢出(数量多但平铺放得下不算溢出) by xu 20260114
  4871. next.secondTags = this.measureSingleLineOverflow(
  4872. model.secondTagsLast,
  4873. width
  4874. );
  4875. }
  4876. if (model.hasThird && !model.thirdFull) {
  4877. next.third = this.measureSingleLineOverflow(model.thirdText, width);
  4878. }
  4879. if (model.hasThird && model.thirdFull) {
  4880. next.thirdFull = this.measureTextOverflowByLines(
  4881. model.thirdText,
  4882. 4,
  4883. width
  4884. );
  4885. }
  4886. const prev = this.ellipsisVisible || {};
  4887. const changed =
  4888. prev.secondSummary !== next.secondSummary ||
  4889. prev.secondTags !== next.secondTags ||
  4890. prev.third !== next.third ||
  4891. prev.thirdFull !== next.thirdFull;
  4892. if (changed) this.ellipsisVisible = next;
  4893. } catch (e) {
  4894. // ignore by xu 20260109
  4895. }
  4896. },
  4897. showTextPopoverFor(el, kind) {
  4898. // 调试开关:window.__SS_LISTCARD_DEBUG__ = true 时打印 hover/溢出判断日志 by xu 20260108
  4899. const debug =
  4900. typeof window !== "undefined" && !!window.__SS_LISTCARD_DEBUG__;
  4901. if (debug) {
  4902. console.log("[SsListCard] ellipsis hover", {
  4903. kind,
  4904. el: el?.className,
  4905. });
  4906. }
  4907. const model = this.buildRightTextLines();
  4908. const lineEl = el?.closest?.(".ss-card-text__line") || el;
  4909. const right = lineEl?.closest?.(".right") || el?.closest?.(".right");
  4910. const rawWidth =
  4911. right?.getBoundingClientRect?.().width || right?.clientWidth || 0;
  4912. const width = Math.max(0, Math.round(rawWidth)); // 修复:内容区不再预留 padding-right,测量按真实宽度 by xu 20260109
  4913. const textEl =
  4914. kind === "second-summary"
  4915. ? lineEl?.querySelector?.(".ss-card-text__secondSummary")
  4916. : kind === "second-tags"
  4917. ? lineEl?.querySelector?.(".ss-card-text__tagLineLast")
  4918. : kind === "third"
  4919. ? lineEl?.querySelector?.(".ss-card-text__thirdLine")
  4920. : lineEl?.querySelector?.(".ss-card-text__thirdFull");
  4921. let payload = null;
  4922. // 仅当真实会出现 ... 时才允许 goheight(避免“没超出也能出 goheight”) by xu 20260109
  4923. const overflowed =
  4924. kind === "second-summary"
  4925. ? this.measureTextOverflowByLines(model.summary, 3, width)
  4926. : kind === "second-tags"
  4927. ? this.measureSingleLineOverflow(model.secondTagsLast, width) // 功能说明:同 refreshEllipsisVisible,tags 仅看最后一行是否溢出 by xu 20260114
  4928. : kind === "third"
  4929. ? this.measureSingleLineOverflow(model.thirdText, width)
  4930. : this.measureTextOverflowByLines(model.thirdText, 4, width);
  4931. if (!overflowed) return;
  4932. if (kind === "second-summary") {
  4933. if (model.summary) payload = { kind, text: model.summary };
  4934. } else if (kind === "second-tags") {
  4935. if (
  4936. Array.isArray(model.secondTagsLinesFull) &&
  4937. model.secondTagsLinesFull.length
  4938. ) {
  4939. payload = { kind, lines: model.secondTagsLinesFull };
  4940. }
  4941. } else if (kind === "third") {
  4942. if (
  4943. model.thirdKind === "category" &&
  4944. Array.isArray(model.categoryLinesFull) &&
  4945. model.categoryLinesFull.length
  4946. ) {
  4947. payload = { kind, lines: model.categoryLinesFull }; // 功能:类目展开一条一行 by xu 20260109
  4948. } else if (model.thirdText) {
  4949. payload = { kind, text: model.thirdText };
  4950. }
  4951. } else if (kind === "third-full") {
  4952. if (
  4953. model.thirdKind === "category" &&
  4954. Array.isArray(model.categoryLinesFull) &&
  4955. model.categoryLinesFull.length
  4956. ) {
  4957. payload = { kind, lines: model.categoryLinesFull }; // 功能:类目占满模式展开一条一行 by xu 20260109
  4958. } else if (model.thirdText) {
  4959. payload = { kind, text: model.thirdText };
  4960. }
  4961. }
  4962. if (debug) {
  4963. console.log("[SsListCard] ellipsis decide", {
  4964. kind,
  4965. rawWidth: Math.round(rawWidth),
  4966. width,
  4967. hasPayload: !!payload,
  4968. textEl: textEl?.className,
  4969. textClient: textEl
  4970. ? {
  4971. cw: textEl.clientWidth,
  4972. ch: textEl.clientHeight,
  4973. sw: textEl.scrollWidth,
  4974. sh: textEl.scrollHeight,
  4975. }
  4976. : null,
  4977. });
  4978. }
  4979. if (!payload) return;
  4980. this.clearHideTextPopoverTimer();
  4981. const container = lineEl?.closest?.(".right");
  4982. const containerRect = container?.getBoundingClientRect?.();
  4983. const lineRect = lineEl?.getBoundingClientRect?.();
  4984. if (containerRect && lineRect) {
  4985. const bottom = Math.max(
  4986. 0,
  4987. Math.round(containerRect.bottom - lineRect.bottom)
  4988. );
  4989. this.textPopoverBottom = bottom;
  4990. } else {
  4991. this.textPopoverBottom = 0;
  4992. }
  4993. this.textPopoverPayload = payload;
  4994. this.textPopoverType = kind;
  4995. this.showTextPopover = true;
  4996. if (debug) console.log("[SsListCard] goheight show", payload);
  4997. },
  4998. isOverflowing(el) {
  4999. if (!el) return false;
  5000. // 单行/多行省略统一判断:scroll 尺寸大于 client 尺寸即认为有 ... by xu 20260108
  5001. return (
  5002. (el.scrollWidth &&
  5003. el.clientWidth &&
  5004. el.scrollWidth > el.clientWidth + 1) ||
  5005. (el.scrollHeight &&
  5006. el.clientHeight &&
  5007. el.scrollHeight > el.clientHeight + 1)
  5008. );
  5009. },
  5010. isSummaryOverflowing(el) {
  5011. if (!el) return false;
  5012. // -webkit-line-clamp 场景下 scrollHeight 不稳定,改用“无 clamp 的离屏测量”判断是否超过 2 行 by xu 20260108
  5013. const text = String(this.item?.desc ?? "").trim();
  5014. if (!text) return false;
  5015. const rect = el.getBoundingClientRect?.();
  5016. const width = rect?.width || el.clientWidth || 0;
  5017. if (!width) return false;
  5018. const probe = document.createElement("div");
  5019. probe.style.position = "fixed";
  5020. probe.style.left = "-99999px";
  5021. probe.style.top = "0";
  5022. probe.style.width = width + "px";
  5023. probe.style.fontSize = "18px";
  5024. probe.style.lineHeight = "24px";
  5025. probe.style.whiteSpace = "normal";
  5026. probe.style.wordBreak = "break-word";
  5027. probe.style.visibility = "hidden";
  5028. probe.textContent = text;
  5029. document.body.appendChild(probe);
  5030. const h = probe.getBoundingClientRect().height || 0;
  5031. document.body.removeChild(probe);
  5032. return h > 48 + 1;
  5033. },
  5034. clearHideTextPopoverTimer() {
  5035. if (this.hideTextPopoverTimer) {
  5036. clearTimeout(this.hideTextPopoverTimer);
  5037. this.hideTextPopoverTimer = null;
  5038. }
  5039. },
  5040. // 修复 goheight hover 无响应:移除重复方法覆盖,统一使用上面的 showTextPopoverFor(el, kind) by xu 20260109
  5041. hideTextPopoverLater() {
  5042. this.clearHideTextPopoverTimer();
  5043. this.hideTextPopoverTimer = setTimeout(() => {
  5044. this.showTextPopover = false;
  5045. this.textPopoverType = "";
  5046. this.textPopoverPayload = null;
  5047. }, 120);
  5048. },
  5049. hideTextPopover() {
  5050. this.clearHideTextPopoverTimer();
  5051. this.showTextPopover = false;
  5052. this.textPopoverType = "";
  5053. this.textPopoverPayload = null;
  5054. },
  5055. // 功能:新需求下不在 updated 内做测量,避免死循环 by xu 20260108
  5056. },
  5057. mounted() {
  5058. // 无需在 mounted/updated 里做 overflow 测量(避免死循环),只在 hover 触发时判断 by xu 20260108
  5059. // 仅用于控制“...命中区是否显示”,不会触发循环更新 by xu 20260109
  5060. this.$nextTick?.(() => {
  5061. requestAnimationFrame(() => this.refreshEllipsisVisible?.());
  5062. });
  5063. this.__ssListCardResizeHandler = () => this.refreshEllipsisVisible?.(); // 功能:窗口变化时刷新 ... 显示 by xu 20260109
  5064. window.addEventListener?.("resize", this.__ssListCardResizeHandler);
  5065. },
  5066. updated() {
  5067. // 卡片数据更新后刷新一次 ... 显示状态(避免“宽度/内容变了但命中区不变”) by xu 20260109
  5068. this.$nextTick?.(() => {
  5069. requestAnimationFrame(() => this.refreshEllipsisVisible?.());
  5070. });
  5071. },
  5072. beforeUnmount() {
  5073. // 清理 timer,避免残留导致异常 by xu 20260108
  5074. this.clearHideTextPopoverTimer?.();
  5075. if (this.__ssListCardResizeHandler) {
  5076. window.removeEventListener?.("resize", this.__ssListCardResizeHandler);
  5077. this.__ssListCardResizeHandler = null;
  5078. }
  5079. },
  5080. render() {
  5081. const SsCartListIcon = Vue.resolveComponent("ss-cart-list-icon");
  5082. const SsIcon = Vue.resolveComponent("ss-icon");
  5083. const hasThumbArea = !!(this.item?.thumb || this.item?.thumbType); // 功能:无图但有 thumbType 时仍保留占位 by xu 20260109
  5084. const thumbSrc = (() => {
  5085. // 功能说明:兼容 thumb 为 raw path 或 {val}/{value},组件内统一转为 dlByHttp URL by xu 20260122
  5086. const t = this.item?.thumb;
  5087. if (!t) return "";
  5088. if (typeof t === "string") {
  5089. const s = t.trim();
  5090. if (!s) return "";
  5091. // 已经是 URL/绝对路径则直接使用;否则按 path 构建 dlByHttp by xu 20260122
  5092. if (
  5093. /^https?:\/\//i.test(s) ||
  5094. s.startsWith("/service?") ||
  5095. s.startsWith("/")
  5096. )
  5097. return s;
  5098. return buildThumbUrl(s);
  5099. }
  5100. return buildThumbUrl(t);
  5101. })();
  5102. return Vue.h(
  5103. "div",
  5104. {
  5105. class: {
  5106. "knowledge-item-container": true,
  5107. active: this.item.active,
  5108. [this.cardType]: !!this.cardType, // 动态添加卡片类型类名 by xu 20260105
  5109. [this.statusClass]: !!this.statusClass,
  5110. },
  5111. onClick: (e) => {
  5112. this.onItemClick?.(e);
  5113. if (this.__allowSelect() && this.cardClickAction === "single") {
  5114. // 功能说明:无 rbarObj 时不允许单选互斥 by xu 20260122
  5115. this.toggleSelectExclusive?.(e);
  5116. }
  5117. }, // 功能:卡片主体点击动作(view/single) by xu 20260109
  5118. // 移除固定宽度,由 CSS min-width 控制 by xu 20260105
  5119. },
  5120. [
  5121. // 右上角状态图标区域 by xu 20260105
  5122. this.item?.statusIcons?.length > 0 &&
  5123. Vue.h(
  5124. "div",
  5125. { class: "card-status-icons" },
  5126. this.item.statusIcons.map((icon) =>
  5127. Vue.h(SsIcon, {
  5128. class: `status-icon ${icon.class}`,
  5129. title: icon.title,
  5130. })
  5131. )
  5132. ),
  5133. this.item?.buttons?.length > 0 &&
  5134. Vue.h(
  5135. "div",
  5136. {
  5137. class: "header",
  5138. style:
  5139. this.item?.statusIcons?.length > 0
  5140. ? {
  5141. right: `${this.item.statusIcons.length * 48}px`,
  5142. borderTopRightRadius: "0",
  5143. }
  5144. : {},
  5145. onMouseenter: () => (this.showButtons = true),
  5146. onMouseleave: () => (this.showButtons = false),
  5147. onClick: (e) => this.onItemChange(e, this.item.buttons[0], 0),
  5148. },
  5149. [
  5150. // 只在有按钮时渲染设置图标
  5151. // this.item?.buttons?.length > 0 &&
  5152. Vue.h("div", {
  5153. class: "cart-list-setting cart-list-icon",
  5154. title: this.item?.buttons?.[0]?.title,
  5155. }),
  5156. // 鼠标移入时显示按钮列表,与图标同级
  5157. // this.item?.buttons?.length > 0 &&
  5158. this.showButtons &&
  5159. this.item?.buttons?.length > 1 &&
  5160. Vue.h(
  5161. "div",
  5162. {
  5163. class: "cart-list-button-popup",
  5164. },
  5165. this.item.buttons.map((btn) =>
  5166. Vue.h(
  5167. "div",
  5168. {
  5169. onClick: (e) => {
  5170. e.stopPropagation();
  5171. btn.onclick?.();
  5172. },
  5173. },
  5174. [
  5175. // 如果有 class,显示对应的图标
  5176. btn.class &&
  5177. Vue.h(SsCartListIcon, {
  5178. class: [btn.class],
  5179. }),
  5180. // 显示按钮文本
  5181. Vue.h("span", null, btn.title),
  5182. ]
  5183. )
  5184. )
  5185. ),
  5186. ]
  5187. ),
  5188. Vue.h("div", { class: "body" }, [
  5189. Vue.h("div", { class: "box-header" }, [
  5190. Vue.h("div", `${this.item.title}`),
  5191. ]),
  5192. Vue.h(
  5193. "div",
  5194. {
  5195. class: !hasThumbArea ? "no-thumb box-body" : "box-body",
  5196. },
  5197. [
  5198. hasThumbArea
  5199. ? thumbSrc
  5200. ? Vue.h("div", { class: "left" }, [
  5201. Vue.h("img", {
  5202. src: thumbSrc,
  5203. alt: "Thumbnail",
  5204. class: "imgUnHandle",
  5205. style: {
  5206. "object-fit": "cover",
  5207. width: "100%",
  5208. height: "100%",
  5209. },
  5210. }),
  5211. ])
  5212. : Vue.h(
  5213. // 功能:无图占位(ss-icon + biz icon,居中) by xu 20260109
  5214. "div",
  5215. { class: "left ss-objlist-thumbPlaceholder" },
  5216. [
  5217. Vue.h(SsIcon, {
  5218. class: `${this.getBizThumbIconClass()} ss-objlist-thumbIcon`,
  5219. }),
  5220. ]
  5221. )
  5222. : null,
  5223. Vue.h(
  5224. "div",
  5225. {
  5226. class: "right",
  5227. },
  5228. (() => {
  5229. const model = this.buildRightTextLines(); // 功能:右侧文字区新规则(第二部分/第三部分优先级) by xu 20260108
  5230. const hasAny = !!(model?.hasSecond || model?.hasThird);
  5231. if (!hasAny) return [];
  5232. const children = [];
  5233. // 第二部分:L1-L3(摘要优先,其次 tags;不足留空;超出 L3 ...) by xu 20260108
  5234. if (model.hasSecond) {
  5235. if (model.secondKind === "summary") {
  5236. children.push(
  5237. Vue.h(
  5238. "div",
  5239. {
  5240. class:
  5241. "ss-card-text__line ss-card-text__secondBlock",
  5242. },
  5243. [
  5244. Vue.h(
  5245. "div",
  5246. {
  5247. class: "ss-card-text__secondSummary",
  5248. title: model.summary,
  5249. },
  5250. model.summary
  5251. ),
  5252. Vue.h("span", {
  5253. class: [
  5254. "ss-card-text__ellipsisHit",
  5255. "ss-card-text__ellipsisHit--second",
  5256. this.ellipsisVisible?.secondSummary
  5257. ? "is-on"
  5258. : "",
  5259. ],
  5260. title: "查看完整摘要",
  5261. onMouseenter: (e) =>
  5262. this.showTextPopoverFor(
  5263. e?.currentTarget,
  5264. "second-summary"
  5265. ),
  5266. onClick: (e) => {
  5267. e?.stopPropagation?.();
  5268. this.showTextPopoverFor(
  5269. e?.currentTarget,
  5270. "second-summary"
  5271. );
  5272. }, // 功能:点击 ... 也可展开(避免 hover 受遮挡) by xu 20260109
  5273. onMouseleave: () => this.hideTextPopoverLater(),
  5274. }),
  5275. ]
  5276. )
  5277. );
  5278. } else if (model.secondKind === "tags") {
  5279. children.push(
  5280. Vue.h(
  5281. "div",
  5282. {
  5283. class: [
  5284. "ss-card-text__line",
  5285. model.secondFull
  5286. ? "ss-card-text__secondFullBlock"
  5287. : "ss-card-text__secondBlock",
  5288. ],
  5289. },
  5290. [
  5291. // 功能:仅标签时占满 4 行 by xu 20260109
  5292. Vue.h(
  5293. "div",
  5294. { class: "ss-card-text__secondTags" },
  5295. [
  5296. ...model.secondTagsHead.map((t) =>
  5297. Vue.h(
  5298. "div",
  5299. {
  5300. class: "ss-card-text__tagLine",
  5301. title: t,
  5302. },
  5303. t
  5304. )
  5305. ),
  5306. // 第三行:平铺剩余(可能为空) by xu 20260108
  5307. Vue.h(
  5308. "div",
  5309. {
  5310. class:
  5311. "ss-card-text__tagLine is-last ss-card-text__tagLineLast",
  5312. title: model.secondTagsLast,
  5313. },
  5314. model.secondTagsLast
  5315. ),
  5316. ].filter(Boolean)
  5317. ),
  5318. // 只在最后一行出现 ... 时才触发 goheight by xu 20260108
  5319. Vue.h("span", {
  5320. class: [
  5321. "ss-card-text__ellipsisHit",
  5322. "ss-card-text__ellipsisHit--second",
  5323. this.ellipsisVisible?.secondTags
  5324. ? "is-on"
  5325. : "",
  5326. ],
  5327. title: "查看完整物品参数",
  5328. onMouseenter: (e) =>
  5329. this.showTextPopoverFor(
  5330. e?.currentTarget,
  5331. "second-tags"
  5332. ),
  5333. onClick: (e) => {
  5334. e?.stopPropagation?.();
  5335. this.showTextPopoverFor(
  5336. e?.currentTarget,
  5337. "second-tags"
  5338. );
  5339. }, // 功能:点击 ... 也可展开(避免 hover 受遮挡) by xu 20260109
  5340. onMouseleave: () => this.hideTextPopoverLater(),
  5341. }),
  5342. ]
  5343. )
  5344. );
  5345. }
  5346. }
  5347. // 第三部分:默认 L4;第二部分为空则占满 L1-L4 by xu 20260108
  5348. if (model.hasThird) {
  5349. if (model.thirdFull) {
  5350. children.push(
  5351. Vue.h(
  5352. "div",
  5353. {
  5354. class:
  5355. "ss-card-text__line ss-card-text__thirdFullBlock",
  5356. },
  5357. [
  5358. Vue.h(
  5359. "div",
  5360. {
  5361. class: "ss-card-text__thirdFull",
  5362. title: model.thirdText,
  5363. },
  5364. model.thirdText
  5365. ),
  5366. Vue.h("span", {
  5367. class: [
  5368. "ss-card-text__ellipsisHit",
  5369. "ss-card-text__ellipsisHit--third",
  5370. this.ellipsisVisible?.thirdFull
  5371. ? "is-on"
  5372. : "",
  5373. ],
  5374. title: "查看完整信息",
  5375. onMouseenter: (e) =>
  5376. this.showTextPopoverFor(
  5377. e?.currentTarget,
  5378. "third-full"
  5379. ),
  5380. onClick: (e) => {
  5381. e?.stopPropagation?.();
  5382. this.showTextPopoverFor(
  5383. e?.currentTarget,
  5384. "third-full"
  5385. );
  5386. }, // 功能:点击 ... 也可展开(避免 hover 受遮挡) by xu 20260109
  5387. onMouseleave: () => this.hideTextPopoverLater(),
  5388. }),
  5389. ]
  5390. )
  5391. );
  5392. } else {
  5393. children.push(
  5394. Vue.h(
  5395. "div",
  5396. {
  5397. class:
  5398. "ss-card-text__line ss-card-text__thirdLineWrap",
  5399. },
  5400. [
  5401. Vue.h(
  5402. "div",
  5403. {
  5404. class: "ss-card-text__thirdLine",
  5405. title: model.thirdText,
  5406. },
  5407. model.thirdText
  5408. ),
  5409. Vue.h("span", {
  5410. class: [
  5411. "ss-card-text__ellipsisHit",
  5412. "ss-card-text__ellipsisHit--third",
  5413. this.ellipsisVisible?.third ? "is-on" : "",
  5414. ],
  5415. title: "查看完整信息",
  5416. onMouseenter: (e) =>
  5417. this.showTextPopoverFor(
  5418. e?.currentTarget,
  5419. "third"
  5420. ),
  5421. onClick: (e) => {
  5422. e?.stopPropagation?.();
  5423. this.showTextPopoverFor(
  5424. e?.currentTarget,
  5425. "third"
  5426. );
  5427. }, // 功能:点击 ... 也可展开(避免 hover 受遮挡) by xu 20260109
  5428. onMouseleave: () => this.hideTextPopoverLater(),
  5429. }),
  5430. ]
  5431. )
  5432. );
  5433. }
  5434. }
  5435. // hover 展开浮层:宽度=右侧文字区,底对齐向上展开,带阴影 by xu 20260108
  5436. // popover 作为 `.right` 的 sibling 渲染,避免被 `.ss-card-text{overflow:hidden}` 裁剪 by xu 20260108
  5437. const popover =
  5438. this.showTextPopover &&
  5439. Vue.h(
  5440. "div",
  5441. {
  5442. class: "ss-card-text-popover",
  5443. style: { bottom: this.textPopoverBottom + "px" },
  5444. onMouseenter: () => {
  5445. this.clearHideTextPopoverTimer();
  5446. this.showTextPopover = true;
  5447. },
  5448. onMouseleave: () => this.hideTextPopoverLater(),
  5449. },
  5450. (() => {
  5451. const p = this.textPopoverPayload || {};
  5452. if (p.kind === "second-summary" && p.text) {
  5453. return [
  5454. Vue.h(
  5455. "div",
  5456. { class: "ss-card-text-popover__summary" },
  5457. p.text
  5458. ),
  5459. ];
  5460. }
  5461. if (Array.isArray(p.lines)) {
  5462. return [
  5463. Vue.h(
  5464. "div",
  5465. { class: "ss-card-text-popover__kvlist" },
  5466. p.lines.map((t) =>
  5467. Vue.h(
  5468. "div",
  5469. { class: "ss-card-text-popover__kv" },
  5470. t
  5471. )
  5472. )
  5473. ),
  5474. ];
  5475. }
  5476. if (
  5477. (p.kind === "third" || p.kind === "third-full") &&
  5478. p.text
  5479. ) {
  5480. return [
  5481. Vue.h(
  5482. "div",
  5483. { class: "ss-card-text-popover__objno" },
  5484. p.text
  5485. ),
  5486. ];
  5487. }
  5488. return [];
  5489. })()
  5490. );
  5491. return [
  5492. Vue.h("div", { class: "ss-card-text" }, children),
  5493. popover,
  5494. ];
  5495. })()
  5496. ),
  5497. ]
  5498. ),
  5499. ]),
  5500. // 右下角卡片选择图标 by xu 20260105
  5501. this.__allowSelect()
  5502. ? Vue.h(SsIcon, {
  5503. class: this.item?._ssSelected
  5504. ? "card-icon icon-cardChk-on"
  5505. : "card-icon icon-cardChk",
  5506. onClick: this.toggleSelect,
  5507. })
  5508. : null,
  5509. // 选中后底部线条 by xu 20260105
  5510. this.__allowSelect() &&
  5511. this.item?._ssSelected &&
  5512. Vue.h("div", { class: "select-bottom-line" }),
  5513. ]
  5514. );
  5515. },
  5516. };
  5517. // 二级对象卡片:复用一级对象新卡片布局/省略浮层,但去掉勾选与 single 选中,仅支持点击查看 by xu 20260115
  5518. const SsCObjCardList = {
  5519. name: "SsCObjCardList",
  5520. props: {
  5521. ssObjName: { type: String, default: "" }, // 功能说明:业务对象名(用于默认缩略图 icon) by xu 20260115
  5522. item: {
  5523. type: Object,
  5524. required: true,
  5525. },
  5526. },
  5527. emits: ["click", "change"],
  5528. setup(props, { emit }) {
  5529. const item = props.item;
  5530. const cardType = Vue.computed(() => {
  5531. if (!item.thumb && !item.thumbType) return "";
  5532. return item.thumbType === "thumbnail" ? "card-thumbnail" : "card-photo";
  5533. });
  5534. const statusClass = Vue.computed(() => {
  5535. if (!item.status) return "";
  5536. const statusMap = {
  5537. available: "status-available",
  5538. unavailable: "status-unavailable",
  5539. disabled: "status-disabled",
  5540. };
  5541. return statusMap[item.status] || "";
  5542. });
  5543. const onItemClick = (e) => {
  5544. // 清除所有类型卡片的 active 状态(保持与一级对象一致) by xu 20260115
  5545. const allListCards = document.querySelectorAll(
  5546. ".knowledge-item-container"
  5547. );
  5548. const allFolderCards = document.querySelectorAll(".ss-folder-list");
  5549. allListCards.forEach((card) => card.classList.remove("active"));
  5550. allFolderCards.forEach((card) => card.classList.remove("active"));
  5551. e.currentTarget.classList.add("active");
  5552. // 二级对象卡片:点击仅查看(调用 item.onclick) by xu 20260115
  5553. props.item.onclick?.();
  5554. emit("click", props.item);
  5555. };
  5556. const onItemChange = (e) => {
  5557. e.stopPropagation();
  5558. props.item.buttons?.[0]?.onclick?.();
  5559. };
  5560. return { item, cardType, statusClass, onItemClick, onItemChange };
  5561. },
  5562. data() {
  5563. return {
  5564. showButtons: false,
  5565. showTextPopover: false, // 功能:右侧文字区 hover 展示全量 by xu 20260115
  5566. textPopoverType: "", // second-summary / second-tags / third / third-full by xu 20260115
  5567. textPopoverBottom: 0, // 功能:popover 从当前省略行位置向上展开 by xu 20260115
  5568. hideTextPopoverTimer: null, // 功能:鼠标从省略行移到浮层的缓冲 by xu 20260115
  5569. textPopoverPayload: null, // { kind, text?, lines? } by xu 20260115
  5570. ellipsisVisible: {
  5571. secondSummary: false,
  5572. secondTags: false,
  5573. third: false,
  5574. thirdFull: false,
  5575. }, // 功能:只在真实出现 ... 时才显示命中区/允许 goheight by xu 20260115
  5576. };
  5577. },
  5578. methods: {
  5579. getBizThumbIconClass() {
  5580. // 功能:无缩略图时,用业务对象 icon 做默认图(ss-icon + icon-obj-xx) by xu 20260115
  5581. const name = String(
  5582. this.ssObjName ||
  5583. this.item?.ssObjName ||
  5584. this.$root?.ssObjName ||
  5585. window?.ss?.dom?.ssObjName ||
  5586. ""
  5587. ).trim();
  5588. if (!name) return "";
  5589. return "icon-obj-" + name;
  5590. },
  5591. buildRightTextLines() {
  5592. // 功能:沿用一级对象卡片右侧文字区规则 by xu 20260115
  5593. const item = this.item || {};
  5594. const summary = String(item?.desc ?? "").trim();
  5595. const objNum = String(item?.objNum ?? "").trim();
  5596. const categoryArr = Array.isArray(item?.category) ? item.category : [];
  5597. const tagsArr = Array.isArray(item?.tags) ? item.tags : [];
  5598. const hasTags = tagsArr.length > 0;
  5599. const hasCategory = categoryArr.length > 0;
  5600. const secondKind = summary ? "summary" : hasTags ? "tags" : "";
  5601. const hasSecond = !!secondKind;
  5602. const thirdKind = objNum ? "objNum" : hasCategory ? "category" : "";
  5603. const hasThird = !!thirdKind;
  5604. const thirdFull = !hasSecond && hasThird;
  5605. const secondFull = hasSecond && !hasThird && secondKind === "tags";
  5606. const toValueText = (obj) => {
  5607. // 功能说明:类目/物品参数回显展示 key: value(否则只显示值看不懂) by xu 20260115
  5608. const [k, v] = Object.entries(obj || {})[0] || ["", ""];
  5609. const key = k !== undefined && k !== null ? String(k).trim() : "";
  5610. const val = v !== undefined && v !== null ? String(v).trim() : "";
  5611. if (key && val) return key + ":" + val;
  5612. if (val) return val;
  5613. if (key) return key;
  5614. return "";
  5615. };
  5616. const toLineList = (arr) =>
  5617. (arr || [])
  5618. .map(toValueText)
  5619. .map((s) => String(s ?? "").trim())
  5620. .filter(Boolean);
  5621. const secondTagsMaxLines = secondFull ? 4 : 3;
  5622. const secondTagsLinesFull = toLineList(tagsArr);
  5623. const secondTagsHead = secondTagsLinesFull.slice(
  5624. 0,
  5625. Math.max(0, secondTagsMaxLines - 1)
  5626. );
  5627. const secondTagsTail = secondTagsLinesFull.slice(
  5628. Math.max(0, secondTagsMaxLines - 1)
  5629. );
  5630. const secondTagsLast =
  5631. secondTagsTail.length <= 1
  5632. ? secondTagsTail[0] || ""
  5633. : secondTagsTail.join(" ");
  5634. const categoryLinesFull = toLineList(categoryArr);
  5635. const categoryLine = categoryLinesFull.join(" ");
  5636. const thirdText =
  5637. thirdKind === "objNum"
  5638. ? objNum
  5639. : thirdKind === "category"
  5640. ? categoryLine
  5641. : "";
  5642. return {
  5643. secondKind,
  5644. thirdKind,
  5645. hasSecond,
  5646. hasThird,
  5647. thirdFull,
  5648. secondFull,
  5649. summary,
  5650. secondTagsMaxLines,
  5651. secondTagsHead,
  5652. secondTagsLast,
  5653. secondTagsLinesFull,
  5654. objNum,
  5655. categoryLine,
  5656. categoryLinesFull,
  5657. thirdText,
  5658. };
  5659. },
  5660. measureTextOverflowByLines(text, maxLines, width) {
  5661. const w = Number(width) || 0;
  5662. if (!w || !text) return false;
  5663. const probe = document.createElement("div");
  5664. probe.style.position = "fixed";
  5665. probe.style.left = "-99999px";
  5666. probe.style.top = "0";
  5667. probe.style.width = w + "px";
  5668. probe.style.fontSize = "18px";
  5669. probe.style.lineHeight = "24px";
  5670. probe.style.whiteSpace = "normal";
  5671. probe.style.wordBreak = "break-word";
  5672. probe.style.visibility = "hidden";
  5673. probe.textContent = text;
  5674. document.body.appendChild(probe);
  5675. const h = probe.getBoundingClientRect().height || 0;
  5676. document.body.removeChild(probe);
  5677. return h > maxLines * 24 + 1;
  5678. },
  5679. measureSingleLineOverflow(text, width) {
  5680. const w = Number(width) || 0;
  5681. if (!w || !text) return false;
  5682. const probe = document.createElement("span");
  5683. probe.style.position = "fixed";
  5684. probe.style.left = "-99999px";
  5685. probe.style.top = "0";
  5686. probe.style.display = "inline-block";
  5687. probe.style.maxWidth = w + "px";
  5688. probe.style.fontSize = "18px";
  5689. probe.style.lineHeight = "24px";
  5690. probe.style.whiteSpace = "nowrap";
  5691. probe.style.visibility = "hidden";
  5692. probe.textContent = text;
  5693. document.body.appendChild(probe);
  5694. const overflow =
  5695. (probe.scrollWidth || 0) > (probe.clientWidth || w) + 1;
  5696. document.body.removeChild(probe);
  5697. return overflow;
  5698. },
  5699. refreshEllipsisVisible() {
  5700. // 功能:刷新「是否出现 ...」状态(用于控制命中区显示) by xu 20260115
  5701. try {
  5702. const right = this.$el?.querySelector?.(".right");
  5703. const rawWidth =
  5704. right?.getBoundingClientRect?.().width || right?.clientWidth || 0;
  5705. const width = Math.max(0, Math.round(rawWidth));
  5706. const model = this.buildRightTextLines();
  5707. const next = {
  5708. secondSummary: false,
  5709. secondTags: false,
  5710. third: false,
  5711. thirdFull: false,
  5712. };
  5713. if (model.secondKind === "summary" && model.summary) {
  5714. next.secondSummary = this.measureTextOverflowByLines(
  5715. model.summary,
  5716. 3,
  5717. width
  5718. );
  5719. }
  5720. if (model.secondKind === "tags") {
  5721. next.secondTags = this.measureSingleLineOverflow(
  5722. model.secondTagsLast,
  5723. width
  5724. ); // 功能说明:tags 仅看最后一行是否溢出 by xu 20260115
  5725. }
  5726. if (model.hasThird && !model.thirdFull) {
  5727. next.third = this.measureSingleLineOverflow(model.thirdText, width);
  5728. }
  5729. if (model.hasThird && model.thirdFull) {
  5730. next.thirdFull = this.measureTextOverflowByLines(
  5731. model.thirdText,
  5732. 4,
  5733. width
  5734. );
  5735. }
  5736. const prev = this.ellipsisVisible || {};
  5737. const changed =
  5738. prev.secondSummary !== next.secondSummary ||
  5739. prev.secondTags !== next.secondTags ||
  5740. prev.third !== next.third ||
  5741. prev.thirdFull !== next.thirdFull;
  5742. if (changed) this.ellipsisVisible = next;
  5743. } catch (e) {
  5744. // ignore by xu 20260115
  5745. }
  5746. },
  5747. clearHideTextPopoverTimer() {
  5748. if (this.hideTextPopoverTimer) {
  5749. clearTimeout(this.hideTextPopoverTimer);
  5750. this.hideTextPopoverTimer = null;
  5751. }
  5752. },
  5753. hideTextPopoverLater() {
  5754. this.clearHideTextPopoverTimer();
  5755. this.hideTextPopoverTimer = setTimeout(() => {
  5756. this.showTextPopover = false;
  5757. this.textPopoverType = "";
  5758. this.textPopoverPayload = null;
  5759. }, 120);
  5760. },
  5761. showTextPopoverFor(el, kind) {
  5762. const model = this.buildRightTextLines();
  5763. const lineEl = el?.closest?.(".ss-card-text__line") || el;
  5764. const right = lineEl?.closest?.(".right") || el?.closest?.(".right");
  5765. const rawWidth =
  5766. right?.getBoundingClientRect?.().width || right?.clientWidth || 0;
  5767. const width = Math.max(0, Math.round(rawWidth));
  5768. const overflowed =
  5769. kind === "second-summary"
  5770. ? this.measureTextOverflowByLines(model.summary, 3, width)
  5771. : kind === "second-tags"
  5772. ? this.measureSingleLineOverflow(model.secondTagsLast, width)
  5773. : kind === "third"
  5774. ? this.measureSingleLineOverflow(model.thirdText, width)
  5775. : this.measureTextOverflowByLines(model.thirdText, 4, width);
  5776. if (!overflowed) return;
  5777. let payload = null;
  5778. if (kind === "second-summary") {
  5779. if (model.summary) payload = { kind, text: model.summary };
  5780. } else if (kind === "second-tags") {
  5781. if (
  5782. Array.isArray(model.secondTagsLinesFull) &&
  5783. model.secondTagsLinesFull.length
  5784. ) {
  5785. payload = { kind, lines: model.secondTagsLinesFull };
  5786. }
  5787. } else if (kind === "third") {
  5788. if (
  5789. model.thirdKind === "category" &&
  5790. Array.isArray(model.categoryLinesFull) &&
  5791. model.categoryLinesFull.length
  5792. ) {
  5793. payload = { kind, lines: model.categoryLinesFull };
  5794. } else if (model.thirdText) {
  5795. payload = { kind, text: model.thirdText };
  5796. }
  5797. } else if (kind === "third-full") {
  5798. if (
  5799. model.thirdKind === "category" &&
  5800. Array.isArray(model.categoryLinesFull) &&
  5801. model.categoryLinesFull.length
  5802. ) {
  5803. payload = { kind, lines: model.categoryLinesFull };
  5804. } else if (model.thirdText) {
  5805. payload = { kind, text: model.thirdText };
  5806. }
  5807. }
  5808. if (!payload) return;
  5809. this.clearHideTextPopoverTimer();
  5810. const container = lineEl?.closest?.(".right");
  5811. const containerRect = container?.getBoundingClientRect?.();
  5812. const lineRect = lineEl?.getBoundingClientRect?.();
  5813. if (containerRect && lineRect) {
  5814. const bottom = Math.max(
  5815. 0,
  5816. Math.round(containerRect.bottom - lineRect.bottom)
  5817. );
  5818. this.textPopoverBottom = bottom;
  5819. } else {
  5820. this.textPopoverBottom = 0;
  5821. }
  5822. this.textPopoverPayload = payload;
  5823. this.textPopoverType = kind;
  5824. this.showTextPopover = true;
  5825. },
  5826. },
  5827. mounted() {
  5828. this.$nextTick?.(() => {
  5829. requestAnimationFrame(() => this.refreshEllipsisVisible?.());
  5830. });
  5831. this.__ssCObjCardResizeHandler = () => this.refreshEllipsisVisible?.(); // 功能说明:窗口变化时刷新 ... 显示 by xu 20260115
  5832. window.addEventListener?.("resize", this.__ssCObjCardResizeHandler);
  5833. },
  5834. updated() {
  5835. this.$nextTick?.(() => {
  5836. requestAnimationFrame(() => this.refreshEllipsisVisible?.());
  5837. });
  5838. },
  5839. beforeUnmount() {
  5840. this.clearHideTextPopoverTimer?.();
  5841. if (this.__ssCObjCardResizeHandler) {
  5842. window.removeEventListener?.("resize", this.__ssCObjCardResizeHandler);
  5843. this.__ssCObjCardResizeHandler = null;
  5844. }
  5845. },
  5846. render() {
  5847. const SsCartListIcon = Vue.resolveComponent("ss-cart-list-icon");
  5848. const SsIcon = Vue.resolveComponent("ss-icon");
  5849. const hasThumbArea = !!(this.item?.thumb || this.item?.thumbType);
  5850. const thumbSrc = (() => {
  5851. // 功能说明:兼容 thumb 为 raw path 或 {val}/{value},组件内统一转为 dlByHttp URL by xu 20260122
  5852. const t = this.item?.thumb;
  5853. if (!t) return "";
  5854. if (typeof t === "string") {
  5855. const s = t.trim();
  5856. if (!s) return "";
  5857. if (
  5858. /^https?:\/\//i.test(s) ||
  5859. s.startsWith("/service?") ||
  5860. s.startsWith("/")
  5861. )
  5862. return s;
  5863. return buildThumbUrl(s);
  5864. }
  5865. return buildThumbUrl(t);
  5866. })();
  5867. return Vue.h(
  5868. "div",
  5869. {
  5870. class: {
  5871. "knowledge-item-container": true,
  5872. active: this.item.active,
  5873. [this.cardType]: !!this.cardType,
  5874. [this.statusClass]: !!this.statusClass,
  5875. },
  5876. onClick: (e) => this.onItemClick?.(e), // 功能说明:二级对象卡片点击仅查看 by xu 20260115
  5877. },
  5878. [
  5879. this.item?.statusIcons?.length > 0 &&
  5880. Vue.h(
  5881. "div",
  5882. { class: "card-status-icons" },
  5883. this.item.statusIcons.map((icon) =>
  5884. Vue.h(SsIcon, {
  5885. class: "status-icon " + icon.class,
  5886. title: icon.title,
  5887. })
  5888. )
  5889. ),
  5890. this.item?.buttons?.length > 0 &&
  5891. Vue.h(
  5892. "div",
  5893. {
  5894. class: "header",
  5895. style:
  5896. this.item?.statusIcons?.length > 0
  5897. ? {
  5898. right: String(this.item.statusIcons.length * 48) + "px",
  5899. borderTopRightRadius: "0",
  5900. }
  5901. : {},
  5902. onMouseenter: () => (this.showButtons = true),
  5903. onMouseleave: () => (this.showButtons = false),
  5904. onClick: (e) => this.onItemChange(e),
  5905. },
  5906. [
  5907. Vue.h("div", {
  5908. class: "cart-list-setting cart-list-icon",
  5909. title: this.item?.buttons?.[0]?.title,
  5910. }),
  5911. this.showButtons &&
  5912. this.item?.buttons?.length > 1 &&
  5913. Vue.h(
  5914. "div",
  5915. { class: "cart-list-button-popup" },
  5916. this.item.buttons.map((btn) =>
  5917. Vue.h(
  5918. "div",
  5919. {
  5920. onClick: (e) => {
  5921. e.stopPropagation();
  5922. btn.onclick?.();
  5923. },
  5924. },
  5925. [
  5926. btn.class &&
  5927. Vue.h(SsCartListIcon, { class: [btn.class] }),
  5928. Vue.h("span", null, btn.title),
  5929. ]
  5930. )
  5931. )
  5932. ),
  5933. ]
  5934. ),
  5935. Vue.h("div", { class: "body" }, [
  5936. Vue.h("div", { class: "box-header" }, [
  5937. Vue.h("div", String(this.item.title || "")),
  5938. ]),
  5939. Vue.h(
  5940. "div",
  5941. { class: !hasThumbArea ? "no-thumb box-body" : "box-body" },
  5942. [
  5943. hasThumbArea
  5944. ? thumbSrc
  5945. ? Vue.h("div", { class: "left" }, [
  5946. Vue.h("img", {
  5947. src: thumbSrc,
  5948. alt: "Thumbnail",
  5949. class: "imgUnHandle",
  5950. style: {
  5951. "object-fit": "cover",
  5952. width: "100%",
  5953. height: "100%",
  5954. },
  5955. }),
  5956. ])
  5957. : Vue.h(
  5958. "div",
  5959. { class: "left ss-objlist-thumbPlaceholder" },
  5960. [
  5961. Vue.h(SsIcon, {
  5962. class:
  5963. this.getBizThumbIconClass() +
  5964. " ss-objlist-thumbIcon",
  5965. }),
  5966. ]
  5967. )
  5968. : null,
  5969. Vue.h(
  5970. "div",
  5971. { class: "right" },
  5972. (() => {
  5973. const model = this.buildRightTextLines();
  5974. const hasAny = !!(model?.hasSecond || model?.hasThird);
  5975. if (!hasAny) return [];
  5976. const children = [];
  5977. if (model.hasSecond && model.secondKind === "summary") {
  5978. children.push(
  5979. Vue.h(
  5980. "div",
  5981. {
  5982. class:
  5983. "ss-card-text__line ss-card-text__secondBlock",
  5984. },
  5985. [
  5986. Vue.h(
  5987. "div",
  5988. {
  5989. class: "ss-card-text__secondSummary",
  5990. title: model.summary,
  5991. },
  5992. model.summary
  5993. ),
  5994. Vue.h("span", {
  5995. class: [
  5996. "ss-card-text__ellipsisHit",
  5997. "ss-card-text__ellipsisHit--second",
  5998. this.ellipsisVisible?.secondSummary
  5999. ? "is-on"
  6000. : "",
  6001. ],
  6002. title: "查看完整信息",
  6003. onMouseenter: (e) =>
  6004. this.showTextPopoverFor(
  6005. e?.currentTarget,
  6006. "second-summary"
  6007. ),
  6008. onClick: (e) => {
  6009. e?.stopPropagation?.();
  6010. this.showTextPopoverFor(
  6011. e?.currentTarget,
  6012. "second-summary"
  6013. );
  6014. },
  6015. onMouseleave: () => this.hideTextPopoverLater(),
  6016. }),
  6017. ]
  6018. )
  6019. );
  6020. }
  6021. if (model.hasSecond && model.secondKind === "tags") {
  6022. children.push(
  6023. Vue.h(
  6024. "div",
  6025. {
  6026. class: [
  6027. "ss-card-text__line",
  6028. model.secondFull
  6029. ? "ss-card-text__secondFullBlock"
  6030. : "ss-card-text__secondBlock",
  6031. ],
  6032. },
  6033. [
  6034. Vue.h(
  6035. "div",
  6036. { class: "ss-card-text__secondTags" },
  6037. [
  6038. ...model.secondTagsHead.map((t) =>
  6039. Vue.h(
  6040. "div",
  6041. {
  6042. class: "ss-card-text__tagLine",
  6043. title: t,
  6044. },
  6045. t
  6046. )
  6047. ),
  6048. Vue.h(
  6049. "div",
  6050. {
  6051. class:
  6052. "ss-card-text__tagLine is-last ss-card-text__tagLineLast",
  6053. title: model.secondTagsLast,
  6054. },
  6055. model.secondTagsLast
  6056. ),
  6057. ]
  6058. ),
  6059. Vue.h("span", {
  6060. class: [
  6061. "ss-card-text__ellipsisHit",
  6062. "ss-card-text__ellipsisHit--second",
  6063. this.ellipsisVisible?.secondTags ? "is-on" : "",
  6064. ],
  6065. title: "查看完整信息",
  6066. onMouseenter: (e) =>
  6067. this.showTextPopoverFor(
  6068. e?.currentTarget,
  6069. "second-tags"
  6070. ),
  6071. onClick: (e) => {
  6072. e?.stopPropagation?.();
  6073. this.showTextPopoverFor(
  6074. e?.currentTarget,
  6075. "second-tags"
  6076. );
  6077. },
  6078. onMouseleave: () => this.hideTextPopoverLater(),
  6079. }),
  6080. ]
  6081. )
  6082. );
  6083. }
  6084. if (model.hasThird) {
  6085. if (model.thirdFull) {
  6086. children.push(
  6087. Vue.h(
  6088. "div",
  6089. {
  6090. class:
  6091. "ss-card-text__line ss-card-text__thirdFullBlock",
  6092. },
  6093. [
  6094. Vue.h(
  6095. "div",
  6096. {
  6097. class: "ss-card-text__thirdFull",
  6098. title: model.thirdText,
  6099. },
  6100. model.thirdText
  6101. ),
  6102. Vue.h("span", {
  6103. class: [
  6104. "ss-card-text__ellipsisHit",
  6105. "ss-card-text__ellipsisHit--third",
  6106. this.ellipsisVisible?.thirdFull
  6107. ? "is-on"
  6108. : "",
  6109. ],
  6110. title: "查看完整信息",
  6111. onMouseenter: (e) =>
  6112. this.showTextPopoverFor(
  6113. e?.currentTarget,
  6114. "third-full"
  6115. ),
  6116. onClick: (e) => {
  6117. e?.stopPropagation?.();
  6118. this.showTextPopoverFor(
  6119. e?.currentTarget,
  6120. "third-full"
  6121. );
  6122. },
  6123. onMouseleave: () => this.hideTextPopoverLater(),
  6124. }),
  6125. ]
  6126. )
  6127. );
  6128. } else {
  6129. children.push(
  6130. Vue.h(
  6131. "div",
  6132. {
  6133. class:
  6134. "ss-card-text__line ss-card-text__thirdLineWrap",
  6135. },
  6136. [
  6137. Vue.h(
  6138. "div",
  6139. {
  6140. class: "ss-card-text__thirdLine",
  6141. title: model.thirdText,
  6142. },
  6143. model.thirdText
  6144. ),
  6145. Vue.h("span", {
  6146. class: [
  6147. "ss-card-text__ellipsisHit",
  6148. "ss-card-text__ellipsisHit--third",
  6149. this.ellipsisVisible?.third ? "is-on" : "",
  6150. ],
  6151. title: "查看完整信息",
  6152. onMouseenter: (e) =>
  6153. this.showTextPopoverFor(
  6154. e?.currentTarget,
  6155. "third"
  6156. ),
  6157. onClick: (e) => {
  6158. e?.stopPropagation?.();
  6159. this.showTextPopoverFor(
  6160. e?.currentTarget,
  6161. "third"
  6162. );
  6163. },
  6164. onMouseleave: () => this.hideTextPopoverLater(),
  6165. }),
  6166. ]
  6167. )
  6168. );
  6169. }
  6170. }
  6171. const popover =
  6172. this.showTextPopover &&
  6173. Vue.h(
  6174. "div",
  6175. {
  6176. class: "ss-card-text-popover",
  6177. style: { bottom: this.textPopoverBottom + "px" },
  6178. onMouseenter: () => {
  6179. this.clearHideTextPopoverTimer();
  6180. this.showTextPopover = true;
  6181. },
  6182. onMouseleave: () => this.hideTextPopoverLater(),
  6183. },
  6184. (() => {
  6185. const p = this.textPopoverPayload || {};
  6186. if (p.kind === "second-summary" && p.text) {
  6187. return [
  6188. Vue.h(
  6189. "div",
  6190. { class: "ss-card-text-popover__summary" },
  6191. p.text
  6192. ),
  6193. ];
  6194. }
  6195. if (Array.isArray(p.lines)) {
  6196. return [
  6197. Vue.h(
  6198. "div",
  6199. { class: "ss-card-text-popover__kvlist" },
  6200. p.lines.map((t) =>
  6201. Vue.h(
  6202. "div",
  6203. { class: "ss-card-text-popover__kv" },
  6204. t
  6205. )
  6206. )
  6207. ),
  6208. ];
  6209. }
  6210. if (
  6211. (p.kind === "third" || p.kind === "third-full") &&
  6212. p.text
  6213. ) {
  6214. return [
  6215. Vue.h(
  6216. "div",
  6217. { class: "ss-card-text-popover__objno" },
  6218. p.text
  6219. ),
  6220. ];
  6221. }
  6222. return [];
  6223. })()
  6224. );
  6225. return [
  6226. Vue.h("div", { class: "ss-card-text" }, children),
  6227. popover,
  6228. ];
  6229. })()
  6230. ),
  6231. ]
  6232. ),
  6233. ]),
  6234. ]
  6235. );
  6236. },
  6237. };
  6238. // ss-sidebar 右侧边栏(容器 + 子组件),用于 objList 右侧区域 by xu 20260106
  6239. // 组件文档补全(JSDoc) by xu 20260108
  6240. /**
  6241. * SsSidebarButtons(右侧边栏顶部按钮栏)
  6242. *
  6243. * 用途:
  6244. * - 渲染 objList 右侧顶部快捷操作(预定/入住/退房/清洁...)
  6245. * - 内部复用 `ss-search-button`(项目现有按钮样式/交互)
  6246. *
  6247. * 调用示例:
  6248. * ```html
  6249. * <ss-sidebar-buttons :items="sidebarButtons" />
  6250. * ```
  6251. *
  6252. * Props:
  6253. * - `items`: 按钮配置数组
  6254. * - `{ id, text, icon?, onClick? }`
  6255. *
  6256. * Emits:
  6257. * - `click`:点击按钮时触发,参数为按钮对象
  6258. */
  6259. const SsSidebarButtons = {
  6260. name: "SsSidebarButtons",
  6261. props: {
  6262. items: { type: Array, default: () => [] },
  6263. },
  6264. emits: ["click"],
  6265. render() {
  6266. const SsSearchButton = Vue.resolveComponent("ss-search-button");
  6267. const items = this.items || [];
  6268. if (!items.length) return null;
  6269. return Vue.h(
  6270. "div",
  6271. { class: "ss-sidebar-actions" },
  6272. items.map((btn) =>
  6273. // 顶部操作按钮复用 ss-search-button(先 mock 固定按钮) by xu 20260106
  6274. Vue.h(SsSearchButton, {
  6275. text: btn?.text ?? "",
  6276. iconClass: btn?.iconClass ?? "",
  6277. opt: btn?.opt ?? [],
  6278. checkId: btn?.checkId ?? "0",
  6279. width: btn?.width,
  6280. id: btn?.id,
  6281. onClick: (e) => {
  6282. e?.stopPropagation?.();
  6283. btn?.onClick?.(btn);
  6284. this.$emit("click", btn);
  6285. },
  6286. })
  6287. )
  6288. );
  6289. },
  6290. };
  6291. // 组件文档补全(JSDoc) by xu 20260108
  6292. /**
  6293. * SsSidebarChart(ECharts 容器渲染)
  6294. *
  6295. * 用途:
  6296. * - 仅负责 echarts init / setOption / resize / dispose
  6297. * - 被 `ss-sidebar-chart-hover` 与图表面板复用
  6298. *
  6299. * 调用示例:
  6300. * ```html
  6301. * <ss-sidebar-chart :options="option" height="220px" />
  6302. * ```
  6303. *
  6304. * Props:
  6305. * - `options`:ECharts option(Object)
  6306. * - `height`:容器高度(String)
  6307. */
  6308. const SsSidebarChart = {
  6309. name: "SsSidebarChart",
  6310. props: {
  6311. options: { type: Object, default: () => ({}) },
  6312. height: { type: String, default: "200px" },
  6313. },
  6314. setup(props) {
  6315. const elRef = Vue.ref(null);
  6316. let chart = null;
  6317. const renderChart = () => {
  6318. if (!elRef.value || !window.echarts) return;
  6319. if (!chart) {
  6320. chart = window.echarts.init(elRef.value);
  6321. }
  6322. chart.setOption(props.options || {}, true);
  6323. };
  6324. const resizeChart = () => {
  6325. chart?.resize?.();
  6326. };
  6327. Vue.onMounted(() => {
  6328. renderChart();
  6329. window.addEventListener("resize", resizeChart);
  6330. });
  6331. Vue.onBeforeUnmount(() => {
  6332. window.removeEventListener("resize", resizeChart);
  6333. chart?.dispose?.();
  6334. chart = null;
  6335. });
  6336. Vue.watch(
  6337. () => props.options,
  6338. () => {
  6339. renderChart();
  6340. },
  6341. { deep: true }
  6342. );
  6343. return { elRef };
  6344. },
  6345. render() {
  6346. return Vue.h("div", {
  6347. ref: "elRef",
  6348. style: {
  6349. width: "100%",
  6350. height: this.height,
  6351. // 图表容器不加 padding/border,由外层布局控制 by xu 20260106
  6352. background: "transparent",
  6353. border: "none",
  6354. "border-radius": "0",
  6355. },
  6356. });
  6357. },
  6358. };
  6359. // ss-sidebar-chart-hover:hover 弹出左侧大图(支持图钉/全屏) by xu 20260106
  6360. // 组件文档补全(JSDoc) by xu 20260108
  6361. /**
  6362. * SsSidebarChartHover(小图 + hover 左侧大图预览 + 图钉固定 + 全屏)
  6363. *
  6364. * 用途:
  6365. * - 右侧统计图小卡片:hover 时在左侧弹出大图预览
  6366. * - 预览头部:左侧图标+标题;右侧固定/全屏按钮(icon-base)
  6367. * - 全屏:方案A(浏览器 Fullscreen API)
  6368. *
  6369. * 调用示例(由 ss-sidebar chart panel 内部调用):
  6370. * ```html
  6371. * <ss-sidebar-chart-hover
  6372. * title="校舍建筑面积和总体分布"
  6373. * icon-class="menu-icon icon-obj-jzw"
  6374. * :options="option"
  6375. * height="220px"
  6376. * />
  6377. * ```
  6378. *
  6379. * Props:
  6380. * - `title/iconClass/icon`:用于预览/全屏 header 显示(与面板 header 一致)
  6381. * - `options`:ECharts option
  6382. * - `height`:小图高度
  6383. * - `previewWidth/previewHeight`:预览建议尺寸(会按视口自适应)
  6384. */
  6385. const SsSidebarChartHover = {
  6386. name: "SsSidebarChartHover",
  6387. props: {
  6388. // hover 大图标题/图标(与小图面板 header 一致) by xu 20260108
  6389. title: { type: String, default: "" },
  6390. iconClass: { type: String, default: "" },
  6391. icon: { type: String, default: "" },
  6392. options: { type: Object, default: () => ({}) },
  6393. height: { type: String, default: "220px" },
  6394. // hover 弹窗建议尺寸:默认 1000x650(比例由逻辑统一管控),实际渲染会按比例自适应视口 by xu 20260115
  6395. previewWidth: { type: Number, default: 1000 },
  6396. previewHeight: { type: Number, default: 650 },
  6397. },
  6398. setup(props) {
  6399. const triggerRef = Vue.ref(null);
  6400. const fullscreenRef = Vue.ref(null);
  6401. const open = Vue.ref(false);
  6402. const pinned = Vue.ref(false);
  6403. const fullscreen = Vue.ref(false);
  6404. const hoveringTrigger = Vue.ref(false);
  6405. const hoveringPreview = Vue.ref(false);
  6406. const previewStyle = Vue.ref({});
  6407. let closeTimer = null;
  6408. const updatePreviewPosition = () => {
  6409. const el = triggerRef.value;
  6410. if (!el) return;
  6411. const rect = el.getBoundingClientRect();
  6412. // 功能说明:优先使用 visualViewport(避免浏览器 UI/缩放导致可视区与 innerHeight 偏差,出现预览被遮挡约 70px) by xu 20260116
  6413. const docEl = document.documentElement;
  6414. const vv = window.visualViewport;
  6415. let viewportLeft = Number(vv?.offsetLeft ?? 0) || 0;
  6416. let viewportTop = Number(vv?.offsetTop ?? 0) || 0;
  6417. let vw =
  6418. Number(vv?.width ?? 0) ||
  6419. Number(docEl?.clientWidth || 0) ||
  6420. window.innerWidth;
  6421. let vh =
  6422. Number(vv?.height ?? 0) ||
  6423. Number(docEl?.clientHeight || 0) ||
  6424. window.innerHeight;
  6425. // 功能说明:若页面在 iframe 内,父页面可能裁切 iframe 可见区域(overflow/弹窗容器),需要用“iframe可见区域”做二次约束 by xu 20260116
  6426. try {
  6427. const inIframe = window.top && window.top !== window;
  6428. const frameEl = window.frameElement;
  6429. if (inIframe && frameEl && window.top?.document) {
  6430. const topVv = window.top.visualViewport;
  6431. const topDocEl = window.top.document.documentElement;
  6432. const topVw =
  6433. Number(topVv?.width ?? 0) ||
  6434. Number(topDocEl?.clientWidth || 0) ||
  6435. window.top.innerWidth;
  6436. const topVh =
  6437. Number(topVv?.height ?? 0) ||
  6438. Number(topDocEl?.clientHeight || 0) ||
  6439. window.top.innerHeight;
  6440. const fr = frameEl.getBoundingClientRect?.();
  6441. if (fr && fr.width > 0 && fr.height > 0) {
  6442. const visibleW = Math.max(
  6443. 0,
  6444. Math.min(fr.right, topVw) - Math.max(fr.left, 0)
  6445. );
  6446. const visibleH = Math.max(
  6447. 0,
  6448. Math.min(fr.bottom, topVh) - Math.max(fr.top, 0)
  6449. );
  6450. if (visibleW > 0) {
  6451. vw = Math.min(vw, visibleW);
  6452. viewportLeft = Math.max(0, -fr.left); // iframe内坐标系偏移(左侧被裁切时) by xu 20260116
  6453. }
  6454. if (visibleH > 0) {
  6455. vh = Math.min(vh, visibleH);
  6456. viewportTop = Math.max(0, -fr.top); // iframe内坐标系偏移(顶部被裁切时) by xu 20260116
  6457. }
  6458. }
  6459. }
  6460. } catch (_) {}
  6461. // 预览窗尺寸:优先保证“完整可见”,其次再尽量对齐右侧 header 顶部 by xu 20260116
  6462. const viewportPaddingX = 40;
  6463. const viewportPaddingTop = 20;
  6464. const viewportPaddingBottom = 10;
  6465. // 功能说明:hover 预览框不要覆盖右侧栏,优先放在 ss-sidebar 左侧;必要时动态缩小宽度 by xu 20260115
  6466. const sidebarEl = el.closest ? el.closest(".ss-sidebar") : null;
  6467. const sidebarRect = sidebarEl?.getBoundingClientRect?.();
  6468. const maxWidthByViewport = Math.max(240, vw - viewportPaddingX);
  6469. const maxWidthByLeftSpace = sidebarRect
  6470. ? Math.max(
  6471. 240,
  6472. sidebarRect.left - viewportPaddingX - 14 /* gapFromSidebar */
  6473. )
  6474. : maxWidthByViewport;
  6475. // 功能说明:预览框尺寸按比例(默认 5:3)缩放,并提供最大/最小值约束 by xu 20260115
  6476. const ratio = 3 / 5;
  6477. const minWidth = 320;
  6478. const minHeight = 240;
  6479. const maxHeightByViewport = Math.max(
  6480. minHeight,
  6481. vh - viewportPaddingTop - viewportPaddingBottom
  6482. );
  6483. const maxWidthByProp = Number(props.previewWidth) || 1000;
  6484. const maxHeightByProp = Number(props.previewHeight) || 650;
  6485. // 优先按高度撑满可视区(保证预览完整可见),再根据左侧可用宽度回退 by xu 20260116
  6486. const maxHeight = Math.min(maxHeightByProp, maxHeightByViewport);
  6487. let height = Math.max(minHeight, maxHeight);
  6488. let width = Math.round(height / ratio);
  6489. width = Math.min(
  6490. width,
  6491. maxWidthByProp,
  6492. maxWidthByViewport,
  6493. maxWidthByLeftSpace
  6494. );
  6495. width = Math.max(minWidth, width);
  6496. height = Math.round(width * ratio);
  6497. if (height > maxHeight) {
  6498. height = maxHeight;
  6499. width = Math.round(height / ratio);
  6500. }
  6501. // 默认贴着小图左侧弹出,右边缘与小图左边缘轻微重叠,避免 1px 缝隙导致 hover 闪断 by xu 20260108
  6502. const overlap = 2;
  6503. const gapFromSidebar = 14; // 功能说明:弹窗与右侧边栏留出距离(更不贴近) by xu 20260108
  6504. // 优先:贴在 sidebar 左侧(不压住右侧栏内容) by xu 20260115
  6505. let left = sidebarRect
  6506. ? sidebarRect.left - gapFromSidebar - width + overlap
  6507. : rect.left - width - gapFromSidebar + overlap;
  6508. // 如果左侧空间不足,则贴右侧(兜底,同样重叠) by xu 20260108
  6509. if (left < 0) left = rect.right + gapFromSidebar - overlap;
  6510. left = Math.max(
  6511. viewportLeft,
  6512. Math.min(left, viewportLeft + vw - width)
  6513. );
  6514. // top:优先保证完整可见,然后才贴近 header 顶部 by xu 20260116
  6515. let headerEl = el;
  6516. while (headerEl && !headerEl.classList?.contains("ss-sidebar-panel")) {
  6517. headerEl = headerEl.parentElement;
  6518. }
  6519. let headerRect = headerEl
  6520. ?.querySelector(".ss-sidebar-panel__header")
  6521. ?.getBoundingClientRect();
  6522. let top = headerRect?.top ?? rect.top;
  6523. top = Math.max(
  6524. viewportTop + viewportPaddingTop,
  6525. Math.min(top, viewportTop + vh - height - viewportPaddingBottom)
  6526. );
  6527. previewStyle.value = {
  6528. position: "fixed",
  6529. left: `${Math.round(left)}px`,
  6530. top: `${Math.round(top)}px`,
  6531. width: `${width}px`,
  6532. height: `${height}px`,
  6533. zIndex: 2147483647, // 功能说明:提高层级到接近浏览器上限,避免仍被页面固定栏/弹窗遮挡 by xu 20260116
  6534. };
  6535. };
  6536. const clearCloseTimer = () => {
  6537. if (closeTimer) {
  6538. clearTimeout(closeTimer);
  6539. closeTimer = null;
  6540. }
  6541. };
  6542. const scheduleClose = () => {
  6543. clearCloseTimer();
  6544. if (pinned.value || fullscreen.value) return;
  6545. if (hoveringTrigger.value || hoveringPreview.value) return; // 功能:鼠标在小图/大图之间移动不关闭 by xu 20260108
  6546. closeTimer = setTimeout(() => {
  6547. open.value = false;
  6548. }, 100);
  6549. };
  6550. const openPreview = () => {
  6551. clearCloseTimer();
  6552. updatePreviewPosition();
  6553. open.value = true;
  6554. };
  6555. const togglePin = () => {
  6556. pinned.value = !pinned.value;
  6557. if (pinned.value) {
  6558. open.value = true;
  6559. updatePreviewPosition();
  6560. }
  6561. };
  6562. const toggleFullscreen = () => {
  6563. // 全屏:采用浏览器 Fullscreen API(方案A),不使用遮罩弹窗 by xu 20260108
  6564. if (!fullscreen.value) {
  6565. open.value = false; // 避免同时渲染预览与全屏 by xu 20260108
  6566. fullscreen.value = true;
  6567. Vue.nextTick(() => {
  6568. const el = fullscreenRef.value;
  6569. if (el?.requestFullscreen) {
  6570. el.requestFullscreen().catch(() => {
  6571. // requestFullscreen 失败则回退为非全屏状态 by xu 20260108
  6572. fullscreen.value = false;
  6573. });
  6574. } else {
  6575. fullscreen.value = false;
  6576. }
  6577. });
  6578. } else {
  6579. if (document?.exitFullscreen) {
  6580. document.exitFullscreen().catch(() => {});
  6581. }
  6582. }
  6583. };
  6584. const handleFullscreenChange = () => {
  6585. const isFs = !!document.fullscreenElement;
  6586. fullscreen.value = isFs; // 功能说明:同步 ESC/系统退出全屏状态 by xu 20260108
  6587. if (isFs) {
  6588. open.value = false;
  6589. clearCloseTimer();
  6590. return;
  6591. }
  6592. // 退出全屏后:若固定或仍 hover,则恢复预览,否则关闭 by xu 20260108
  6593. if (pinned.value || hoveringTrigger.value || hoveringPreview.value) {
  6594. open.value = true;
  6595. updatePreviewPosition();
  6596. } else {
  6597. open.value = false;
  6598. }
  6599. };
  6600. Vue.onMounted(() => {
  6601. window.addEventListener("resize", updatePreviewPosition);
  6602. window.addEventListener("scroll", updatePreviewPosition, true);
  6603. document.addEventListener("fullscreenchange", handleFullscreenChange); // 功能说明:监听全屏状态变化 by xu 20260108
  6604. });
  6605. Vue.onBeforeUnmount(() => {
  6606. clearCloseTimer();
  6607. window.removeEventListener("resize", updatePreviewPosition);
  6608. window.removeEventListener("scroll", updatePreviewPosition, true);
  6609. document.removeEventListener(
  6610. "fullscreenchange",
  6611. handleFullscreenChange
  6612. );
  6613. });
  6614. return {
  6615. triggerRef,
  6616. fullscreenRef,
  6617. open,
  6618. pinned,
  6619. fullscreen,
  6620. hoveringTrigger,
  6621. hoveringPreview,
  6622. previewStyle,
  6623. openPreview,
  6624. scheduleClose,
  6625. clearCloseTimer,
  6626. togglePin,
  6627. toggleFullscreen,
  6628. };
  6629. },
  6630. render() {
  6631. const SsIcon = Vue.resolveComponent("ss-icon");
  6632. const Chart = Vue.resolveComponent("ss-sidebar-chart");
  6633. const hasHeader = !!(this.title || this.iconClass || this.icon); // 功能:hover 大图显示左侧图标+标题 by xu 20260108
  6634. const previewContent = Vue.h(
  6635. "div",
  6636. {
  6637. class: { "ss-sidebar-chart-preview": true, "is-pinned": this.pinned },
  6638. style: this.previewStyle,
  6639. onMouseenter: () => {
  6640. this.hoveringPreview = true;
  6641. this.clearCloseTimer();
  6642. },
  6643. onMouseleave: () => {
  6644. this.hoveringPreview = false;
  6645. this.scheduleClose();
  6646. },
  6647. },
  6648. [
  6649. hasHeader
  6650. ? Vue.h(
  6651. "div",
  6652. {
  6653. class:
  6654. "ss-sidebar-panel__header ss-sidebar-chart-preview__header",
  6655. },
  6656. [
  6657. Vue.h("div", { class: "ss-sidebar-panel__title" }, [
  6658. this.iconClass
  6659. ? Vue.h(SsIcon, {
  6660. class: this.iconClass + " ss-sidebar-panel__icon",
  6661. })
  6662. : this.icon
  6663. ? Vue.h(SsIcon, {
  6664. name: this.icon,
  6665. size: "16px",
  6666. class: "ss-sidebar-panel__icon",
  6667. })
  6668. : null,
  6669. Vue.h("span", null, this.title || "统计图"),
  6670. ]),
  6671. Vue.h("div", { class: "ss-sidebar-panel__tools" }, [
  6672. Vue.h(
  6673. "button",
  6674. {
  6675. type: "button",
  6676. class: {
  6677. "ss-sidebar-chart-tool": true,
  6678. "is-active": this.pinned,
  6679. },
  6680. title: this.pinned ? "取消固定" : "固定",
  6681. onClick: (e) => {
  6682. e.stopPropagation();
  6683. this.togglePin();
  6684. },
  6685. },
  6686. // 功能说明:右侧栏 hover 工具图标使用 ss-sidebar-base-icon(不复用左侧 menu-base-icon) by xu 20260123
  6687. [
  6688. Vue.h(SsIcon, {
  6689. class: this.pinned
  6690. ? "ss-sidebar-base-icon icon-fix-bold"
  6691. : "ss-sidebar-base-icon icon-fix",
  6692. }),
  6693. ]
  6694. ),
  6695. Vue.h(
  6696. "button",
  6697. {
  6698. type: "button",
  6699. class: "ss-sidebar-chart-tool",
  6700. title: "全屏",
  6701. onClick: (e) => {
  6702. e.stopPropagation();
  6703. this.toggleFullscreen();
  6704. },
  6705. },
  6706. [
  6707. Vue.h(SsIcon, {
  6708. class: this.fullscreen
  6709. ? "ss-sidebar-base-icon icon-fs-exit"
  6710. : "ss-sidebar-base-icon icon-fs",
  6711. }),
  6712. ]
  6713. ),
  6714. ]),
  6715. ]
  6716. )
  6717. : Vue.h(
  6718. "div",
  6719. {
  6720. class:
  6721. "ss-sidebar-chart-preview__header ss-sidebar-chart-preview__header--simple",
  6722. },
  6723. [
  6724. Vue.h("div", { class: "ss-sidebar-panel__tools" }, [
  6725. Vue.h(
  6726. "button",
  6727. {
  6728. type: "button",
  6729. class: {
  6730. "ss-sidebar-chart-tool": true,
  6731. "is-active": this.pinned,
  6732. },
  6733. title: this.pinned ? "取消固定" : "固定",
  6734. onClick: (e) => {
  6735. e.stopPropagation();
  6736. this.togglePin();
  6737. },
  6738. },
  6739. [
  6740. Vue.h(SsIcon, {
  6741. class: this.pinned
  6742. ? "ss-sidebar-base-icon icon-fix-bold"
  6743. : "ss-sidebar-base-icon icon-fix",
  6744. }),
  6745. ]
  6746. ),
  6747. Vue.h(
  6748. "button",
  6749. {
  6750. type: "button",
  6751. class: "ss-sidebar-chart-tool",
  6752. title: "全屏",
  6753. onClick: (e) => {
  6754. e.stopPropagation();
  6755. this.toggleFullscreen();
  6756. },
  6757. },
  6758. [
  6759. Vue.h(SsIcon, {
  6760. class: this.fullscreen
  6761. ? "ss-sidebar-base-icon icon-chk-on"
  6762. : "ss-sidebar-base-icon icon-chk",
  6763. }),
  6764. ]
  6765. ),
  6766. ]),
  6767. ]
  6768. ),
  6769. Vue.h("div", { class: "ss-sidebar-chart-preview__body" }, [
  6770. Vue.h(Chart, { options: this.options, height: "100%" }),
  6771. ]),
  6772. ]
  6773. );
  6774. const fullscreenContent =
  6775. this.fullscreen &&
  6776. Vue.h(
  6777. "div",
  6778. {
  6779. ref: "fullscreenRef",
  6780. class: "ss-sidebar-chart-fullscreen",
  6781. },
  6782. [
  6783. Vue.h(
  6784. "div",
  6785. {
  6786. class:
  6787. "ss-sidebar-panel__header ss-sidebar-chart-fullscreen__header",
  6788. },
  6789. [
  6790. Vue.h("div", { class: "ss-sidebar-panel__title" }, [
  6791. this.iconClass
  6792. ? Vue.h(SsIcon, {
  6793. class: this.iconClass + " ss-sidebar-panel__icon",
  6794. })
  6795. : this.icon
  6796. ? Vue.h(SsIcon, {
  6797. name: this.icon,
  6798. size: "16px",
  6799. class: "ss-sidebar-panel__icon",
  6800. })
  6801. : null,
  6802. Vue.h("span", null, this.title || "统计图"),
  6803. ]),
  6804. Vue.h("div", { class: "ss-sidebar-panel__tools" }, [
  6805. Vue.h(
  6806. "button",
  6807. {
  6808. type: "button",
  6809. class: {
  6810. "ss-sidebar-chart-tool": true,
  6811. "is-active": this.pinned,
  6812. },
  6813. title: this.pinned ? "取消固定" : "固定",
  6814. onClick: (e) => {
  6815. e.stopPropagation();
  6816. this.togglePin();
  6817. },
  6818. },
  6819. [
  6820. Vue.h(SsIcon, {
  6821. class: this.pinned
  6822. ? "ss-sidebar-base-icon icon-fix-bold"
  6823. : "ss-sidebar-base-icon icon-fix",
  6824. }),
  6825. ]
  6826. ),
  6827. Vue.h(
  6828. "button",
  6829. {
  6830. type: "button",
  6831. class: "ss-sidebar-chart-tool",
  6832. title: "退出全屏",
  6833. onClick: (e) => {
  6834. e.stopPropagation();
  6835. this.toggleFullscreen();
  6836. },
  6837. },
  6838. [
  6839. Vue.h(SsIcon, {
  6840. class: "ss-sidebar-base-icon icon-fs-exit",
  6841. }),
  6842. ]
  6843. ),
  6844. ]),
  6845. ]
  6846. ),
  6847. Vue.h("div", { class: "ss-sidebar-chart-fullscreen__body" }, [
  6848. Vue.h(Chart, { options: this.options, height: "100%" }),
  6849. ]),
  6850. ]
  6851. );
  6852. return Vue.h("div", { class: "ss-sidebar-chart-hover" }, [
  6853. Vue.h(
  6854. "div",
  6855. {
  6856. ref: "triggerRef",
  6857. class: "ss-sidebar-chart-hover__trigger",
  6858. onMouseenter: () => {
  6859. this.hoveringTrigger = true;
  6860. this.openPreview();
  6861. },
  6862. onMouseleave: () => {
  6863. this.hoveringTrigger = false;
  6864. this.scheduleClose();
  6865. },
  6866. },
  6867. [Vue.h(Chart, { options: this.options, height: this.height })]
  6868. ),
  6869. (this.open || this.fullscreen) &&
  6870. Vue.h(Vue.Teleport, { to: "body" }, [
  6871. this.open ? previewContent : null,
  6872. fullscreenContent || null,
  6873. ]),
  6874. ]);
  6875. },
  6876. };
  6877. // 组件文档补全(JSDoc) by xu 20260108
  6878. /**
  6879. * SsSidebarList(右侧业务面板:人员/已选/服务/预定...)
  6880. *
  6881. * 用途:
  6882. * - 统一渲染面板 header(图标/标题/数量/右侧按钮)
  6883. * - 统一渲染 list(固定行高、hover、右侧移除按钮等)
  6884. *
  6885. * 调用示例(由 ss-sidebar 通过 panels 配置驱动):
  6886. * ```js
  6887. * { type:'list', title:'已选', iconClass:'menu-icon icon-obj-xcd', mode:'selected', items:selectedItems, closable:true }
  6888. * ```
  6889. *
  6890. * Props(核心):
  6891. * - `title`:header 标题
  6892. * - `iconClass/icon`:header 图标(优先 iconClass)
  6893. * - `count`:数量回显(图表面板可不传)
  6894. * - `closable`:是否显示“清空”按钮(触发 emit clear)
  6895. * - `headerFilters`:header 条件数组(组件名 + props),用于联调接口搜索
  6896. * - `headerSearchButton`:是否显示搜索按钮(触发 emit search)
  6897. * - `items`:列表数据
  6898. * - `mode`:`selected` 时右侧按钮语义为“移除”
  6899. * - `itemLayout`:`simple` / `person`(人员号槽位)
  6900. * - `itemAction`:是否显示 item 右侧操作按钮(hover 才出现)
  6901. *
  6902. * Emits:
  6903. * - `remove(item)`:点击 item 右侧移除
  6904. * - `clear()`:点击 header 清空
  6905. * - `search({keyword, filters})`:点击 header 搜索
  6906. */
  6907. const SsSidebarList = {
  6908. name: "SsSidebarList",
  6909. props: {
  6910. title: { type: String, default: "" },
  6911. // header 图标:优先使用 iconClass(走 ss-icon v3.0 class 分支) by xu 20260106
  6912. iconClass: { type: String, default: "" },
  6913. icon: { type: String, default: "" }, // 兼容旧写法(ss-icon name)
  6914. count: { type: [Number, String], default: "" },
  6915. // 选中类分区:右侧关闭按钮=清空分区数据 by xu 20260106
  6916. closable: { type: Boolean, default: false },
  6917. searchable: { type: Boolean, default: false },
  6918. // 搜索框是否放在 header 内(人员块需要该布局) by xu 20260106
  6919. searchInHeader: { type: Boolean, default: false },
  6920. // header 搜索:下拉条件 + 搜索按钮(适合“人员”块) by xu 20260106
  6921. headerFilters: { type: Array, default: () => [] },
  6922. headerSearchButton: { type: Boolean, default: false },
  6923. searchPlaceholder: { type: String, default: "搜索" },
  6924. // 列表项布局:simple(仅标题) / person(标题+人员号槽位) by xu 20260106
  6925. itemLayout: { type: String, default: "simple" },
  6926. itemAction: { type: Boolean, default: true },
  6927. collapsible: { type: Boolean, default: true }, // 功能说明:是否允许双击 header 折叠/展开 by xu 20260116
  6928. collapsed: { type: Boolean, default: false }, // 功能说明:折叠态仅展示 header by xu 20260116
  6929. items: { type: Array, default: () => [] },
  6930. mode: { type: String, default: "search" }, // search / selected
  6931. },
  6932. emits: ["select", "remove", "clear", "search", "toggle-collapse"],
  6933. data() {
  6934. return {
  6935. keyword: "",
  6936. filterValues: {},
  6937. }; // 功能说明:折叠状态完全由 props.collapsed 驱动,避免多面板复用导致状态不同步 by xu 20260116
  6938. },
  6939. created() {
  6940. // header 下拉条件默认值初始化 by xu 20260106
  6941. (this.headerFilters || []).forEach((f) => {
  6942. if (!f || !f.key) return;
  6943. if (this.filterValues[f.key] !== undefined) return;
  6944. const first = f?.options?.[0]?.value ?? "";
  6945. this.filterValues[f.key] = f.value !== undefined ? f.value : first;
  6946. });
  6947. },
  6948. methods: {
  6949. __shouldIgnoreHeaderToggle(e) {
  6950. // 功能说明:忽略工具区/输入区触发折叠,避免误触 by xu 20260116
  6951. const t = e?.target;
  6952. if (!t || !t.closest) return false;
  6953. if (t.closest(".ss-sidebar-panel__tools")) return true;
  6954. if (t.closest(".ss-sidebar-panel__filters")) return true;
  6955. if (t.closest("input,textarea,select,button")) return true;
  6956. return false;
  6957. },
  6958. __toggleCollapseInternal(e, source) {
  6959. if (!this.collapsible) return;
  6960. if (this.__shouldIgnoreHeaderToggle(e)) return;
  6961. const nextCollapsed = !this.collapsed;
  6962. console.log("[SsSidebarList] toggle emit", {
  6963. title: this.title,
  6964. source,
  6965. to: nextCollapsed,
  6966. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  6967. // 功能说明:由父组件(SsSidebar.toggleSectionCollapse)统一控制 section 高度与折叠数组 by xu 20260116
  6968. this.$emit("toggle-collapse");
  6969. },
  6970. },
  6971. render() {
  6972. const items = this.items || [];
  6973. const SsIcon = Vue.resolveComponent("ss-icon");
  6974. const isSelectedMode = this.mode === "selected";
  6975. const activeKeyword = this.filterValues?.keyword ?? this.keyword; // 功能:keyword 优先取 headerFilters.keyword by xu 20260106
  6976. const hasHeaderKeyword = (this.headerFilters || []).some(
  6977. (f) => f?.key === "keyword"
  6978. ); // 功能:header 内 keyword 过滤 by xu 20260106
  6979. const renderHeaderFilter = (f) => {
  6980. if (!f) return null;
  6981. const key = f.key;
  6982. const componentName = f.component;
  6983. if (!key || !componentName) return null;
  6984. const Comp = Vue.resolveComponent(componentName);
  6985. if (!Comp) return null;
  6986. const modelValue = this.filterValues[key];
  6987. const props = f.props || {};
  6988. return Vue.h(Comp, {
  6989. ...props,
  6990. modelValue,
  6991. "onUpdate:modelValue": (v) => {
  6992. this.filterValues[key] = v;
  6993. },
  6994. });
  6995. };
  6996. const filteredItems =
  6997. this.searchable && activeKeyword
  6998. ? items.filter((it) =>
  6999. String(it?.title ?? "")
  7000. .toLowerCase()
  7001. .includes(String(activeKeyword).toLowerCase())
  7002. )
  7003. : items;
  7004. if (!filteredItems.length && !this.title) return null;
  7005. if (this.collapsed) {
  7006. return Vue.h("div", { class: "ss-sidebar-panel" }, [
  7007. this.title
  7008. ? Vue.h(
  7009. "div",
  7010. {
  7011. class: "ss-sidebar-panel__header",
  7012. // 功能说明:折叠触发绑定到整个 header(dblclick + click.detail==2 兜底) by xu 20260116
  7013. onDblclick: (e) => {
  7014. e?.preventDefault?.();
  7015. e?.stopPropagation?.();
  7016. console.log("[SsSidebarList] header dblclick", {
  7017. title: this.title,
  7018. collapsed: true,
  7019. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7020. this.__toggleCollapseInternal(e, "dblclick");
  7021. },
  7022. // 功能说明:移除 click.detail==2 兜底,避免双击同时触发 click+dblclick 导致“折叠又立刻展开” by xu 20260116
  7023. },
  7024. [
  7025. Vue.h("div", { class: "ss-sidebar-panel__title" }, [
  7026. this.iconClass
  7027. ? Vue.h(SsIcon, {
  7028. class: this.iconClass + " ss-sidebar-panel__icon",
  7029. })
  7030. : this.icon
  7031. ? Vue.h(SsIcon, {
  7032. name: this.icon,
  7033. size: "16px",
  7034. class: "ss-sidebar-panel__icon",
  7035. })
  7036. : null,
  7037. Vue.h("span", null, this.title),
  7038. this.count !== ""
  7039. ? Vue.h(
  7040. "span",
  7041. { class: "ss-sidebar-panel__count" },
  7042. `(${this.count})`
  7043. )
  7044. : null,
  7045. ]),
  7046. Vue.h("div", { class: "ss-sidebar-panel__tools" }, [
  7047. this.closable
  7048. ? Vue.h(
  7049. "button",
  7050. {
  7051. type: "button",
  7052. class: "ss-sidebar-icon-btn ss-sidebar-header-btn",
  7053. title: "清空",
  7054. onClick: (e) => {
  7055. e.stopPropagation();
  7056. this.$emit("clear");
  7057. },
  7058. },
  7059. [
  7060. Vue.h(SsIcon, {
  7061. class: "ss-sidebar-base-icon icon-cl",
  7062. }),
  7063. ] // 功能说明:右侧栏“已选”清空按钮图标使用 ss-sidebar-base-icon by xu 20260123
  7064. )
  7065. : null,
  7066. ]),
  7067. ]
  7068. )
  7069. : null,
  7070. ]);
  7071. }
  7072. return Vue.h("div", { class: "ss-sidebar-panel" }, [
  7073. this.title
  7074. ? Vue.h(
  7075. "div",
  7076. {
  7077. class: "ss-sidebar-panel__header",
  7078. // 功能说明:折叠触发绑定到整个 header(dblclick + click.detail==2 兜底) by xu 20260116
  7079. onDblclick: (e) => {
  7080. e?.preventDefault?.();
  7081. e?.stopPropagation?.();
  7082. console.log("[SsSidebarList] header dblclick", {
  7083. title: this.title,
  7084. collapsed: false,
  7085. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7086. this.__toggleCollapseInternal(e, "dblclick");
  7087. },
  7088. // 功能说明:移除 click.detail==2 兜底,避免双击同时触发 click+dblclick 导致“折叠又立刻展开” by xu 20260116
  7089. },
  7090. [
  7091. Vue.h("div", { class: "ss-sidebar-panel__title" }, [
  7092. // 图标 + 标题(每个分区都有) by xu 20260106
  7093. this.iconClass
  7094. ? Vue.h(SsIcon, {
  7095. class: this.iconClass + " ss-sidebar-panel__icon",
  7096. })
  7097. : this.icon
  7098. ? Vue.h(SsIcon, {
  7099. name: this.icon,
  7100. size: "16px",
  7101. class: "ss-sidebar-panel__icon",
  7102. })
  7103. : null,
  7104. Vue.h("span", null, this.title),
  7105. // 数量回显:图表分区可不传 count by xu 20260106
  7106. this.count !== ""
  7107. ? Vue.h(
  7108. "span",
  7109. { class: "ss-sidebar-panel__count" },
  7110. `(${this.count})`
  7111. )
  7112. : null,
  7113. ]),
  7114. Vue.h("div", { class: "ss-sidebar-panel__tools" }, [
  7115. // header 条件(例如下拉框)+ 右侧搜索按钮 by xu 20260106
  7116. this.headerFilters?.length
  7117. ? Vue.h(
  7118. "div",
  7119. { class: "ss-sidebar-panel__filters" },
  7120. this.headerFilters
  7121. .map(renderHeaderFilter)
  7122. .filter(Boolean)
  7123. )
  7124. : null,
  7125. this.headerSearchButton
  7126. ? Vue.h(
  7127. "button",
  7128. {
  7129. type: "button",
  7130. class: "ss-sidebar-icon-btn",
  7131. title: "搜索",
  7132. onClick: (e) => {
  7133. e.stopPropagation();
  7134. this.$emit("search", {
  7135. // headerFilters 内也可能包含 keyword by xu 20260106
  7136. keyword: activeKeyword,
  7137. filters: { ...(this.filterValues || {}) },
  7138. });
  7139. },
  7140. },
  7141. [Vue.h(SsIcon, { name: "search", size: "14px" })]
  7142. )
  7143. : null,
  7144. // 人员块:搜索框在 header 内 by xu 20260106
  7145. this.searchable &&
  7146. this.searchInHeader &&
  7147. !this.headerSearchButton
  7148. ? Vue.h(
  7149. "div",
  7150. { class: "ss-sidebar-panel__searchInline" },
  7151. [
  7152. Vue.h(
  7153. "div",
  7154. { class: "ss-sidebar-search is-inline" },
  7155. [
  7156. Vue.h(SsIcon, {
  7157. name: "search",
  7158. size: "14px",
  7159. class: "ss-sidebar-search__prefix",
  7160. }),
  7161. Vue.h("input", {
  7162. class: "ss-sidebar-search__input",
  7163. value: this.keyword,
  7164. placeholder: this.searchPlaceholder,
  7165. onInput: (e) => {
  7166. this.keyword = e?.target?.value ?? "";
  7167. },
  7168. }),
  7169. ]
  7170. ),
  7171. ]
  7172. )
  7173. : null,
  7174. this.closable
  7175. ? Vue.h(
  7176. "button",
  7177. {
  7178. type: "button",
  7179. class: "ss-sidebar-icon-btn ss-sidebar-header-btn",
  7180. title: "清空",
  7181. onClick: (e) => {
  7182. e.stopPropagation();
  7183. this.$emit("clear");
  7184. },
  7185. },
  7186. // 清空按钮使用 icon-base 的 icon-cl by xu 20260106
  7187. [
  7188. Vue.h(SsIcon, {
  7189. class: "ss-sidebar-base-icon icon-cl",
  7190. }),
  7191. ] // 功能说明:右侧栏清空按钮图标使用 ss-sidebar-base-icon by xu 20260123
  7192. )
  7193. : null,
  7194. ]),
  7195. ]
  7196. )
  7197. : null,
  7198. // 非 header 内搜索:独立一行 by xu 20260106
  7199. // headerSearchButton/headerFilters 已覆盖搜索能力时,不再额外渲染独立搜索行 by xu 20260106
  7200. this.searchable &&
  7201. !this.searchInHeader &&
  7202. !this.headerSearchButton &&
  7203. !hasHeaderKeyword
  7204. ? Vue.h("div", { class: "ss-sidebar-panel__search" }, [
  7205. Vue.h("div", { class: "ss-sidebar-search" }, [
  7206. Vue.h(SsIcon, {
  7207. name: "search",
  7208. size: "14px",
  7209. class: "ss-sidebar-search__prefix",
  7210. }),
  7211. Vue.h("input", {
  7212. class: "ss-sidebar-search__input",
  7213. value: this.keyword,
  7214. placeholder: this.searchPlaceholder,
  7215. onInput: (e) => {
  7216. this.keyword = e?.target?.value ?? "";
  7217. },
  7218. }),
  7219. ]),
  7220. ])
  7221. : null,
  7222. Vue.h(
  7223. "div",
  7224. { class: "ss-sidebar-list" },
  7225. filteredItems.map((item, idx) => {
  7226. const title = item?.title ?? "";
  7227. const tags = item?.tags || [];
  7228. const isPersonLayout = this.itemLayout === "person";
  7229. const hasTags = !isPersonLayout && tags?.length > 0; // 列表项垂直对齐:有 tags 顶对齐 by xu 20260106
  7230. return Vue.h(
  7231. "div",
  7232. {
  7233. class: {
  7234. "ss-sidebar-list-item": true,
  7235. "is-first": idx === 0,
  7236. "is-person": isPersonLayout,
  7237. "has-tags": hasTags,
  7238. },
  7239. },
  7240. [
  7241. Vue.h("div", { class: "ss-sidebar-list-item__main" }, [
  7242. Vue.h(
  7243. "div",
  7244. { class: "ss-sidebar-list-item__title" },
  7245. Vue.h(
  7246. "span",
  7247. {
  7248. style: {
  7249. "white-space": "nowrap",
  7250. overflow: "hidden",
  7251. "text-overflow": "ellipsis",
  7252. },
  7253. },
  7254. title
  7255. )
  7256. ),
  7257. // 非人员布局才显示 tags by xu 20260106
  7258. !isPersonLayout && tags?.length
  7259. ? Vue.h(
  7260. "div",
  7261. { class: "ss-sidebar-list-item__tags" },
  7262. tags.map((tag) => {
  7263. const [k, v] = Object.entries(tag)[0] || ["", ""];
  7264. return Vue.h(
  7265. "span",
  7266. { class: "ss-sidebar-tag", title: `${k}: ${v}` },
  7267. `${k}: ${v}`
  7268. );
  7269. })
  7270. )
  7271. : null,
  7272. ]),
  7273. // 人员布局:中间保留“人员号”槽位 by xu 20260106
  7274. isPersonLayout
  7275. ? Vue.h(
  7276. "div",
  7277. {
  7278. class: "ss-sidebar-list-item__meta",
  7279. title: String(item?.meta ?? ""),
  7280. },
  7281. item?.meta ?? ""
  7282. )
  7283. : null,
  7284. this.itemAction
  7285. ? Vue.h(
  7286. "button",
  7287. {
  7288. type: "button",
  7289. class: {
  7290. // item 操作按钮:默认无背景/无边框,hover 才高亮 by xu 20260106
  7291. "ss-sidebar-item-btn": true,
  7292. },
  7293. title: isSelectedMode ? "移除" : "选择",
  7294. onClick: (e) => {
  7295. e.stopPropagation();
  7296. if (isSelectedMode) this.$emit("remove", item);
  7297. else this.$emit("select", item);
  7298. },
  7299. },
  7300. [
  7301. // item 移除图标使用 icon-base 的 icon-cl by xu 20260106
  7302. isSelectedMode
  7303. ? Vue.h(SsIcon, {
  7304. class: "ss-sidebar-base-icon icon-cl",
  7305. }) // 功能说明:右侧栏 item 移除图标使用 ss-sidebar-base-icon by xu 20260123
  7306. : Vue.h(SsIcon, { name: "check", size: "14px" }),
  7307. ]
  7308. )
  7309. : null,
  7310. ]
  7311. );
  7312. })
  7313. ),
  7314. ]);
  7315. },
  7316. };
  7317. // ss-sidebar-report-table:右侧“统计表/报表”面板(pstatList grtjlbm=51 聚拢渲染) by xu 20260115
  7318. const SsSidebarReportTable = {
  7319. name: "SsSidebarReportTable",
  7320. props: {
  7321. title: { type: String, default: "" },
  7322. iconClass: { type: String, default: "" },
  7323. icon: { type: String, default: "" },
  7324. items: { type: Array, default: () => [] }, // pstatList(grtjlbm=51) 数组
  7325. onOpen: { type: Function, default: null }, // (srv, ctx) => void
  7326. collapsible: { type: Boolean, default: true }, // 功能说明:是否允许双击 header 折叠/展开 by xu 20260116
  7327. collapsed: { type: Boolean, default: false }, // 功能说明:折叠态仅展示 header by xu 20260116
  7328. },
  7329. emits: ["open", "toggle-collapse"],
  7330. data() {
  7331. return {}; // 功能说明:折叠状态完全由 props.collapsed 驱动 by xu 20260116
  7332. },
  7333. methods: {
  7334. __toggleCollapseInternal(e, source) {
  7335. if (!this.collapsible) return;
  7336. const next = !this.collapsed;
  7337. console.log("[SsSidebarReportTable] toggle emit", {
  7338. title: this.title,
  7339. source,
  7340. to: next,
  7341. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7342. this.$emit("toggle-collapse");
  7343. },
  7344. },
  7345. render() {
  7346. const SsIcon = Vue.resolveComponent("ss-icon");
  7347. const list = this.items || [];
  7348. if (!this.title && !list.length) return null;
  7349. const header = this.title
  7350. ? Vue.h(
  7351. "div",
  7352. {
  7353. class: "ss-sidebar-panel__header",
  7354. // 功能说明:折叠触发绑定到整个 header(dblclick + click.detail==2 兜底) by xu 20260116
  7355. onDblclick: (e) => {
  7356. e?.preventDefault?.();
  7357. e?.stopPropagation?.();
  7358. console.log("[SsSidebarReportTable] header dblclick", {
  7359. title: this.title,
  7360. collapsed: this.collapsed,
  7361. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7362. this.__toggleCollapseInternal(e, "dblclick");
  7363. },
  7364. // 功能说明:移除 click.detail==2 兜底,避免双击同时触发 click+dblclick 导致“折叠又立刻展开” by xu 20260116
  7365. },
  7366. [
  7367. Vue.h("div", { class: "ss-sidebar-panel__title" }, [
  7368. this.iconClass
  7369. ? Vue.h(SsIcon, {
  7370. class: this.iconClass + " ss-sidebar-panel__icon",
  7371. })
  7372. : this.icon
  7373. ? Vue.h(SsIcon, {
  7374. name: this.icon,
  7375. size: "16px",
  7376. class: "ss-sidebar-panel__icon",
  7377. })
  7378. : null,
  7379. Vue.h("span", null, this.title),
  7380. ]),
  7381. Vue.h("div", { class: "ss-sidebar-panel__tools" }),
  7382. ]
  7383. )
  7384. : null;
  7385. const renderReport = (report) => {
  7386. const title = String(report?.mc ?? "");
  7387. const mx = Array.isArray(report?.grtjmxList) ? report.grtjmxList : [];
  7388. if (!title && !mx.length) return null;
  7389. // 功能说明:每个报表对象渲染为一个 table(有边框、无圆角、表间距10px;样式由 base.css 统一控制) by xu 20260115
  7390. const cols = Math.max(1, mx.length);
  7391. // 功能说明:table 外层包一层 wrap,子项过多时支持横向滚动 by xu 20260115
  7392. return Vue.h("div", { class: "ss-sidebar-report-table-wrap" }, [
  7393. Vue.h("table", { class: "ss-sidebar-report-table" }, [
  7394. Vue.h("thead", null, [
  7395. Vue.h("tr", null, [
  7396. Vue.h(
  7397. "th",
  7398. { class: "ss-sidebar-report-table__title", colspan: cols },
  7399. Vue.h(
  7400. "div",
  7401. { class: "ss-sidebar-report-table__title-content" },
  7402. [
  7403. Vue.h("span", { class: "ss-sidebar-report-table__dot" }),
  7404. Vue.h(
  7405. "span",
  7406. { class: "ss-sidebar-report-table__title-text", title },
  7407. title
  7408. ),
  7409. ]
  7410. )
  7411. ),
  7412. ]),
  7413. ]),
  7414. Vue.h("tbody", null, [
  7415. Vue.h(
  7416. "tr",
  7417. null,
  7418. mx.map((cell) => {
  7419. const text = String(cell?.mc ?? "");
  7420. const srv = {
  7421. servName: cell?.fwm ?? "",
  7422. dest: cell?.bjm ?? "",
  7423. title: text,
  7424. width: cell?.width,
  7425. height: cell?.height,
  7426. minHeight: cell?.height,
  7427. maxHeight: cell?.height,
  7428. showTitle: text,
  7429. };
  7430. return Vue.h(
  7431. "td",
  7432. {
  7433. class: "ss-sidebar-report-table__cell",
  7434. title: text,
  7435. onClick: (e) => {
  7436. e?.stopPropagation?.();
  7437. try {
  7438. this.onOpen?.(srv, { report, cell });
  7439. } catch (_) {}
  7440. this.$emit("open", { report, cell, srv });
  7441. },
  7442. },
  7443. text
  7444. );
  7445. })
  7446. ),
  7447. ]),
  7448. ]),
  7449. ]);
  7450. };
  7451. // 功能说明:报表面板增加独立 class,便于 base.css 统一控制 padding/间距 by xu 20260115
  7452. return Vue.h(
  7453. "div",
  7454. { class: "ss-sidebar-panel ss-sidebar-report-panel" },
  7455. [
  7456. header,
  7457. this.collapsed
  7458. ? null
  7459. : Vue.h(
  7460. "div",
  7461. // 功能说明:报表列表滚动/高度样式下沉到 base.css,避免写在 DOM 上 by xu 20260115
  7462. { class: "ss-sidebar-report__list" },
  7463. list.map(renderReport).filter(Boolean)
  7464. ),
  7465. ]
  7466. );
  7467. },
  7468. };
  7469. // 组件文档补全(JSDoc) by xu 20260108
  7470. /**
  7471. * SsSidebar(objList 右侧边栏容器)
  7472. *
  7473. * 用途:
  7474. * - 统一渲染顶部按钮栏(buttons)
  7475. * - 统一渲染中间业务面板(list panels,可拖拽调高度)
  7476. * - 统一渲染底部图表(chart panels,内部用 ss-sidebar-chart-hover)
  7477. *
  7478. * 调用示例:
  7479. * ```html
  7480. * <ss-sidebar :buttons="sidebarButtons" :panels="sidebarPanels" @remove="handleSidebarRemove" />
  7481. * ```
  7482. *
  7483. * Props:
  7484. * - `buttons`:顶部按钮配置数组
  7485. * - `panels`:分区配置数组(`type: 'list' | 'chart'`)
  7486. *
  7487. * Events(向外透传):
  7488. * - `remove(item)`:来自 list 面板移除
  7489. * - `select(item)`:来自 list 面板选择(如后续需要)
  7490. */
  7491. const SsSidebar = {
  7492. name: "SsSidebar",
  7493. props: {
  7494. buttons: { type: Array, default: () => [] },
  7495. charts: { type: Array, default: () => [] },
  7496. list: { type: Array, default: () => [] }, // legacy
  7497. listMode: { type: String, default: "search" }, // legacy
  7498. panels: { type: Array, default: () => [] },
  7499. },
  7500. emits: ["select", "remove"],
  7501. data() {
  7502. return {
  7503. // 业务面板高度(索引 -> px) by xu 20260106
  7504. sectionHeights: [],
  7505. sectionCollapsed: [], // 功能说明:面板折叠状态(sectionPanels 索引) by xu 20260116
  7506. sectionHeightsExpanded: [], // 功能说明:面板展开高度缓存(用于折叠后恢复) by xu 20260116
  7507. sectionLastItemCounts: [], // 功能说明:记录栏目数据量,供“无数据默认关闭/已选自动展开”规则复用 by xu 20260313
  7508. chartCollapsed: [], // 功能说明:图表面板折叠状态(chartPanels 索引) by xu 20260116
  7509. chartHeaderTitleDownAt: [], // 功能说明:双击检测绑定到 chart 标题区 by xu 20260116
  7510. reportCollapsed: [], // 功能说明:报表面板折叠状态(reportPanels 索引) by xu 20260116
  7511. resizeTimer: null,
  7512. resizing: false,
  7513. resizeIndex: -1,
  7514. resizeStartY: 0,
  7515. resizeStartPrev: 0,
  7516. resizeStartNext: 0,
  7517. __resizeMoveHandler: null, // 功能说明:显式绑定 this 的 pointermove handler,避免 addEventListener 场景 this 丢失导致拖拽无效 by xu 20260122
  7518. __resizeEndHandler: null, // 功能说明:显式绑定 this 的 pointerup handler,确保能正确结束拖拽 by xu 20260122
  7519. __resizeCancelHandler: null, // 功能说明:显式绑定 this 的 pointercancel handler,触控取消也能收尾 by xu 20260122
  7520. __resizePrevSectionEl: null, // 功能说明:拖拽时直接写 DOM 高度(修复响应式更新不生效) by xu 20260122
  7521. __resizeNextSectionEl: null, // 功能说明:拖拽时直接写 DOM 高度(修复响应式更新不生效) by xu 20260122
  7522. };
  7523. },
  7524. methods: {
  7525. // 初始化默认高度(只在第一次/面板数量变化时补齐) by xu 20260106
  7526. ensureSectionHeights(sectionCount) {
  7527. if (!Array.isArray(this.sectionHeights)) this.sectionHeights = [];
  7528. if (this.sectionHeights.length === sectionCount) return;
  7529. const next = [];
  7530. for (let i = 0; i < sectionCount; i++) {
  7531. next[i] = this.sectionHeights[i] ?? 190; // 默认高度 by xu 20260106
  7532. }
  7533. this.sectionHeights = next;
  7534. // 功能说明:面板数量变化时补齐折叠/缓存数组长度 by xu 20260116
  7535. this.sectionCollapsed = Array.from(
  7536. { length: sectionCount },
  7537. (_, i) => !!this.sectionCollapsed?.[i]
  7538. );
  7539. this.sectionHeightsExpanded = Array.from(
  7540. { length: sectionCount },
  7541. (_, i) => this.sectionHeightsExpanded?.[i] ?? null
  7542. );
  7543. this.sectionLastItemCounts = Array.from(
  7544. { length: sectionCount },
  7545. (_, i) => Number(this.sectionLastItemCounts?.[i] ?? 0) || 0
  7546. );
  7547. },
  7548. __getPanelsForSectionState(panelsInput) {
  7549. return (panelsInput || []).length
  7550. ? panelsInput
  7551. : this.list?.length
  7552. ? [
  7553. {
  7554. type: "list",
  7555. title: "已选",
  7556. icon: "",
  7557. mode: this.listMode,
  7558. items: this.list,
  7559. },
  7560. ]
  7561. : [];
  7562. },
  7563. __getSectionPanelsForState(panelsInput) {
  7564. return this.__getPanelsForSectionState(panelsInput)
  7565. .filter((p) => {
  7566. const k = String(p?._tabKey ?? "")
  7567. .trim()
  7568. .toLowerCase();
  7569. const t = String(p?.title ?? "").trim();
  7570. if (k === "rbarobj") return false;
  7571. if (t === "对象") return false;
  7572. return true;
  7573. })
  7574. .filter((p) => p?.type !== "chart" && p?.type !== "report-table");
  7575. },
  7576. __getSectionPanelCount(panel) {
  7577. const explicitCount = Number(panel?.count);
  7578. if (Number.isFinite(explicitCount)) return explicitCount;
  7579. return Array.isArray(panel?.items) ? panel.items.length : 0;
  7580. },
  7581. __setSectionCollapsedState(index, collapsed) {
  7582. const i = Number(index);
  7583. if (isNaN(i) || i < 0) return;
  7584. const nextCollapsed = !!collapsed;
  7585. const collapsedHeight = 37;
  7586. const cur = !!this.sectionCollapsed?.[i];
  7587. const currentHeight = Number(this.sectionHeights?.[i] ?? 190) || 190;
  7588. if (nextCollapsed === cur) {
  7589. if (nextCollapsed && currentHeight !== collapsedHeight) {
  7590. this.sectionHeights.splice(i, 1, collapsedHeight);
  7591. }
  7592. return;
  7593. }
  7594. if (nextCollapsed) {
  7595. this.sectionHeightsExpanded[i] =
  7596. currentHeight > collapsedHeight
  7597. ? currentHeight
  7598. : Number(this.sectionHeightsExpanded?.[i] ?? 190) || 190;
  7599. this.sectionHeights.splice(i, 1, collapsedHeight);
  7600. } else {
  7601. const restore =
  7602. Number(this.sectionHeightsExpanded?.[i] ?? 190) || 190;
  7603. this.sectionHeights.splice(i, 1, restore);
  7604. }
  7605. this.sectionCollapsed.splice(i, 1, nextCollapsed);
  7606. },
  7607. __syncSectionAutoCollapse(panelsInput) {
  7608. const sectionPanels = this.__getSectionPanelsForState(panelsInput);
  7609. const sectionCount = sectionPanels.length;
  7610. this.ensureSectionHeights(sectionCount);
  7611. const prevCounts = Array.isArray(this.sectionLastItemCounts)
  7612. ? this.sectionLastItemCounts.slice()
  7613. : [];
  7614. const nextCounts = Array.from({ length: sectionCount }, (_, i) =>
  7615. this.__getSectionPanelCount(sectionPanels[i])
  7616. );
  7617. this.sectionLastItemCounts = nextCounts;
  7618. sectionPanels.forEach((panel, index) => {
  7619. const count = Number(nextCounts[index] ?? 0) || 0;
  7620. const prevCount = Number(prevCounts[index] ?? 0) || 0;
  7621. const title = String(panel?.title ?? "").trim();
  7622. if (count <= 0) {
  7623. this.__setSectionCollapsedState(index, true);
  7624. return;
  7625. }
  7626. if (title === "已选" && count > prevCount) {
  7627. this.__setSectionCollapsedState(index, false);
  7628. }
  7629. });
  7630. },
  7631. toggleSectionCollapse(index) {
  7632. // 功能说明:双击 header 折叠/展开 section 面板(仅控制高度与内容渲染) by xu 20260116
  7633. const i = Number(index);
  7634. if (isNaN(i) || i < 0) return;
  7635. // 功能说明:加更细粒度日志,定位“多面板折叠无视觉效果”的根因(高度是否真的变、DOM 是否更新) by xu 20260116
  7636. const collapsedHeight = 37; // 功能说明:header(35) + panel 边框(2),避免 flex shrink 导致 header 变 25px by xu 20260116
  7637. const cur = !!this.sectionCollapsed?.[i];
  7638. console.log("[SsSidebar] toggleSectionCollapse", {
  7639. index: i,
  7640. to: !cur,
  7641. prevHeight: this.sectionHeights?.[i],
  7642. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7643. if (!cur) {
  7644. this.sectionHeightsExpanded[i] = this.sectionHeights[i] ?? 190;
  7645. this.sectionHeights.splice(i, 1, collapsedHeight);
  7646. } else {
  7647. const restore =
  7648. Number(this.sectionHeightsExpanded?.[i] ?? 190) || 190;
  7649. this.sectionHeights.splice(i, 1, restore);
  7650. }
  7651. this.sectionCollapsed.splice(i, 1, !cur);
  7652. console.log("[SsSidebar] section state(after)", {
  7653. index: i,
  7654. height: this.sectionHeights?.[i],
  7655. collapsed: this.sectionCollapsed?.[i],
  7656. allHeights: Array.from(this.sectionHeights || []),
  7657. allCollapsed: Array.from(this.sectionCollapsed || []),
  7658. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7659. Vue.nextTick(() => {
  7660. try {
  7661. const root = this.$el;
  7662. const sections = root?.querySelectorAll?.(".ss-sidebar-section");
  7663. const el = sections?.[i];
  7664. const rectH = el?.getBoundingClientRect?.().height;
  7665. console.log("[SsSidebar] section dom(beforeFix)", {
  7666. index: i,
  7667. styleHeight: el?.style?.height,
  7668. rectH,
  7669. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7670. // 功能说明:若渲染未把 height patch 到 DOM,则在 nextTick 强制同步一次(并打印) by xu 20260116
  7671. const targetH =
  7672. (Number(this.sectionHeights?.[i] ?? 190) || 190) + "px";
  7673. if (el && el.style && el.style.height !== targetH) {
  7674. el.style.height = targetH;
  7675. }
  7676. if (el?.classList) {
  7677. el.classList.toggle("is-collapsed", !!this.sectionCollapsed?.[i]);
  7678. }
  7679. const rectAfter = el?.getBoundingClientRect?.().height;
  7680. console.log("[SsSidebar] section dom(afterFix)", {
  7681. index: i,
  7682. styleHeight: el?.style?.height,
  7683. rectH: rectAfter,
  7684. targetH,
  7685. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7686. } catch (err) {
  7687. console.log("[SsSidebar] section dom(afterTick) error", err); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7688. }
  7689. });
  7690. },
  7691. toggleChartCollapse(index) {
  7692. // 功能说明:双击 header 折叠/展开底部 chart 面板(隐藏/显示 chart-hover) by xu 20260116
  7693. const i = Number(index);
  7694. if (isNaN(i) || i < 0) return;
  7695. const cur = !!this.chartCollapsed?.[i];
  7696. console.log("[SsSidebar] toggleChartCollapse", { index: i, to: !cur }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7697. this.chartCollapsed.splice(i, 1, !cur);
  7698. console.log("[SsSidebar] chart state(after)", {
  7699. index: i,
  7700. collapsed: this.chartCollapsed?.[i],
  7701. allCollapsed: Array.from(this.chartCollapsed || []),
  7702. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7703. Vue.nextTick(() => {
  7704. try {
  7705. const root = this.$el;
  7706. const el = root?.querySelector?.(
  7707. `.ss-sidebar-chart-panel[data-chart-idx="${i}"]`
  7708. );
  7709. console.log("[SsSidebar] chart dom(beforeFix)", {
  7710. index: i,
  7711. found: !!el,
  7712. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7713. if (el?.classList)
  7714. el.classList.toggle("is-collapsed", !!this.chartCollapsed?.[i]);
  7715. console.log("[SsSidebar] chart dom(afterFix)", {
  7716. index: i,
  7717. collapsed: !!this.chartCollapsed?.[i],
  7718. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7719. } catch (err) {
  7720. console.log("[SsSidebar] chart dom(afterTick) error", err); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7721. }
  7722. });
  7723. },
  7724. toggleReportCollapse(index) {
  7725. // 功能说明:双击 header 折叠/展开底部 report-table 面板(隐藏/显示表格) by xu 20260116
  7726. const i = Number(index);
  7727. if (isNaN(i) || i < 0) return;
  7728. const cur = !!this.reportCollapsed?.[i];
  7729. console.log("[SsSidebar] toggleReportCollapse", { index: i, to: !cur }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7730. this.reportCollapsed.splice(i, 1, !cur);
  7731. console.log("[SsSidebar] report state(after)", {
  7732. index: i,
  7733. collapsed: this.reportCollapsed?.[i],
  7734. allCollapsed: Array.from(this.reportCollapsed || []),
  7735. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7736. Vue.nextTick(() => {
  7737. try {
  7738. const root = this.$el;
  7739. const el = root?.querySelector?.(
  7740. `.ss-sidebar-report-panel[data-report-idx="${i}"]`
  7741. );
  7742. console.log("[SsSidebar] report dom(beforeFix)", {
  7743. index: i,
  7744. found: !!el,
  7745. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7746. if (el?.classList)
  7747. el.classList.toggle("is-collapsed", !!this.reportCollapsed?.[i]);
  7748. console.log("[SsSidebar] report dom(afterFix)", {
  7749. index: i,
  7750. collapsed: !!this.reportCollapsed?.[i],
  7751. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7752. } catch (err) {
  7753. console.log("[SsSidebar] report dom(afterTick) error", err); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7754. }
  7755. });
  7756. },
  7757. startResize(index, e) {
  7758. if (e?.preventDefault) e.preventDefault();
  7759. if (e?.stopPropagation) e.stopPropagation();
  7760. if (this.resizing) return;
  7761. // 长按 0.5s 后才进入拖拽调高度 by xu 20260106
  7762. clearTimeout(this.resizeTimer);
  7763. // 功能说明:修复“长按拖拽偶发失效/卡死”——pointerup 早于 500ms 时必须取消 timer,否则 timer 触发后进入 resizing=true 且再也收不到 pointerup,导致后续都无法拖拽 by xu 20260122
  7764. const pointerId = e?.pointerId;
  7765. const cancelPendingResize = (ev) => {
  7766. try {
  7767. if (
  7768. pointerId != null &&
  7769. ev?.pointerId != null &&
  7770. ev.pointerId !== pointerId
  7771. )
  7772. return;
  7773. } catch (_) {}
  7774. clearTimeout(this.resizeTimer);
  7775. this.resizeTimer = null;
  7776. document.removeEventListener("pointerup", cancelPendingResize, true);
  7777. document.removeEventListener(
  7778. "pointercancel",
  7779. cancelPendingResize,
  7780. true
  7781. );
  7782. };
  7783. // 功能说明:用 document+capture 监听,避免 window 监听在部分容器/iframe 场景丢事件导致拖拽无响应 by xu 20260122
  7784. document.addEventListener("pointerup", cancelPendingResize, {
  7785. passive: false,
  7786. once: true,
  7787. capture: true,
  7788. });
  7789. document.addEventListener("pointercancel", cancelPendingResize, {
  7790. passive: false,
  7791. once: true,
  7792. capture: true,
  7793. });
  7794. const startY = e?.clientY ?? 0;
  7795. const gapEl = e?.currentTarget;
  7796. // 功能说明:记录 gap 前后 section 节点,拖拽过程中直接写入 style.height,避免某些环境下 Vue render 不刷新导致“拖拽无视觉变化” by xu 20260122
  7797. this.__resizePrevSectionEl = gapEl?.previousElementSibling || null;
  7798. this.__resizeNextSectionEl = gapEl?.nextElementSibling || null;
  7799. this.resizeTimer = setTimeout(() => {
  7800. // 定时器触发后进入拖拽态,移除“取消等待”监听,避免误取消 by xu 20260122
  7801. document.removeEventListener("pointerup", cancelPendingResize, true);
  7802. document.removeEventListener(
  7803. "pointercancel",
  7804. cancelPendingResize,
  7805. true
  7806. );
  7807. this.resizing = true;
  7808. this.resizeIndex = index;
  7809. this.resizeStartY = startY;
  7810. this.resizeStartPrev = this.sectionHeights[index] ?? 190;
  7811. this.resizeStartNext = this.sectionHeights[index + 1] ?? 190;
  7812. gapEl?.classList?.add("is-active");
  7813. try {
  7814. if (gapEl?.setPointerCapture && pointerId != null)
  7815. gapEl.setPointerCapture(pointerId);
  7816. } catch (_) {} // 功能说明:捕获 pointer,避免拖拽过程中移出窗口导致 pointermove 丢失 by xu 20260122
  7817. // 功能说明:用 document+capture,确保拖拽时 pointermove/up 不会被中途 stopPropagation 影响 by xu 20260122
  7818. document.addEventListener(
  7819. "pointermove",
  7820. this.__resizeMoveHandler || this.onResizeMove,
  7821. { passive: false, capture: true }
  7822. );
  7823. document.addEventListener(
  7824. "pointerup",
  7825. this.__resizeEndHandler || this.endResize,
  7826. { passive: false, once: true, capture: true }
  7827. );
  7828. document.addEventListener(
  7829. "pointercancel",
  7830. this.__resizeCancelHandler || this.endResize,
  7831. { passive: false, once: true, capture: true }
  7832. ); // 功能说明:触控/手势取消时也要结束拖拽 by xu 20260122
  7833. }, 500);
  7834. },
  7835. onResizeMove(e) {
  7836. if (!this.resizing) return;
  7837. if (e?.preventDefault) e.preventDefault();
  7838. const dy = (e?.clientY ?? 0) - this.resizeStartY;
  7839. const minPanelHeight = 83; // header(35) + listMin(48) by xu 20260106
  7840. const prev = Math.max(minPanelHeight, this.resizeStartPrev + dy);
  7841. const next = Math.max(minPanelHeight, this.resizeStartNext - dy);
  7842. // 若其中一个达到最小值,则停止继续挤压 by xu 20260106
  7843. const adjustedDy = prev - this.resizeStartPrev;
  7844. const nextAdjusted = this.resizeStartNext - adjustedDy;
  7845. const appliedPrev = prev;
  7846. const appliedNext = Math.max(minPanelHeight, nextAdjusted);
  7847. this.sectionHeights.splice(this.resizeIndex, 1, appliedPrev);
  7848. this.sectionHeights.splice(this.resizeIndex + 1, 1, appliedNext);
  7849. // 功能说明:强制触发一次 sectionHeights 引用变化,避免某些运行时环境下 splice 未触发视图更新导致“拖拽没反应” by xu 20260122
  7850. this.sectionHeights = (this.sectionHeights || []).slice();
  7851. // 功能说明:兜底——直接写 DOM 的 height,确保视觉立即响应(用于排查/修复某些环境下 render 不更新) by xu 20260122
  7852. try {
  7853. if (this.__resizePrevSectionEl?.style)
  7854. this.__resizePrevSectionEl.style.height = appliedPrev + "px";
  7855. if (this.__resizeNextSectionEl?.style)
  7856. this.__resizeNextSectionEl.style.height = appliedNext + "px";
  7857. } catch (_) {}
  7858. },
  7859. endResize(e) {
  7860. clearTimeout(this.resizeTimer);
  7861. this.resizeTimer = null;
  7862. if (!this.resizing) return;
  7863. if (e?.preventDefault) e.preventDefault();
  7864. const activeGaps = document.querySelectorAll(
  7865. ".ss-sidebar-gap.is-active"
  7866. );
  7867. activeGaps.forEach((g) => g.classList.remove("is-active"));
  7868. this.resizing = false;
  7869. this.resizeIndex = -1;
  7870. this.__resizePrevSectionEl = null; // 功能说明:释放 DOM 引用,避免内存泄漏 by xu 20260122
  7871. this.__resizeNextSectionEl = null; // 功能说明:释放 DOM 引用,避免内存泄漏 by xu 20260122
  7872. document.removeEventListener(
  7873. "pointermove",
  7874. this.__resizeMoveHandler || this.onResizeMove,
  7875. true
  7876. );
  7877. document.removeEventListener(
  7878. "pointercancel",
  7879. this.__resizeCancelHandler || this.endResize,
  7880. true
  7881. ); // 功能说明:清理 cancel 监听,避免残留 by xu 20260122
  7882. },
  7883. },
  7884. watch: {
  7885. panels: {
  7886. handler(nextPanels) {
  7887. this.__syncSectionAutoCollapse(nextPanels);
  7888. },
  7889. deep: true,
  7890. immediate: true,
  7891. },
  7892. list: {
  7893. handler(nextList) {
  7894. if ((this.panels || []).length) return;
  7895. this.__syncSectionAutoCollapse(nextList);
  7896. },
  7897. deep: true,
  7898. immediate: true,
  7899. },
  7900. },
  7901. mounted() {
  7902. clearTimeout(this.resizeTimer);
  7903. this.resizeTimer = null;
  7904. // 功能说明:绑定拖拽事件 handler(用于 add/removeEventListener) by xu 20260122
  7905. if (!this.__resizeMoveHandler)
  7906. this.__resizeMoveHandler = (e) => this.onResizeMove?.(e);
  7907. if (!this.__resizeEndHandler)
  7908. this.__resizeEndHandler = (e) => this.endResize?.(e);
  7909. if (!this.__resizeCancelHandler)
  7910. this.__resizeCancelHandler = (e) => this.endResize?.(e);
  7911. // 功能说明:暂时回退为固定底部留白方案(CSS 控制),后续再定位遮挡根因 by xu 20260115
  7912. },
  7913. beforeUnmount() {
  7914. clearTimeout(this.resizeTimer);
  7915. this.resizeTimer = null;
  7916. document.removeEventListener(
  7917. "pointermove",
  7918. this.__resizeMoveHandler || this.onResizeMove,
  7919. true
  7920. );
  7921. document.removeEventListener(
  7922. "pointerup",
  7923. this.__resizeEndHandler || this.endResize,
  7924. true
  7925. );
  7926. document.removeEventListener(
  7927. "pointercancel",
  7928. this.__resizeCancelHandler || this.endResize,
  7929. true
  7930. );
  7931. },
  7932. render() {
  7933. const SsSidebarButtonsComp = Vue.resolveComponent("ss-sidebar-buttons");
  7934. const SsSidebarChartComp = Vue.resolveComponent("ss-sidebar-chart");
  7935. const SsSidebarListComp = Vue.resolveComponent("ss-sidebar-list");
  7936. const SsSidebarReportTableComp = Vue.resolveComponent(
  7937. "ss-sidebar-report-table"
  7938. );
  7939. const SsIcon = Vue.resolveComponent("ss-icon");
  7940. // 支持 panels(多分区),list/listMode 作为 legacy 兜底 by xu 20260106
  7941. const panels = (this.panels || []).length
  7942. ? this.panels
  7943. : this.list?.length
  7944. ? [
  7945. {
  7946. type: "list",
  7947. title: "已选",
  7948. icon: "",
  7949. mode: this.listMode,
  7950. items: this.list,
  7951. },
  7952. ]
  7953. : [];
  7954. // 功能说明:右侧栏强制移除“对象”tab(兼容后端返回 rbarObj/rbarobj 或直接返回中文“对象”标题) by xu 20260116
  7955. const panelsNoObj = (panels || []).filter((p) => {
  7956. const k = String(p?._tabKey ?? "")
  7957. .trim()
  7958. .toLowerCase();
  7959. const t = String(p?.title ?? "").trim();
  7960. if (k === "rbarobj") return false;
  7961. if (t === "对象") return false;
  7962. return true;
  7963. });
  7964. // 功能说明:report-table 作为底部报表区(放在统计图下面),不参与可拖拽 section 面板 by xu 20260115
  7965. const sectionPanels = panelsNoObj.filter(
  7966. (p) => p?.type !== "chart" && p?.type !== "report-table"
  7967. );
  7968. const chartPanels = panelsNoObj.filter((p) => p?.type === "chart");
  7969. const reportPanels = panelsNoObj.filter(
  7970. (p) => p?.type === "report-table"
  7971. );
  7972. this.ensureSectionHeights(sectionPanels.length);
  7973. // 功能说明:补齐 chart/report 折叠数组长度 by xu 20260116
  7974. this.chartCollapsed = Array.from(
  7975. { length: chartPanels.length },
  7976. (_, i) => !!this.chartCollapsed?.[i]
  7977. );
  7978. this.chartHeaderTitleDownAt = Array.from(
  7979. { length: chartPanels.length },
  7980. (_, i) => this.chartHeaderTitleDownAt?.[i] ?? 0
  7981. );
  7982. this.reportCollapsed = Array.from(
  7983. { length: reportPanels.length },
  7984. (_, i) => !!this.reportCollapsed?.[i]
  7985. );
  7986. return Vue.h("div", { class: "ss-sidebar" }, [
  7987. this.buttons?.length
  7988. ? Vue.h(SsSidebarButtonsComp, { items: this.buttons })
  7989. : null,
  7990. Vue.h(
  7991. "div",
  7992. { class: "ss-sidebar__inner" },
  7993. [
  7994. ...(this.charts || []).map((c) =>
  7995. Vue.h(SsSidebarChartComp, {
  7996. options: c?.options || {},
  7997. height: c?.height || "200px",
  7998. })
  7999. ),
  8000. // 可拖拽的业务面板容器 by xu 20260106
  8001. Vue.h(
  8002. "div",
  8003. { class: "ss-sidebar-sections", style: { flex: "0 0 auto" } },
  8004. sectionPanels.flatMap((p, idx) => {
  8005. const panelContent =
  8006. p?.type === "report-table"
  8007. ? Vue.h(SsSidebarReportTableComp, {
  8008. key: `ss-sidebar-report-in-section-${idx}-${
  8009. p?.title ?? ""
  8010. }`,
  8011. title: p?.title ?? "",
  8012. icon: p?.icon ?? "",
  8013. iconClass: p?.iconClass ?? "",
  8014. items: p?.items || [],
  8015. onOpen: (srv, ctx) => p?.onOpen?.(srv, ctx),
  8016. })
  8017. : Vue.h(SsSidebarListComp, {
  8018. key: `ss-sidebar-list-${idx}-${p?.title ?? ""}`,
  8019. title: p?.title ?? "",
  8020. icon: p?.icon ?? "",
  8021. count: p?.count ?? p?.items?.length ?? "",
  8022. closable: !!p?.closable,
  8023. searchable: !!p?.searchable,
  8024. searchInHeader: !!p?.searchInHeader,
  8025. headerFilters: p?.headerFilters || [],
  8026. headerSearchButton: !!p?.headerSearchButton,
  8027. searchPlaceholder: p?.searchPlaceholder ?? "搜索",
  8028. itemLayout: p?.itemLayout ?? "simple",
  8029. itemAction: p?.itemAction ?? true,
  8030. collapsible: true,
  8031. collapsed: !!this.sectionCollapsed?.[idx],
  8032. onToggleCollapse: () =>
  8033. this.toggleSectionCollapse?.(idx),
  8034. iconClass: p?.iconClass ?? "",
  8035. items: p?.items || [],
  8036. mode: p?.mode || "search",
  8037. onSelect: (item) => this.$emit("select", item),
  8038. onRemove: (item) => this.$emit("remove", item),
  8039. onClear: () => p?.onClear?.(),
  8040. onSearch: (payload) => p?.onSearch?.(payload),
  8041. });
  8042. const section = Vue.h(
  8043. "div",
  8044. {
  8045. class: {
  8046. "ss-sidebar-section": true,
  8047. "is-collapsed": !!this.sectionCollapsed?.[idx],
  8048. },
  8049. key: `ss-sidebar-section-${idx}-${p?.type ?? "list"}-${
  8050. p?.title ?? ""
  8051. }`,
  8052. style: {
  8053. height: (this.sectionHeights[idx] ?? 190) + "px",
  8054. flex: "0 0 auto",
  8055. },
  8056. },
  8057. [
  8058. Vue.h("div", { class: "ss-sidebar-section__content" }, [
  8059. panelContent,
  8060. ]),
  8061. ]
  8062. );
  8063. const gap =
  8064. idx < sectionPanels.length - 1
  8065. ? Vue.h("div", {
  8066. class: "ss-sidebar-gap",
  8067. onPointerdown: (e) => this.startResize(idx, e),
  8068. })
  8069. : null;
  8070. return gap ? [section, gap] : [section];
  8071. })
  8072. ),
  8073. // 图表区固定在底部(hover 弹出大图) by xu 20260106
  8074. ...chartPanels.map((p, chartIdx) =>
  8075. Vue.h(
  8076. "div",
  8077. {
  8078. class: {
  8079. "ss-sidebar-panel": true,
  8080. "ss-sidebar-chart-panel": true,
  8081. "is-collapsed": !!this.chartCollapsed?.[chartIdx],
  8082. },
  8083. style: { flex: "0 0 auto", minHeight: "37px" },
  8084. "data-chart-idx": chartIdx,
  8085. key: `ss-sidebar-chart-${chartIdx}-${p?.title ?? ""}`,
  8086. },
  8087. [
  8088. p?.title
  8089. ? Vue.h(
  8090. "div",
  8091. {
  8092. class: "ss-sidebar-panel__header",
  8093. onDblclick: (e) => {
  8094. e?.preventDefault?.();
  8095. e?.stopPropagation?.();
  8096. console.log("[SsSidebar] chart header dblclick", {
  8097. idx: chartIdx,
  8098. title: p?.title,
  8099. });
  8100. this.toggleChartCollapse?.(chartIdx);
  8101. },
  8102. },
  8103. [
  8104. Vue.h("div", { class: "ss-sidebar-panel__title" }, [
  8105. p?.iconClass
  8106. ? Vue.h(SsIcon, {
  8107. class:
  8108. p.iconClass + " ss-sidebar-panel__icon",
  8109. })
  8110. : p?.icon
  8111. ? Vue.h(SsIcon, {
  8112. name: p.icon,
  8113. size: "16px",
  8114. class: "ss-sidebar-panel__icon",
  8115. })
  8116. : null,
  8117. Vue.h("span", null, p.title),
  8118. ]),
  8119. Vue.h("div", { class: "ss-sidebar-panel__tools" }),
  8120. ]
  8121. )
  8122. : null,
  8123. this.chartCollapsed?.[chartIdx]
  8124. ? null
  8125. : Vue.h(Vue.resolveComponent("ss-sidebar-chart-hover"), {
  8126. title: p?.title ?? "",
  8127. iconClass: p?.iconClass ?? "",
  8128. icon: p?.icon ?? "",
  8129. options: p?.options || {},
  8130. height: p?.height || "240px",
  8131. }),
  8132. ]
  8133. )
  8134. ),
  8135. ...reportPanels.map((p, reportIdx) =>
  8136. Vue.h(
  8137. "div",
  8138. {
  8139. class: {
  8140. "ss-sidebar-report-panel-wrap": true,
  8141. "ss-sidebar-report-panel": true,
  8142. "is-collapsed": !!this.reportCollapsed?.[reportIdx],
  8143. },
  8144. style: { flex: "0 0 auto", minHeight: "37px" },
  8145. "data-report-idx": reportIdx,
  8146. key: `ss-sidebar-report-wrap-${reportIdx}-${p?.title ?? ""}`,
  8147. },
  8148. [
  8149. Vue.h(SsSidebarReportTableComp, {
  8150. key: `ss-sidebar-report-${reportIdx}-${p?.title ?? ""}`,
  8151. title: p?.title ?? "",
  8152. icon: p?.icon ?? "",
  8153. iconClass: p?.iconClass ?? "",
  8154. items: p?.items || [],
  8155. collapsible: true,
  8156. collapsed: !!this.reportCollapsed?.[reportIdx],
  8157. onToggleCollapse: () =>
  8158. this.toggleReportCollapse?.(reportIdx),
  8159. onOpen: (srv, ctx) => p?.onOpen?.(srv, ctx),
  8160. }),
  8161. ]
  8162. )
  8163. ),
  8164. ].filter(Boolean)
  8165. ),
  8166. ]);
  8167. },
  8168. };
  8169. // ss-folder-card 文件夹卡片
  8170. const SsFolderCard = {
  8171. name: "SsFolderCard",
  8172. props: {
  8173. item: {
  8174. type: Object,
  8175. required: true,
  8176. },
  8177. },
  8178. data() {
  8179. return {
  8180. showButtons: false,
  8181. };
  8182. },
  8183. emits: ["click", "change"],
  8184. setup(props, { emit }) {
  8185. const item = props.item;
  8186. const showChildren = ref(false);
  8187. const eventBus = window.parent.sharedEventBus;
  8188. const itemWidth = Vue.computed(() => {
  8189. // 功能说明:页面改为 grid 等分布局后,卡片宽度交给容器控制,这里固定 100% by xu 20260116
  8190. return "100%";
  8191. });
  8192. onMounted(() => {
  8193. eventBus.subscribe("folderPath", (path) => {
  8194. const currentPath = path || [];
  8195. // 如果当前文件夹不在路径中,则销毁视图
  8196. if (
  8197. !currentPath.some((item) => item.folder.title === props.item.title)
  8198. ) {
  8199. showChildren.value = false;
  8200. }
  8201. });
  8202. });
  8203. const onItemClick = (e) => {
  8204. if (e && e.stopPropagation) {
  8205. e.stopPropagation();
  8206. }
  8207. // 单击只处理 active 状态
  8208. if (e && e.currentTarget) {
  8209. const allListCards = document.querySelectorAll(
  8210. ".knowledge-item-container"
  8211. );
  8212. const allFolderCards = document.querySelectorAll(".ss-folder-list");
  8213. allListCards.forEach((card) => card.classList.remove("active"));
  8214. allFolderCards.forEach((card) => card.classList.remove("active"));
  8215. e.currentTarget.classList.add("active");
  8216. } else {
  8217. // 如果是数据对象,需要找到对应的 DOM 元素
  8218. const allListCards = document.querySelectorAll(
  8219. ".knowledge-item-container"
  8220. );
  8221. const allFolderCards = document.querySelectorAll(".ss-folder-list");
  8222. allListCards.forEach((card) => card.classList.remove("active"));
  8223. allFolderCards.forEach((card) => card.classList.remove("active"));
  8224. // 找到标题匹配的文件夹元素
  8225. const targetFolder = Array.from(allFolderCards).find((card) =>
  8226. card.textContent.includes(e.title)
  8227. );
  8228. if (targetFolder) {
  8229. targetFolder.classList.add("active");
  8230. }
  8231. }
  8232. emit("click", item);
  8233. };
  8234. // 修改双击处理函数
  8235. const handleFolderDblClick = (folder, e) => {
  8236. if (e) e.stopPropagation();
  8237. if (folder.children?.length) {
  8238. showChildren.value = true;
  8239. const pathInfo = {
  8240. title: folder.title,
  8241. folder: folder,
  8242. };
  8243. const currentPath = eventBus.getState("folderPath") || [];
  8244. if (!currentPath.some((item) => item.title === folder.title)) {
  8245. eventBus.publish("folderPath", [...currentPath, pathInfo]);
  8246. }
  8247. }
  8248. };
  8249. const onItemChange = (e, icon, index) => {
  8250. e.stopPropagation();
  8251. props.item.buttons[0].onclick();
  8252. // emit("change", { item: props.item, icon, index });
  8253. };
  8254. return {
  8255. item,
  8256. itemWidth,
  8257. showChildren,
  8258. onItemClick,
  8259. onItemChange,
  8260. handleFolderDblClick,
  8261. };
  8262. },
  8263. render() {
  8264. const SsCartListIcon = Vue.resolveComponent("ss-cart-list-icon");
  8265. if (this.showChildren) {
  8266. return h(SsFolderCartView, {
  8267. folder: this.item,
  8268. });
  8269. }
  8270. return Vue.h(
  8271. "div",
  8272. {
  8273. class: { "ss-folder-list": true, active: this.item.active },
  8274. onClick: (e) => {
  8275. e.stopPropagation();
  8276. this.onItemClick(e);
  8277. },
  8278. onDblclick: (e) => this.handleFolderDblClick(this.item, e),
  8279. style: { width: this.itemWidth },
  8280. },
  8281. [
  8282. // 文件夹特有的装饰元素
  8283. Vue.h("div", { class: "ss-folder-list-trapezoid" }),
  8284. Vue.h("div", { class: "ss-folder-list-top-transparent" }),
  8285. Vue.h("div", { class: "ss-folder-list-top" }),
  8286. Vue.h("div", { class: "ss-folder-list-right" }),
  8287. // header 部分(按钮)
  8288. this.item?.buttons?.length > 0 &&
  8289. Vue.h(
  8290. "div",
  8291. {
  8292. class: "header",
  8293. onMouseenter: () => (this.showButtons = true),
  8294. onMouseleave: () => (this.showButtons = false),
  8295. onClick: (e) => this.onItemChange(e, this.item.buttons[0], 0),
  8296. },
  8297. [
  8298. // this.item?.buttons?.length > 0 &&
  8299. Vue.h("div", {
  8300. class: "cart-list-setting cart-list-icon",
  8301. title: this.item?.buttons?.[0]?.title,
  8302. }),
  8303. // this.item?.buttons?.length > 0 &&
  8304. this.showButtons &&
  8305. this.item?.buttons?.length > 1 &&
  8306. Vue.h(
  8307. "div",
  8308. {
  8309. class: "cart-list-button-popup",
  8310. },
  8311. this.item.buttons.map((btn) =>
  8312. Vue.h(
  8313. "div",
  8314. {
  8315. onClick: (e) => {
  8316. e.stopPropagation();
  8317. btn.onclick?.();
  8318. },
  8319. },
  8320. [
  8321. btn.class &&
  8322. Vue.h(SsCartListIcon, {
  8323. class: [btn.class],
  8324. }),
  8325. Vue.h("span", null, btn.title),
  8326. ]
  8327. )
  8328. )
  8329. ),
  8330. ]
  8331. ),
  8332. // body 部分
  8333. Vue.h("div", { class: "body" }, [
  8334. Vue.h("div", { class: "box-header" }, [
  8335. Vue.h("div", null, this.item.title),
  8336. ]),
  8337. Vue.h(
  8338. "div",
  8339. {
  8340. class: !this.item.thumb ? "no-thumb box-body" : "box-body",
  8341. },
  8342. [
  8343. this.item.thumb
  8344. ? Vue.h("div", { class: "left" }, [
  8345. Vue.h("img", {
  8346. src: this.item.thumb,
  8347. alt: "Thumbnail",
  8348. class: "imgUnHandle",
  8349. style: {
  8350. "object-fit": "cover",
  8351. width: "100%",
  8352. height: "100%",
  8353. },
  8354. }),
  8355. ])
  8356. : null,
  8357. Vue.h("div", { class: "right" }, [
  8358. ...this.item.tags.map((tag) => {
  8359. const [key, value] = Object.entries(tag)[0];
  8360. return Vue.h(
  8361. "div",
  8362. {
  8363. class: "title",
  8364. title: `${key}: ${value}`,
  8365. },
  8366. `${key}: ${value}`
  8367. );
  8368. }),
  8369. ]),
  8370. ]
  8371. ),
  8372. ]),
  8373. ]
  8374. );
  8375. },
  8376. };
  8377. // SsFolderCartView 组件 - 用于显示文件夹内容
  8378. const SsFolderCartView = {
  8379. name: "SsFolderCartView",
  8380. props: {
  8381. folder: {
  8382. type: Object,
  8383. required: true,
  8384. },
  8385. },
  8386. emits: ["click"],
  8387. setup(props, { emit }) {
  8388. const eventBus = window.parent.sharedEventBus;
  8389. const currentFolder = ref(props.folder);
  8390. const showChildren = ref(false);
  8391. const onItemClick = (e) => {
  8392. if (e && e.stopPropagation) {
  8393. e.stopPropagation();
  8394. }
  8395. // 单击只处理 active 状态
  8396. if (e && e.currentTarget) {
  8397. const allListCards = document.querySelectorAll(
  8398. ".knowledge-item-container"
  8399. );
  8400. const allFolderCards = document.querySelectorAll(".ss-folder-list");
  8401. allListCards.forEach((card) => card.classList.remove("active"));
  8402. allFolderCards.forEach((card) => card.classList.remove("active"));
  8403. e.currentTarget.classList.add("active");
  8404. } else {
  8405. // 如果是数据对象,需要找到对应的 DOM 元素
  8406. const allListCards = document.querySelectorAll(
  8407. ".knowledge-item-container"
  8408. );
  8409. const allFolderCards = document.querySelectorAll(".ss-folder-list");
  8410. allListCards.forEach((card) => card.classList.remove("active"));
  8411. allFolderCards.forEach((card) => card.classList.remove("active"));
  8412. // 找到标题匹配的文件夹元素
  8413. const targetFolder = Array.from(allFolderCards).find((card) =>
  8414. card.textContent.includes(e.title)
  8415. );
  8416. if (targetFolder) {
  8417. targetFolder.classList.add("active");
  8418. }
  8419. }
  8420. emit("click", props.folder);
  8421. };
  8422. const handleFolderDblClick = (folder, e) => {
  8423. if (e) e.stopPropagation();
  8424. if (folder.children?.length) {
  8425. showChildren.value = true;
  8426. const pathInfo = {
  8427. title: folder.title,
  8428. folder: folder,
  8429. };
  8430. const currentPath = eventBus.getState("folderPath") || [];
  8431. if (!currentPath.some((item) => item.title === folder.title)) {
  8432. eventBus.publish("folderPath", [...currentPath, pathInfo]);
  8433. currentFolder.value = folder;
  8434. }
  8435. }
  8436. };
  8437. const goBack = (targetFolder) => {
  8438. if (targetFolder === null) {
  8439. // 返回根目录
  8440. eventBus.publish("folderPath", []);
  8441. } else {
  8442. currentFolder.value = targetFolder;
  8443. }
  8444. };
  8445. return {
  8446. currentFolder,
  8447. showChildren,
  8448. onItemClick,
  8449. handleFolderDblClick,
  8450. goBack,
  8451. };
  8452. },
  8453. render() {
  8454. return h(
  8455. "div",
  8456. {
  8457. class: "page-container",
  8458. style: {
  8459. position: "fixed",
  8460. top: 0,
  8461. left: 0,
  8462. width: "100%",
  8463. height: "100%",
  8464. background: "var(--lightgray)",
  8465. padding: "20px 0",
  8466. zIndex: 1000,
  8467. },
  8468. },
  8469. [
  8470. // 搜索栏
  8471. h("div", { class: "search-bar" }, [
  8472. h("div", { class: "search-bar-contaienr" }, [
  8473. h(SsBreadcrumb, {
  8474. level: {
  8475. onBack: this.goBack,
  8476. },
  8477. }),
  8478. ]),
  8479. ]),
  8480. // 内容区域
  8481. h(
  8482. "div",
  8483. {
  8484. class: "content-area item-content-area",
  8485. style: { gap: "20px" },
  8486. },
  8487. [
  8488. ...(this.currentFolder.children || []).map((child, index) =>
  8489. h(child.children ? SsFolderCard : SsListCard, {
  8490. key: index,
  8491. item: child,
  8492. onClick: (e) => this.onItemClick(e),
  8493. onDblclick: (e) => this.handleFolderDblClick(child, e),
  8494. })
  8495. ),
  8496. ]
  8497. ),
  8498. ]
  8499. );
  8500. },
  8501. };
  8502. // ss-page分页
  8503. const SsPage = {
  8504. name: "SsPage",
  8505. props: {
  8506. total: {
  8507. type: Number,
  8508. required: true,
  8509. },
  8510. size: {
  8511. type: Number,
  8512. default: 10,
  8513. },
  8514. page: {
  8515. type: Number,
  8516. default: 1,
  8517. },
  8518. onChange: {
  8519. type: Function,
  8520. default: () => {},
  8521. },
  8522. },
  8523. setup(props) {
  8524. const totalItems = ref(props.total); // 总条目数
  8525. const totalPages = ref(Math.ceil(props.total / props.size));
  8526. const currentPage = ref(props.page); // 当前页码
  8527. // 计算显示的信息
  8528. const pageInfo = ref(
  8529. `共${totalItems.value}条,第 ${currentPage.value}/${totalPages.value} 页`
  8530. );
  8531. // 上一页的逻辑
  8532. const goToPreviousPage = (e) => {
  8533. e.preventDefault(); // 阻止默认行为
  8534. if (currentPage.value > 1) {
  8535. currentPage.value -= 1;
  8536. updatePageInfo();
  8537. props.onChange?.({
  8538. pageNo: currentPage.value, // 当前页码
  8539. rowNumPer: props.size, // 每页条数
  8540. rowNum: props.total, // 总记录数
  8541. });
  8542. }
  8543. };
  8544. // 下一页的逻辑
  8545. const goToNextPage = (e) => {
  8546. e.preventDefault(); // 阻止默认行为
  8547. if (currentPage.value < totalPages.value) {
  8548. currentPage.value += 1;
  8549. updatePageInfo();
  8550. props.onChange?.({
  8551. pageNo: currentPage.value, // 当前页码
  8552. rowNumPer: props.size, // 每页条数
  8553. rowNum: props.total, // 总记录数
  8554. });
  8555. }
  8556. };
  8557. // 更新页码信息的函数
  8558. const updatePageInfo = () => {
  8559. pageInfo.value = `共${totalItems.value}条,第 ${currentPage.value}/${totalPages.value} 页`;
  8560. };
  8561. return {
  8562. pageInfo,
  8563. totalPages,
  8564. goToPreviousPage,
  8565. goToNextPage,
  8566. };
  8567. },
  8568. render(props, { slots, emit }) {
  8569. return Vue.h("div", { class: "pager-container" }, [
  8570. Vue.h("input", { type: "hidden", name: "rowNum", value: props.total }),
  8571. Vue.h("input", {
  8572. type: "hidden",
  8573. name: "rowNumPer",
  8574. value: props.size,
  8575. }),
  8576. Vue.h("input", {
  8577. type: "hidden",
  8578. name: "pageCount",
  8579. value: this.totalPages,
  8580. }),
  8581. Vue.h("input", { type: "hidden", name: "pageNo", value: props.page }),
  8582. Vue.h("div", { class: "pager-content" }, [
  8583. Vue.h("div", { class: "info" }, this.pageInfo),
  8584. Vue.h(
  8585. "div",
  8586. { class: "btn" },
  8587. Vue.h(
  8588. "button",
  8589. { onClick: (e) => this.goToPreviousPage(e) },
  8590. "上一页"
  8591. )
  8592. ),
  8593. Vue.h(
  8594. "div",
  8595. { class: "btn" },
  8596. Vue.h("button", { onClick: (e) => this.goToNextPage(e) }, "下一页")
  8597. ),
  8598. ]),
  8599. ]);
  8600. },
  8601. };
  8602. // ss-right-info 一级页面右边栏
  8603. const SSRightInfo = {
  8604. name: "SSRightInfo",
  8605. setup() {
  8606. // 初始化响应式数据
  8607. const item = ref({
  8608. thumb: "images/example/project-img.png", // 更换为适合你项目的实际路径
  8609. title: "工业和信息化产业高质量发展资金",
  8610. });
  8611. return {
  8612. item,
  8613. };
  8614. },
  8615. render() {
  8616. return Vue.h("div", { class: "info-container" }, [
  8617. Vue.h("div", { class: "header" }, [
  8618. Vue.h("div", [
  8619. Vue.h("img", {
  8620. src: this.item.thumb,
  8621. class: "imgUnHandle",
  8622. style: { "object-fit": "cover", width: "100%", height: "100%" },
  8623. }), // 将 ImageViewer 替换为 img 标签
  8624. ]),
  8625. Vue.h("div", [Vue.h("div", this.item.title)]),
  8626. ]),
  8627. Vue.h("div", { class: "section-container" }, [
  8628. Vue.h("div", { class: "section" }, [
  8629. Vue.h("div", { class: "title" }, "合同"),
  8630. Vue.h("div", { class: "text" }, "合同总金额:42,399,320"),
  8631. Vue.h(
  8632. "div",
  8633. { class: "a" },
  8634. "《工业和信息化产业高质量发展资金补助合同》"
  8635. ),
  8636. ]),
  8637. Vue.h("div", { class: "section" }, [
  8638. Vue.h("div", { class: "title" }, "发票"),
  8639. Vue.h("div", { class: "text" }, "应开发票总额:42,399,320"),
  8640. Vue.h("div", { class: "text" }, "已开发票金额:17,235,345"),
  8641. Vue.h("div", { class: "text" }, "未开发票金额:25,163,975"),
  8642. ]),
  8643. Vue.h("div", { class: "section" }, [
  8644. Vue.h("div", { class: "title" }, "项目组成员"),
  8645. Vue.h("div", { class: "text" }, "我司:3人"),
  8646. Vue.h("div", { class: "text" }, "对方:2人"),
  8647. Vue.h("div", { class: "text" }, "项目负责人:张三"),
  8648. ]),
  8649. Vue.h("div", { class: "section" }, [
  8650. Vue.h("div", { class: "title" }, "采购"),
  8651. Vue.h("div", { class: "text" }, "总额:999,320"),
  8652. Vue.h("div", { class: "text" }, "已付金额:335,345"),
  8653. Vue.h("div", { class: "text" }, "未付金额:663,975"),
  8654. ]),
  8655. ]),
  8656. ]);
  8657. },
  8658. };
  8659. //
  8660. const SsSuccessPopup = {
  8661. name: "SsSuccessPopup",
  8662. props: {
  8663. right: {
  8664. type: String,
  8665. default: "20px",
  8666. },
  8667. bottom: {
  8668. type: String,
  8669. default: "calc(100% + 5px)",
  8670. },
  8671. },
  8672. setup(props, { expose }) {
  8673. // 响应式状态:是否可见
  8674. const visible = ref(false);
  8675. // 计算样式
  8676. const style = computed(() => {
  8677. return {
  8678. "--message-dialog-right": props.right,
  8679. "--message-dialog-bottom": props.bottom,
  8680. };
  8681. });
  8682. // 显示对话框的方法
  8683. const show = () => {
  8684. visible.value = true;
  8685. };
  8686. // 隐藏对话框的方法
  8687. const hide = () => {
  8688. visible.value = false;
  8689. };
  8690. // 将方法暴露给外部使用
  8691. expose({ show, hide });
  8692. // 返回渲染函数
  8693. return () => {
  8694. if (!visible.value) return null;
  8695. const SsIcon = resolveComponent("ss-icon");
  8696. return h(
  8697. "div",
  8698. {
  8699. class: "success-popup",
  8700. style: style.value,
  8701. onClick: (e) => e.stopPropagation(),
  8702. },
  8703. [
  8704. h("div", { class: "left" }, [
  8705. h("div", { class: "icon" }, [
  8706. h(SsIcon, { name: "check", size: "36px" }),
  8707. ]),
  8708. ]),
  8709. h("div", { class: "right" }, [
  8710. h("div", { class: "title" }, "提交成功"),
  8711. h("div", { class: "desc" }, "您的信息已成功提交。"),
  8712. ]),
  8713. ]
  8714. );
  8715. };
  8716. },
  8717. };
  8718. const SsErrorDialog = {
  8719. name: "SsErrorDialog",
  8720. setup(props, { emit }) {
  8721. const visible = ref(false);
  8722. const style = computed(() => {
  8723. return {};
  8724. });
  8725. const show = () => {
  8726. visible.value = true;
  8727. };
  8728. const hide = () => {
  8729. visible.value = false;
  8730. };
  8731. const onBack = () => {
  8732. emit("back");
  8733. hide();
  8734. };
  8735. return {
  8736. visible,
  8737. style,
  8738. show,
  8739. hide,
  8740. onBack,
  8741. };
  8742. },
  8743. render() {
  8744. const SsIcon = resolveComponent("ss-icon");
  8745. return this.visible
  8746. ? h(
  8747. "div",
  8748. {
  8749. class: "errorDialog",
  8750. style: this.style,
  8751. onClick: (event) => event.stopPropagation(),
  8752. },
  8753. [
  8754. h("div", { class: "body" }, [
  8755. h("div", { class: "left" }, [
  8756. h("div", { class: "icon" }, [
  8757. h(SsIcon, { name: "close", size: "36px" }),
  8758. ]),
  8759. ]),
  8760. h("div", { class: "right" }, [
  8761. h("div", { class: "title" }, "操作失败"),
  8762. h("div", { class: "desc" }, "请点击返回以继续。"),
  8763. ]),
  8764. ]),
  8765. h("div", { class: "footer" }, [
  8766. h("div", { class: "left" }),
  8767. h("div", { class: "right" }, [
  8768. h(
  8769. "div",
  8770. {
  8771. class: "btn",
  8772. onClick: this.onBack,
  8773. },
  8774. [h(SsIcon, { name: "arrow-left-line" }), h("div", "返回")]
  8775. ),
  8776. ]),
  8777. ]),
  8778. ]
  8779. )
  8780. : null;
  8781. },
  8782. };
  8783. /**
  8784. * 审核链条
  8785. * @name ss-verify
  8786. * @param { Array } verify-list 审核节点列表
  8787. * @property { Array } verify-list 审核节点列表
  8788. * @example <ss-verify :verify-list="verifyList"></ss-verify>
  8789. * verify-list [
  8790. * {
  8791. * groupName: "", // 群组名称
  8792. * open: true, // 默认是否展开
  8793. * children:[ //群组里的人员
  8794. * {
  8795. * thumb: "images/example/user-4.png", // 头像
  8796. * name: "李丽思 ", // 姓名
  8797. * role: "人事处处长", // 角色
  8798. * description: "同意。", // 审核意见
  8799. * time: "09:38 08/11", // 审核时间
  8800. * video: false, // false不显示/true显示 视频icon
  8801. * link: false, // false不显示/true显示 链接icon 后续应该是附件
  8802. * }
  8803. * ]
  8804. * }
  8805. * ]
  8806. */
  8807. const SsVerify = {
  8808. name: "SsVerify",
  8809. props: {
  8810. verifyList: {
  8811. type: Array,
  8812. required: true,
  8813. },
  8814. },
  8815. setup(props) {
  8816. const toggleOpen = (item) => {
  8817. item.open = !item.open;
  8818. };
  8819. onMounted(() => {
  8820. setTimeout(() => {
  8821. const lastOpenGroup = document.querySelector(".group-item-last-open");
  8822. console.log("lastOpenGroup", lastOpenGroup);
  8823. if (lastOpenGroup) {
  8824. const nodes = $(lastOpenGroup).find(".verify-node-container");
  8825. if (nodes.length) {
  8826. let totalHeight = 0;
  8827. const gudingHeight = 100;
  8828. if (nodes.length === 1) {
  8829. totalHeight = gudingHeight;
  8830. } else {
  8831. // 累加除最后一个节点外的所有节点高度
  8832. for (let i = 0; i < nodes.length - 1; i++) {
  8833. totalHeight += $(nodes[i]).outerHeight();
  8834. }
  8835. totalHeight += gudingHeight;
  8836. }
  8837. console.log("节点信息:", {
  8838. 节点总数: nodes.length,
  8839. 计算后的高度: totalHeight,
  8840. });
  8841. lastOpenGroup.style.setProperty(
  8842. "--group-line-height",
  8843. `${totalHeight}px`
  8844. );
  8845. }
  8846. }
  8847. }, 0);
  8848. });
  8849. return {
  8850. toggleOpen,
  8851. };
  8852. },
  8853. render() {
  8854. const SsIcon = resolveComponent("ss-icon");
  8855. const SsCommonIcon = resolveComponent("ss-common-icon");
  8856. const SsVerifyNode = resolveComponent("ss-verify-node");
  8857. return h(
  8858. "div",
  8859. { class: "verify-nodes" },
  8860. this.verifyList.map((item, i) =>
  8861. h(
  8862. "div",
  8863. {
  8864. key: i,
  8865. class: {
  8866. "group-item": true,
  8867. "group-item-last-open":
  8868. i === this.verifyList.length - 1 && item.open,
  8869. },
  8870. },
  8871. [
  8872. h(
  8873. "div",
  8874. {
  8875. class: "group-item-title",
  8876. onClick: () => this.toggleOpen(item),
  8877. },
  8878. [
  8879. h("div", { class: "icon" }, [
  8880. item.open
  8881. ? h(SsCommonIcon, { class: "common-icon-folder-open" })
  8882. : h(SsCommonIcon, { class: "common-icon-folder-close" }),
  8883. h(
  8884. "div",
  8885. {
  8886. class: "num",
  8887. style: { top: item.open ? "60%" : "55%" },
  8888. },
  8889. item.children?.length || 0
  8890. ),
  8891. ]),
  8892. h("div", { class: "name" }, item.groupName),
  8893. ]
  8894. ),
  8895. item.open && item.children?.length > 0
  8896. ? h(
  8897. "div",
  8898. { class: "group-item-children" },
  8899. item.children.map((citem, j) =>
  8900. h(SsVerifyNode, {
  8901. key: j,
  8902. item: citem,
  8903. // isGroup: i + 1 !== this.verifyList.length,
  8904. isGroup: true,
  8905. })
  8906. )
  8907. )
  8908. : null,
  8909. ]
  8910. )
  8911. )
  8912. );
  8913. },
  8914. };
  8915. /**
  8916. * 审核页面的审核节点
  8917. * @name ss-verify-node
  8918. * @param {Object} item 审核节点信息
  8919. * @param {Boolean} isGroup 是否为分组节点
  8920. */
  8921. const SsVerifyNode = {
  8922. name: "SsVerifyNode",
  8923. props: {
  8924. item: {
  8925. type: Object,
  8926. required: true,
  8927. },
  8928. isGroup: {
  8929. type: Boolean,
  8930. default: false,
  8931. },
  8932. },
  8933. render() {
  8934. const SsIcon = resolveComponent("ss-icon");
  8935. const SsCommonIcon = resolveComponent("ss-common-icon");
  8936. return Vue.h("div", { class: "verify-node-container" }, [
  8937. Vue.h("div", { class: "info" }, [
  8938. Vue.h("div", { class: "avatar" }, [
  8939. Vue.h("img", {
  8940. src: this.item.thumb,
  8941. style: {
  8942. width: "50px",
  8943. height: "50px",
  8944. borderRadius: "50%",
  8945. },
  8946. }),
  8947. ]),
  8948. Vue.h("div", { class: "desc" }, [
  8949. Vue.h("div", this.item.name),
  8950. Vue.h("div", this.item.role),
  8951. ]),
  8952. Vue.h("div", { class: "link" }, [
  8953. Vue.h("div", [
  8954. this.item.video
  8955. ? Vue.h(SsCommonIcon, { class: "common-icon-video" })
  8956. : null,
  8957. this.item.link
  8958. ? Vue.h(SsCommonIcon, {
  8959. class: "common-icon-paper-clip",
  8960. })
  8961. : null,
  8962. ]),
  8963. ]),
  8964. ]),
  8965. Vue.h(
  8966. "div",
  8967. {
  8968. class: {
  8969. description: true,
  8970. link: this.isGroup,
  8971. },
  8972. attrs: { "data-num": "3" },
  8973. },
  8974. [Vue.h("div", this.item.description)]
  8975. ),
  8976. Vue.h("div", { class: "time" }, this.item.time),
  8977. ]);
  8978. },
  8979. };
  8980. /**
  8981. * 智能识别图片的左侧图片播放 可以放大缩小旋转图片
  8982. * @name ss-orc-img-box
  8983. * @param { Object } image-obj 包含图片的url, 和图片的名称
  8984. *
  8985. */
  8986. const SsOrcImgBox = {
  8987. name: "SsOrcImgBox",
  8988. props: {
  8989. imageObj: {
  8990. type: Object,
  8991. required: true,
  8992. },
  8993. },
  8994. setup(props) {
  8995. const zoom = ref(1);
  8996. const rotation = ref(0);
  8997. const containerWidth = ref(0);
  8998. const containerHeight = ref(0);
  8999. const container = ref(null);
  9000. const imgPosition = ref({ x: 0, y: 0 });
  9001. const isDragging = ref(false);
  9002. const lastMousePosition = ref({ x: 0, y: 0 });
  9003. const imgStyle = computed(() => ({
  9004. width: `${zoom.value * 100}%`,
  9005. height: `${zoom.value * 100}%`,
  9006. transform: `rotate(${rotation.value}deg) translate(${imgPosition.value.x}px, ${imgPosition.value.y}px)`,
  9007. transformOrigin: "center center",
  9008. cursor: isDragging.value ? "grabbing" : "grab",
  9009. }));
  9010. const resetZoom = () => {
  9011. zoom.value = 1;
  9012. rotation.value = rotation.value + 90;
  9013. imgPosition.value = { x: 0, y: 0 };
  9014. };
  9015. const handleRangeChange = (event) => {
  9016. const value = event.target.value / 50; // 0 到 100 映射到 0 到 2 的缩放
  9017. zoom.value = Math.max(value, 0.1); // 设置最小缩放值为 0.1
  9018. };
  9019. const updateImgBoxDimensions = () => {
  9020. if (container.value) {
  9021. containerWidth.value = container.value.clientWidth;
  9022. containerHeight.value = container.value.clientHeight;
  9023. }
  9024. };
  9025. const onMouseDown = (event) => {
  9026. isDragging.value = true;
  9027. lastMousePosition.value = { x: event.clientX, y: event.clientY };
  9028. };
  9029. const onMouseMove = (event) => {
  9030. if (isDragging.value) {
  9031. const dx = event.clientX - lastMousePosition.value.x;
  9032. const dy = event.clientY - lastMousePosition.value.y;
  9033. // 防止旋转后拖动的x,y反转
  9034. // 首先将当前旋转角度从度数转换为弧度,因为 JavaScript 的 Math 库使用弧度
  9035. const angle = rotation.value * (Math.PI / 180);
  9036. // 使用基本的二维旋转矩阵将原始位移 dx 和 dy 转换为旋转后的位移 rotatedDx 和 rotatedDy。
  9037. const rotatedDx = dx * Math.cos(angle) + dy * Math.sin(angle);
  9038. const rotatedDy = dy * Math.cos(angle) - dx * Math.sin(angle);
  9039. imgPosition.value = {
  9040. x: imgPosition.value.x + rotatedDx,
  9041. y: imgPosition.value.y + rotatedDy,
  9042. };
  9043. lastMousePosition.value = { x: event.clientX, y: event.clientY };
  9044. }
  9045. };
  9046. const onMouseUp = () => {
  9047. isDragging.value = false;
  9048. };
  9049. onMounted(() => {
  9050. nextTick(() => {
  9051. updateImgBoxDimensions();
  9052. window.addEventListener("resize", updateImgBoxDimensions);
  9053. window.addEventListener("mousemove", onMouseMove);
  9054. window.addEventListener("mouseup", onMouseUp);
  9055. });
  9056. });
  9057. return {
  9058. zoom,
  9059. rotation,
  9060. container,
  9061. imgStyle,
  9062. resetZoom,
  9063. handleRangeChange,
  9064. containerWidth,
  9065. containerHeight,
  9066. onMouseDown,
  9067. imgPosition,
  9068. };
  9069. },
  9070. render() {
  9071. const SsIcon = resolveComponent("ss-icon");
  9072. return h("div", { class: "ocr-img-box" }, [
  9073. h("div", { class: "img-bar" }, [
  9074. h("div", this.imageObj.name),
  9075. h("div", { class: "action-bar" }, [
  9076. h("div", { class: "ocr-img-range-box" }, [
  9077. h("input", {
  9078. type: "range",
  9079. min: 0,
  9080. max: 100,
  9081. value: this.zoom * 50, // 初始位置为50
  9082. onInput: this.handleRangeChange,
  9083. }),
  9084. h("span", { class: "line" }),
  9085. ]),
  9086. h(SsIcon, {
  9087. name: "reset",
  9088. size: "26px",
  9089. onClick: this.resetZoom,
  9090. }),
  9091. ]),
  9092. ]),
  9093. h("div", { class: "img-viewer", ref: "container" }, [
  9094. h(
  9095. "div",
  9096. {
  9097. class: "img-box",
  9098. style: {
  9099. width: `${this.containerWidth}px`,
  9100. height: `${this.containerHeight}px`,
  9101. overflow: "hidden",
  9102. position: "relative",
  9103. },
  9104. },
  9105. [
  9106. h("img", {
  9107. src: this.imageObj.thumb,
  9108. style: this.imgStyle,
  9109. class: "zoomable-img",
  9110. onMousedown: this.onMouseDown,
  9111. }),
  9112. ]
  9113. ),
  9114. ]),
  9115. ]);
  9116. },
  9117. };
  9118. // 搜索输入框组件
  9119. const SsSearchInput = {
  9120. name: "SsSearchInput",
  9121. props: {
  9122. name: String,
  9123. placeholder: String,
  9124. width: {
  9125. type: String,
  9126. default: "100px",
  9127. },
  9128. modelValue: String,
  9129. },
  9130. emits: ["update:modelValue", "search"],
  9131. setup(props, { emit }) {
  9132. const handleInput = (e) => {
  9133. emit("update:modelValue", e.target.value);
  9134. };
  9135. const handleKeyup = (e) => {
  9136. if (e.key === "Enter") {
  9137. emit("search");
  9138. }
  9139. };
  9140. return { handleInput, handleKeyup };
  9141. },
  9142. render() {
  9143. return h(
  9144. "div",
  9145. {
  9146. class: "input",
  9147. style: this.width ? { width: this.width } : undefined,
  9148. },
  9149. [
  9150. h("input", {
  9151. name: this.name,
  9152. placeholder: this.placeholder,
  9153. value: this.modelValue,
  9154. onInput: this.handleInput,
  9155. onKeyup: this.handleKeyup,
  9156. }),
  9157. ]
  9158. );
  9159. },
  9160. };
  9161. // ss-search-date-picker 日期时间选择器组件
  9162. const SsSearchDatePicker = {
  9163. name: "SsSearchDatePicker",
  9164. props: {
  9165. modelValue: {
  9166. type: [String, Number, Date],
  9167. default: "",
  9168. },
  9169. name: {
  9170. type: String,
  9171. required: true,
  9172. },
  9173. type: {
  9174. type: String,
  9175. default: "date",
  9176. validator: (value) => ["date", "datetime", "time"].includes(value),
  9177. },
  9178. fmt: {
  9179. type: String,
  9180. default: null,
  9181. },
  9182. placeholder: {
  9183. type: String,
  9184. default: "",
  9185. },
  9186. width: {
  9187. type: String,
  9188. default: "100%",
  9189. },
  9190. },
  9191. emits: ["update:modelValue"],
  9192. setup(props, { emit }) {
  9193. const errMsg = ref("");
  9194. const validate = () => {
  9195. if (window.ssVm) {
  9196. const result = window.ssVm.validateField(props.name);
  9197. console.log("validate", window.ssVm.validateField(props.name));
  9198. errMsg.value = result.valid ? "" : result.message;
  9199. }
  9200. };
  9201. // 根据type确定默认格式
  9202. const defaultFormat = computed(() => {
  9203. switch (props.type) {
  9204. case "datetime":
  9205. return "YYYY-MM-DD HH:mm:ss";
  9206. case "date":
  9207. return "YYYY-MM-DD";
  9208. case "time":
  9209. return "HH:mm:ss";
  9210. }
  9211. });
  9212. const convertJavaFormatToElement = (javaFormat) => {
  9213. if (!javaFormat) return null;
  9214. return javaFormat
  9215. .replace("yyyy", "YYYY")
  9216. .replace("MM", "MM")
  9217. .replace("dd", "DD")
  9218. .replace("HH", "HH")
  9219. .replace("mm", "mm")
  9220. .replace("ss", "ss");
  9221. };
  9222. const finalFormat = computed(() => {
  9223. if (props.fmt) {
  9224. return convertJavaFormatToElement(props.fmt);
  9225. }
  9226. return defaultFormat.value;
  9227. });
  9228. // 使用 resolveComponent 获取组件
  9229. const ElDatePicker = resolveComponent("ElDatePicker");
  9230. const ElTimePicker = resolveComponent("ElTimePicker");
  9231. const SsFormIcon = resolveComponent("SsFormIcon");
  9232. const ElIcon = resolveComponent("ElIcon");
  9233. let useTimePicker = true;
  9234. //"yyyy-MM-dd HH:mm:ss"; "日期字符串格式在java的写法",传到本组件fmt属性也是按这个格式
  9235. if (props.fmt) {
  9236. //有fmt属性,则以fmt属性优先判断类型
  9237. if (/[dMy]/.test(props.fmt)) {
  9238. //如果有传入日期格式,且含年月日
  9239. useTimePicker = false;
  9240. } else {
  9241. useTimePicker = true;
  9242. }
  9243. } else if (props.type !== "time") {
  9244. useTimePicker = false;
  9245. }
  9246. const dateType = computed(() => {
  9247. const fmt = props.fmt || "";
  9248. if (fmt.includes("HH:mm:ss")) {
  9249. return "datetime";
  9250. } else if (fmt.includes("HH:mm")) {
  9251. return "datetime";
  9252. } else if (fmt.includes("mm:ss")) {
  9253. return "time";
  9254. }
  9255. return "date";
  9256. });
  9257. const handleValueUpdate = (val) => {
  9258. emit("update:modelValue", val);
  9259. emit("change", val); // 同时触发 change 事件
  9260. setTimeout(() => {
  9261. validate();
  9262. }, 50);
  9263. };
  9264. return () =>
  9265. h(
  9266. "div",
  9267. { class: "ss-search-date-picker", style: { width: props.width } },
  9268. [
  9269. h("input", {
  9270. type: "hidden",
  9271. name: props.name,
  9272. value: props.modelValue,
  9273. }),
  9274. h(useTimePicker ? ElTimePicker : ElDatePicker, {
  9275. modelValue: props.modelValue,
  9276. "onUpdate:modelValue": handleValueUpdate,
  9277. type: dateType.value,
  9278. format: finalFormat.value,
  9279. "value-format": finalFormat.value,
  9280. clearable: true,
  9281. placeholder: props.placeholder,
  9282. class: "custom-date-picker", // 用于自定义样式
  9283. "time-arrow-control": props.type === "datetime", // 修改这里
  9284. size: "large", // 添加这一行,改为 large 尺寸
  9285. style: { width: "100%" },
  9286. "prefix-icon": h(SsFormIcon, { class: "form-icon-time" }),
  9287. }),
  9288. ]
  9289. );
  9290. },
  9291. };
  9292. // 搜索按钮组件(包含下拉按钮)
  9293. const SsSearchButton = {
  9294. name: "SsSearchButton",
  9295. props: {
  9296. text: {
  9297. type: String,
  9298. required: true,
  9299. },
  9300. iconClass: {
  9301. type: String,
  9302. required: false,
  9303. },
  9304. opt: {
  9305. type: Array,
  9306. default: () => [],
  9307. },
  9308. checkId: {
  9309. type: String,
  9310. default: "0",
  9311. },
  9312. width: {
  9313. //add by Ben(20251225)
  9314. type: String,
  9315. required: false,
  9316. },
  9317. id: {
  9318. //add by Ben(20251225)
  9319. type: String,
  9320. required: false,
  9321. },
  9322. },
  9323. emits: ["click"],
  9324. setup(props, { emit }) {
  9325. const currentId = ref(props.checkId || "0");
  9326. const showPopup = ref(false);
  9327. const handleMouseEnter = () => {
  9328. showPopup.value = true;
  9329. };
  9330. const handleMouseLeave = () => {
  9331. showPopup.value = false;
  9332. };
  9333. // 添加点击事件处理,阻止默认行为
  9334. const handleClick = (e) => {
  9335. e.preventDefault();
  9336. if (props.opt?.length > 0) {
  9337. const selectedOption =
  9338. currentId.value === "0"
  9339. ? props.opt[0]
  9340. : props.opt.find((opt) => opt.id === currentId.value);
  9341. if (selectedOption) {
  9342. selectedOption.callback?.();
  9343. }
  9344. } else {
  9345. emit("click", e);
  9346. }
  9347. };
  9348. // 获取显示文本
  9349. const getDisplayText = () => {
  9350. if (!props.opt?.length) return props.text;
  9351. const selectedOption =
  9352. currentId.value === "0"
  9353. ? props.opt[0]
  9354. : props.opt.find((opt) => opt.id === currentId.value);
  9355. return selectedOption ? selectedOption.desc : props.opt[0].desc;
  9356. };
  9357. return () =>
  9358. h(
  9359. "button",
  9360. {
  9361. class:
  9362. props.opt?.length > 0
  9363. ? "ss-drop-button ss-drop-button-more"
  9364. : "ss-drop-button",
  9365. type: "button", // 明确指定按钮类型为 button
  9366. onMouseenter: handleMouseEnter,
  9367. onMouseleave: handleMouseLeave,
  9368. onClick: handleClick, // 添加点击事件处理
  9369. style: { width: props.width }, //add by Ben(20251225)
  9370. id: props.id, //add by Ben(20251225)
  9371. },
  9372. [
  9373. props.iconClass
  9374. ? h("span", {
  9375. class: props.iconClass,
  9376. style: { fontFamily: "iconfont", marginRight: "5px" },
  9377. })
  9378. : null,
  9379. h("span", getDisplayText()),
  9380. props.opt.length > 0 &&
  9381. showPopup.value &&
  9382. h(
  9383. "div",
  9384. {
  9385. class: "popup",
  9386. },
  9387. props.opt.map((item) =>
  9388. h(
  9389. "div",
  9390. {
  9391. onClick: (e) => {
  9392. e.preventDefault(); // 选项点击也阻止默认行为
  9393. e.stopPropagation(); // 阻止事件冒泡
  9394. currentId.value = item.id; // 更新当前选中的ID
  9395. item.callback();
  9396. showPopup.value = false; // 选择后关闭弹窗
  9397. },
  9398. },
  9399. item.desc
  9400. )
  9401. )
  9402. ),
  9403. ]
  9404. );
  9405. },
  9406. };
  9407. // 下拉按钮组件
  9408. const SsDropButton = {
  9409. name: "SsDropButton",
  9410. props: {
  9411. text: {
  9412. type: String,
  9413. required: true,
  9414. },
  9415. iconClass: {
  9416. type: String,
  9417. required: true,
  9418. },
  9419. opt: {
  9420. type: Array,
  9421. default: () => [],
  9422. },
  9423. checkId: {
  9424. type: String,
  9425. default: "0",
  9426. },
  9427. onclick: {
  9428. type: Function,
  9429. default: null,
  9430. },
  9431. },
  9432. setup(props) {
  9433. const currentId = ref(props.checkId || "0");
  9434. const showPopup = ref(false);
  9435. const handleMouseEnter = () => {
  9436. showPopup.value = true;
  9437. };
  9438. const handleMouseLeave = () => {
  9439. showPopup.value = false;
  9440. };
  9441. // 添加点击事件处理,阻止默认行为
  9442. const handleClick = (e) => {
  9443. e.preventDefault();
  9444. if (props.opt?.length > 0) {
  9445. const selectedOption =
  9446. currentId.value === "0"
  9447. ? props.opt[0]
  9448. : props.opt.find((opt) => opt.id === currentId.value);
  9449. if (selectedOption) {
  9450. selectedOption.callback?.();
  9451. }
  9452. } else if (props.onclick) {
  9453. props.onclick();
  9454. }
  9455. };
  9456. // 获取显示文本
  9457. const getDisplayText = () => {
  9458. if (!props.opt?.length) return props.text;
  9459. const selectedOption =
  9460. currentId.value === "0"
  9461. ? props.opt[0]
  9462. : props.opt.find((opt) => opt.id === currentId.value);
  9463. return selectedOption ? selectedOption.desc : props.opt[0].desc;
  9464. };
  9465. return () =>
  9466. h(
  9467. "button",
  9468. {
  9469. class:
  9470. props.opt?.length > 0
  9471. ? "ss-drop-button ss-drop-button-more"
  9472. : "ss-drop-button",
  9473. type: "button", // 明确指定按钮类型为 button
  9474. onMouseenter: handleMouseEnter,
  9475. onMouseleave: handleMouseLeave,
  9476. onClick: handleClick, // 添加点击事件处理
  9477. },
  9478. [
  9479. h("span", {
  9480. class: props.iconClass,
  9481. style: { fontFamily: "iconfont" },
  9482. }),
  9483. h("span", getDisplayText()),
  9484. props.opt.length > 0 &&
  9485. showPopup.value &&
  9486. h(
  9487. "div",
  9488. {
  9489. class: "popup",
  9490. },
  9491. props.opt.map((item) =>
  9492. h(
  9493. "div",
  9494. {
  9495. onClick: (e) => {
  9496. e.preventDefault(); // 选项点击也阻止默认行为
  9497. e.stopPropagation(); // 阻止事件冒泡
  9498. currentId.value = item.id; // 更新当前选中的ID
  9499. item.callback();
  9500. showPopup.value = false; // 选择后关闭弹窗
  9501. },
  9502. },
  9503. item.desc
  9504. )
  9505. )
  9506. ),
  9507. ]
  9508. );
  9509. },
  9510. };
  9511. /**
  9512. * 二级页面标签组件
  9513. * @name ss-sub-tab
  9514. * @description 用于展示二级页面的布局组件,包含左侧垂直标签导航(支持分组)和右侧iframe内容区
  9515. * @property {String} headerImage - 左侧顶部图片地址
  9516. * @property {Array} menuList - 菜单配置列表
  9517. * @property {Object} [activeMenu] - 当前选中的菜单项,不传则自动选择第一个可选菜单
  9518. * @property {Array} footerButtons - 底部按钮配置列表
  9519. */
  9520. /**
  9521. * SsSubTab 左侧菜单+iframe内容组件
  9522. * v3.0 改造:去掉顶部图片,改为图标+悬浮模式,iframe懒加载 by xu 20251216
  9523. */
  9524. const SsSubTab = {
  9525. name: "SsSubTab",
  9526. props: {
  9527. menuList: {
  9528. type: Array,
  9529. required: true,
  9530. },
  9531. activeMenu: {
  9532. type: String,
  9533. default: "",
  9534. },
  9535. footerButtons: {
  9536. type: Array,
  9537. default: () => [],
  9538. },
  9539. leftDisplay: {
  9540. type: Boolean,
  9541. default: true,
  9542. },
  9543. // v3.0 新增:菜单模式 collapse(悬浮展开) / fixed(始终收起) by xu 20251216
  9544. initialMode: {
  9545. type: String,
  9546. default: "collapse",
  9547. },
  9548. },
  9549. emits: ["menu-change", "footer-click"],
  9550. setup(props, { emit }) {
  9551. // v3.0 新增:默认图标映射,使用icon-biz图标 by xu 20251216
  9552. const defaultIcons = [
  9553. "icon-obj-ry", // 人员
  9554. "icon-obj-dw", // 单位
  9555. "icon-obj-gw", // 岗位
  9556. "icon-biz-rc", // 人才
  9557. "icon-biz-xc", // 巡查
  9558. "icon-biz-cl", // 材料
  9559. "icon-biz-men", // 门
  9560. "icon-obj-xy", // 协议
  9561. ];
  9562. //功能: SsSubTab 支持后端下发 iconName + pobj/cobj 两级菜单 by xu 20251222
  9563. const isTrue = (v) => v === true || v === "true" || v === 1 || v === "1"; //功能 by xu 20251222
  9564. const resolveIconClass = (iconNameOrClass, fallbackIndex) => {
  9565. //功能 by xu 20251222
  9566. const fallback = `menu-icon ${
  9567. defaultIcons[fallbackIndex % defaultIcons.length]
  9568. }`;
  9569. if (!iconNameOrClass) {
  9570. return fallback;
  9571. }
  9572. // 已经是完整 class(可能包含 menu-icon / menu-base-icon / 多个 class)
  9573. if (
  9574. typeof iconNameOrClass === "string" &&
  9575. iconNameOrClass.indexOf(" ") > -1
  9576. ) {
  9577. return iconNameOrClass;
  9578. }
  9579. const iconName = iconNameOrClass;
  9580. if (iconName === "menu-icon" || iconName === "menu-base-icon") {
  9581. return fallback;
  9582. }
  9583. // 业务图标库:icon-biz / icon-obj -> menu-icon
  9584. if (
  9585. typeof iconName === "string" &&
  9586. (iconName.indexOf("icon-obj-") === 0 ||
  9587. iconName.indexOf("icon-biz-") === 0)
  9588. ) {
  9589. return `menu-icon ${iconName}`;
  9590. }
  9591. // 默认认为是 icon-base 图标 -> menu-base-icon
  9592. return `menu-base-icon ${iconName}`;
  9593. };
  9594. const getMenuIcon = (item, index) => {
  9595. //功能 by xu 20251222
  9596. if (!item) {
  9597. return resolveIconClass(null, index);
  9598. }
  9599. //功能: 变动图标后端暂不正确,前端先写死为 icon-chg by xu 20251223
  9600. if (item.title === "变动" || item.name === "sys_bd") {
  9601. return resolveIconClass("icon-chg", index);
  9602. }
  9603. // 兼容旧字段 icon(优先使用)
  9604. if (item.icon) return resolveIconClass(item.icon, index);
  9605. // v3.0 使用后端下发 iconName
  9606. if (item.iconName) return resolveIconClass(item.iconName, index);
  9607. return resolveIconClass(null, index);
  9608. };
  9609. //功能: SsSubTab 底部按钮支持 icon+文字(icon-base)by xu 20251224
  9610. const getFooterIcon = (button) => {
  9611. //功能 by xu 20251224
  9612. if (!button) return "menu-base-icon icon-subm";
  9613. const iconNameOrClass =
  9614. button.iconClass || button.iconName || button.icon;
  9615. if (!iconNameOrClass) return "menu-base-icon icon-subm";
  9616. if (
  9617. typeof iconNameOrClass === "string" &&
  9618. iconNameOrClass.indexOf(" ") > -1
  9619. ) {
  9620. return iconNameOrClass;
  9621. }
  9622. return `menu-base-icon ${iconNameOrClass}`;
  9623. };
  9624. //功能: pobj/cobj 扁平结构转换为 children 树结构,兼容原 children 结构 by xu 20251222
  9625. const normalizeMenuList = (rawList) => {
  9626. if (!Array.isArray(rawList) || rawList.length === 0) {
  9627. return [];
  9628. }
  9629. const hasTree = rawList.some(
  9630. (it) => Array.isArray(it?.children) && it.children.length > 0
  9631. );
  9632. if (hasTree) {
  9633. return rawList.map((it) => ({
  9634. ...it,
  9635. __level: 1,
  9636. children: Array.isArray(it.children)
  9637. ? it.children.map((c) => ({ ...c, __level: 2 }))
  9638. : it.children,
  9639. }));
  9640. }
  9641. const hasMarker = rawList.some(
  9642. (it) => it && ("pobj" in it || "cobj" in it)
  9643. );
  9644. if (!hasMarker) {
  9645. return rawList.map((it) => ({ ...it, __level: 1 }));
  9646. }
  9647. const result = [];
  9648. let currentGroup = null;
  9649. for (const item of rawList) {
  9650. //功能: “变动”始终按一级处理(即使后端误传 pobj/cobj)by xu 20251223
  9651. const isChgItem =
  9652. item && (item.title === "变动" || item.name === "sys_bd");
  9653. if (isChgItem) {
  9654. result.push({ ...item, __level: 1 });
  9655. continue;
  9656. }
  9657. const isParent = isTrue(item?.pobj);
  9658. const isChild = isTrue(item?.cobj);
  9659. if (isParent) {
  9660. currentGroup = {
  9661. ...item,
  9662. __level: 1,
  9663. children: [],
  9664. };
  9665. result.push(currentGroup);
  9666. continue;
  9667. }
  9668. if (isChild && currentGroup) {
  9669. currentGroup.children.push({ ...item, __level: 2 });
  9670. continue;
  9671. }
  9672. //功能: 变动等无 pobj/cobj 的选项按一级展示(不挂到 children,也不打断当前分组)by xu 20251223
  9673. result.push({ ...item, __level: 1 });
  9674. }
  9675. return result;
  9676. };
  9677. const menuListComputed = computed(() =>
  9678. normalizeMenuList(props.menuList)
  9679. ); //功能 by xu 20251222
  9680. //功能: 分组展开状态(默认展开),避免 computed 生成对象导致 open 状态丢失 by xu 20251222
  9681. const groupOpenState = reactive({}); // { [key]: boolean }
  9682. const getGroupKey = (item) => item?.name || item?.title || ""; //功能 by xu 20251222
  9683. const isGroupOpen = (item) => {
  9684. //功能 by xu 20251222
  9685. const key = getGroupKey(item);
  9686. if (!key) return true;
  9687. return groupOpenState[key] !== false;
  9688. };
  9689. const toggleGroupOpen = (item) => {
  9690. //功能 by xu 20251222
  9691. const key = getGroupKey(item);
  9692. if (!key) return;
  9693. groupOpenState[key] = !isGroupOpen(item);
  9694. };
  9695. const getLevelClass = (item, fallbackLevel) => {
  9696. //功能 by xu 20251222
  9697. //功能: “变动”始终按一级样式处理 by xu 20251223
  9698. if (item && (item.title === "变动" || item.name === "sys_bd")) {
  9699. return "level-1";
  9700. }
  9701. const level = item?.__level || fallbackLevel || 1;
  9702. return level === 2 ? "level-2" : "level-1";
  9703. };
  9704. // v3.0 新增:菜单模式管理 by xu 20251216
  9705. const menuMode = ref(props.initialMode);
  9706. const isHovering = ref(false);
  9707. const toggleMenuMode = () => {
  9708. menuMode.value = menuMode.value === "collapse" ? "fixed" : "collapse";
  9709. };
  9710. //功能: 提供给旧UI弹窗顶部按钮调用的 API(替代点击 .menu-mode-toggle DOM)by xu 20251224
  9711. const registerSsSubTabApi = () => {
  9712. //功能 by xu 20251224
  9713. try {
  9714. window.SS = window.SS || {};
  9715. window.SS.dom = window.SS.dom || {};
  9716. //功能: 兼容小写 ss 命名空间(部分页面只引用 window.ss)by xu 20251224
  9717. window.ss = window.ss || window.SS;
  9718. window.ss.dom = window.ss.dom || window.SS.dom;
  9719. const api = {
  9720. toggleMenuMode,
  9721. getMenuMode: () => menuMode.value,
  9722. };
  9723. window.SS.dom.ssSubTabApi = api;
  9724. window.ss.dom.ssSubTabApi = api;
  9725. //功能: 多层弹窗(如 objPlay -> objInfo) 场景,将 API 注册到 topWindow 供按钮跨层调用 by xu 20251224
  9726. try {
  9727. const wdDialogId =
  9728. window.wd &&
  9729. wd.display &&
  9730. wd.display.getwdDialogId &&
  9731. wd.display.getwdDialogId();
  9732. if (wdDialogId && window.top) {
  9733. window.top.__ssSubTabApiMap = window.top.__ssSubTabApiMap || {};
  9734. window.top.__ssSubTabApiMap[wdDialogId] = api;
  9735. }
  9736. } catch (e) {}
  9737. try {
  9738. console.log(
  9739. "[SsSubTabApi] registered",
  9740. window.location && window.location.pathname
  9741. );
  9742. } catch (e) {}
  9743. } catch (e) {}
  9744. };
  9745. const unregisterSsSubTabApi = () => {
  9746. //功能 by xu 20251224
  9747. try {
  9748. //功能: 从 topWindow 解绑(避免弹窗关闭后残留)by xu 20251224
  9749. try {
  9750. const wdDialogId =
  9751. window.wd &&
  9752. wd.display &&
  9753. wd.display.getwdDialogId &&
  9754. wd.display.getwdDialogId();
  9755. if (
  9756. wdDialogId &&
  9757. window.top &&
  9758. window.top.__ssSubTabApiMap &&
  9759. window.top.__ssSubTabApiMap[wdDialogId]
  9760. ) {
  9761. delete window.top.__ssSubTabApiMap[wdDialogId];
  9762. }
  9763. } catch (e) {}
  9764. if (window.SS?.dom?.ssSubTabApi?.toggleMenuMode === toggleMenuMode) {
  9765. delete window.SS.dom.ssSubTabApi;
  9766. }
  9767. if (window.ss?.dom?.ssSubTabApi?.toggleMenuMode === toggleMenuMode) {
  9768. delete window.ss.dom.ssSubTabApi;
  9769. }
  9770. } catch (e) {}
  9771. };
  9772. //功能: 立即注册,避免 enable 早于 onMounted 导致“api not ready”by xu 20251224
  9773. registerSsSubTabApi(); //功能 by xu 20251224
  9774. onBeforeUnmount(unregisterSsSubTabApi); //功能 by xu 20251224
  9775. const onMouseEnter = () => {
  9776. if (menuMode.value === "collapse") {
  9777. isHovering.value = true;
  9778. }
  9779. };
  9780. const onMouseLeave = () => {
  9781. isHovering.value = false;
  9782. };
  9783. const isExpanded = computed(() => {
  9784. return menuMode.value === "collapse" && isHovering.value;
  9785. });
  9786. // v3.0 新增:iframe 懒加载,点击才加载 by xu 20251216
  9787. const loadedMenus = ref(new Set());
  9788. const isMenuLoaded = (menuName) => {
  9789. return loadedMenus.value.has(menuName);
  9790. };
  9791. // 根据标题找到对应的菜单项
  9792. const findMenuByTitle = (title) => {
  9793. for (const item of menuListComputed.value) {
  9794. //功能 by xu 20251222
  9795. if (item.children?.length > 0) {
  9796. const child = item.children.find((c) => c.title === title);
  9797. if (child) return child;
  9798. } else if (item.title === title) {
  9799. return item;
  9800. }
  9801. }
  9802. return null;
  9803. };
  9804. // 计算默认选中的菜单项
  9805. const defaultActiveMenu = computed(() => {
  9806. if (props.activeMenu) {
  9807. const menu = findMenuByTitle(props.activeMenu);
  9808. if (menu) return menu;
  9809. }
  9810. const firstItem = menuListComputed.value[0]; //功能 by xu 20251222
  9811. if (!firstItem) return null;
  9812. //功能: 默认选中第一个一级菜单(不默认跳到第一个二级)by xu 20251224
  9813. return firstItem;
  9814. });
  9815. const currentMenu = ref(defaultActiveMenu.value);
  9816. // 监听外部activeMenu变化
  9817. watch(
  9818. () => props.activeMenu,
  9819. (newTitle) => {
  9820. if (newTitle) {
  9821. const menu = findMenuByTitle(newTitle);
  9822. if (menu) {
  9823. currentMenu.value = menu;
  9824. }
  9825. }
  9826. }
  9827. );
  9828. // 初始化:默认选中项加入已加载集合
  9829. watch(
  9830. currentMenu,
  9831. (menu) => {
  9832. if (menu?.name) {
  9833. loadedMenus.value.add(menu.name);
  9834. }
  9835. },
  9836. { immediate: true }
  9837. );
  9838. // 选择菜单项时触发 menu-change 钩子
  9839. const selectItem = (item) => {
  9840. currentMenu.value = item;
  9841. // 标记为已加载
  9842. if (item.name) {
  9843. loadedMenus.value.add(item.name);
  9844. }
  9845. emit("menu-change", item);
  9846. };
  9847. // 处理底部按钮点击
  9848. const handleFooterClick = (button, index) => {
  9849. emit("footer-click", { button, index });
  9850. };
  9851. return {
  9852. menuListComputed, //功能 by xu 20251222
  9853. currentMenu,
  9854. selectItem,
  9855. handleFooterClick,
  9856. getFooterIcon, //功能: SsSubTab 底部按钮支持 icon+文字(icon-base)by xu 20251224
  9857. menuMode,
  9858. isHovering,
  9859. isExpanded,
  9860. toggleMenuMode,
  9861. onMouseEnter,
  9862. onMouseLeave,
  9863. isMenuLoaded,
  9864. getMenuIcon,
  9865. isGroupOpen, //功能 by xu 20251222
  9866. toggleGroupOpen, //功能 by xu 20251222
  9867. getLevelClass, //功能 by xu 20251222
  9868. };
  9869. },
  9870. template: `
  9871. <div class="project-edit-container">
  9872. <div class="left-side"
  9873. v-if="leftDisplay"
  9874. :data-mode="menuMode"
  9875. :class="{ 'is-expanded': isExpanded }"
  9876. @mouseenter="onMouseEnter"
  9877. @mouseleave="onMouseLeave">
  9878. <!-- 菜单内容 -->
  9879. <div class="menu-content">
  9880. <div class="scroll-view">
  9881. <template v-for="(menuItem, i) in menuListComputed" :key="i">
  9882. <!-- 分组菜单 -->
  9883. <div v-if="menuItem.children?.length > 0" class="group">
  9884. <!-- 功能: 一级(pobj)可点击进入,箭头仅控制展开/收起;二级点击不影响一级选中状态 by xu 20251223 -->
  9885. <div class="menu-item"
  9886. :class="[getLevelClass(menuItem, 1), { active: menuItem.name === currentMenu?.name }]"
  9887. @click="selectItem(menuItem)">
  9888. <ss-icon :class="getMenuIcon(menuItem, i)" />
  9889. <span class="menu-label">{{ menuItem.title }}</span>
  9890. <!-- 功能: 一级菜单有子项时显示 dot(参考全局左侧菜单)by xu 20251224 -->
  9891. <div class="has-children-dot"></div>
  9892. <div class="menu-tooltip">{{ menuItem.title }}</div>
  9893. </div>
  9894. <!-- 功能: 二级菜单始终展示,不做收缩展开 by xu 20251223 -->
  9895. <div class="group-detail">
  9896. <div v-for="(item, j) in menuItem.children"
  9897. :key="j"
  9898. class="menu-item"
  9899. :class="[getLevelClass(item, 2), { active: item.name === currentMenu?.name }]"
  9900. @click.stop="selectItem(item)">
  9901. <ss-icon :class="getMenuIcon(item, j)" />
  9902. <span class="menu-label">{{ item.title }}</span>
  9903. </div>
  9904. </div>
  9905. </div>
  9906. <!-- 普通菜单项 -->
  9907. <div v-else
  9908. class="menu-item"
  9909. :class="[getLevelClass(menuItem, 1), { active: menuItem.name === currentMenu?.name }]"
  9910. @click="selectItem(menuItem)">
  9911. <ss-icon :class="getMenuIcon(menuItem, i)" />
  9912. <span class="menu-label">{{ menuItem.title }}</span>
  9913. <div class="menu-tooltip">{{ menuItem.title }}</div>
  9914. </div>
  9915. </template>
  9916. </div>
  9917. </div>
  9918. <!-- 底部按钮 -->
  9919. <div v-if="footerButtons.length > 0"
  9920. class="sub-tab-menu-footer"
  9921. :class="{ 'has-text': !!footerButtons[0].text }"
  9922. @click="footerButtons[0].onclick">
  9923. <ss-icon :class="getFooterIcon(footerButtons[0])" />
  9924. <div class="footer-label" v-if="footerButtons[0].text">{{ footerButtons[0].text }}</div>
  9925. <ss-icon v-if="footerButtons.length > 1" class="footer-arrow" name="arrow-up" size="24px" />
  9926. <div v-if="footerButtons.length > 1" class="sub-tab-menu-popup">
  9927. <div v-for="(button, index) in footerButtons.slice(1)"
  9928. :key="index"
  9929. @click.stop="button.onclick">
  9930. {{ button.text }}
  9931. </div>
  9932. </div>
  9933. </div>
  9934. </div>
  9935. <!-- 右侧内容区域 - 懒加载 iframe -->
  9936. <div class="content-area fit-height-content" style="overflow: hidden;" :style="!leftDisplay ? { width: '100%' } : {}">
  9937. <template v-for="(menuItem, i) in menuList" :key="i">
  9938. <iframe
  9939. v-if="isMenuLoaded(menuItem.name)"
  9940. :src="menuItem.url"
  9941. style="height: 100%;width: 100%;"
  9942. frameborder="0"
  9943. class="sub-tab-iframe"
  9944. :id="i === 0 ? 'sub-tab-iframe' : ''"
  9945. v-show="currentMenu?.name === menuItem.name"
  9946. />
  9947. </template>
  9948. </div>
  9949. </div>
  9950. `,
  9951. };
  9952. // <iframe
  9953. // v-if="currentMenu?.url"
  9954. // :src="currentMenu.url"
  9955. // style="height: 100%;width: 100%;"
  9956. // frameborder="0"
  9957. // id="sub-tab-iframe"
  9958. // />
  9959. // ss-photo-upload 通用图片上传组件
  9960. const SsImgUpload = {
  9961. name: "SsImgUpload",
  9962. props: {
  9963. name: {
  9964. type: String,
  9965. required: true,
  9966. },
  9967. // 图片URL,用于回显
  9968. // url: {
  9969. // type: String,
  9970. // default: "",
  9971. // },
  9972. // 样式类名
  9973. class: {
  9974. type: String,
  9975. required: true,
  9976. },
  9977. // 裁剪配置
  9978. cropperOpt: {
  9979. type: Object,
  9980. default: () => ({
  9981. width: 360,
  9982. height: 360,
  9983. aspectRatio: 1,
  9984. }),
  9985. },
  9986. //上传图片url(未加图片名参数之前的部分)
  9987. ulUrl: {
  9988. type: String,
  9989. required: true,
  9990. },
  9991. //下载图片url(未加图片名参数之前的部分)
  9992. dlUrl: {
  9993. type: String,
  9994. required: true,
  9995. },
  9996. modelValue: [String, Number],
  9997. },
  9998. emits: ["update:modelValue"],
  9999. setup(props, { emit }) {
  10000. const inputId = Vue.computed(
  10001. () => `file_${Vue.getCurrentInstance().uid}`
  10002. );
  10003. //修改图片初始显示路径
  10004. let pathVal = ref(props.modelValue);
  10005. let picUrl = ref("");
  10006. if (props.modelValue) {
  10007. picUrl.value = props.dlUrl + "&path=" + props.modelValue;
  10008. }
  10009. Vue.onMounted(() => {
  10010. window.SS.cropper.init({
  10011. el: $(`#${inputId.value}`),
  10012. photoSize: {
  10013. width: props.cropperOpt.width,
  10014. height: props.cropperOpt.height,
  10015. },
  10016. aspectRatio: props.cropperOpt.aspectRatio,
  10017. uploadUrl: props.ulUrl,
  10018. success: (path) => {
  10019. pathVal.value = path;
  10020. picUrl.value = props.dlUrl + "&path=" + path;
  10021. emit("update:modelValue", path);
  10022. },
  10023. });
  10024. });
  10025. return () =>
  10026. h("div", { class: [props.class] }, [
  10027. h("input", {
  10028. type: "file",
  10029. accept: "image/*",
  10030. id: inputId.value,
  10031. style: { display: "none" },
  10032. }),
  10033. h("input", {
  10034. type: "hidden",
  10035. name: props.name,
  10036. value: pathVal.value,
  10037. }),
  10038. h(
  10039. "div",
  10040. {
  10041. style: {
  10042. width: "100%",
  10043. height: "100%",
  10044. },
  10045. onClick: () => $(`#${inputId.value}`).click(),
  10046. },
  10047. [
  10048. picUrl.value &&
  10049. h("img", {
  10050. src: picUrl.value,
  10051. style:
  10052. "width: 100%; height: 100%;object-fit: inherit;position: relative;z-index: 11;",
  10053. }),
  10054. ]
  10055. ),
  10056. ]);
  10057. },
  10058. };
  10059. // 初始化函数,负责创建和挂载 Vue 应用
  10060. // window.SS = { dom: {} };
  10061. /**
  10062. * 获取当前窗口的父窗口
  10063. * @returns {Window} 父窗口对象
  10064. */
  10065. window.SS.topWin = (function (p, c) {
  10066. while (p != c) {
  10067. c = p;
  10068. p = p.parent;
  10069. }
  10070. return c;
  10071. })(window.parent, window);
  10072. window.SS.createSsDialogInstance = createSsDialogInstance;
  10073. /**
  10074. * 创建弹窗
  10075. * @param {Object} setting
  10076. * @param {Function} callbackEvent
  10077. */
  10078. window.SS.openDialog = function (setting, callbackEvent) {
  10079. if (setting.params) {
  10080. const encodedParams = encodeURIComponent(JSON.stringify(setting.params));
  10081. setting.src +=
  10082. (setting.src.includes("?") ? "&" : "?") + "params=" + encodedParams;
  10083. }
  10084. if (window.parent && window.parent !== window) {
  10085. window.parent.SS.createSsDialogInstance(setting, callbackEvent);
  10086. } else {
  10087. createSsDialogInstance(setting, callbackEvent);
  10088. }
  10089. };
  10090. //关闭弹窗
  10091. window.SS.closeDialog = function () {
  10092. console.log("关闭弹窗");
  10093. if (topWindow.dialogInstances.length > 0) {
  10094. const instance = topWindow.dialogInstances.pop();
  10095. console.log("instance", instance);
  10096. console.log("instance.callbackEvent", instance.callbackEvent);
  10097. console.log(
  10098. "instance.callbackEvent.end",
  10099. typeof instance.callbackEvent === "function"
  10100. );
  10101. if (instance.callbackEvent) {
  10102. // 判断是否有end回调并执行
  10103. if (typeof instance.callbackEvent === "function") {
  10104. instance.callbackEvent();
  10105. }
  10106. if (typeof instance.callbackEvent.end === "function") {
  10107. instance.callbackEvent.end();
  10108. }
  10109. }
  10110. instance.app.unmount(); // 卸载最后一个实例
  10111. if (instance.container && instance.container.parentNode) {
  10112. instance.container.parentNode.removeChild(instance.container); // 移除容器
  10113. }
  10114. }
  10115. };
  10116. /**
  10117. * 裁剪插件
  10118. */
  10119. window.SS.cropper = {
  10120. init: function (setting) {
  10121. if (!window.top.SS) window.top.SS = {};
  10122. // 重要:确保 cropper 对象的完整初始化
  10123. if (!window.top.SS.cropper) {
  10124. window.top.SS.cropper = {
  10125. settings: new Map(),
  10126. _backupSettings: {},
  10127. getSetting: this.getSetting,
  10128. clearSetting: this.clearSetting,
  10129. debug: this.debug,
  10130. };
  10131. } else if (!window.top.SS.cropper.settings) {
  10132. // 如果 cropper 存在但 settings 不存在,重新初始化 settings
  10133. window.top.SS.cropper.settings = new Map();
  10134. window.top.SS.cropper._backupSettings = {};
  10135. }
  10136. const uploaderId = `uploader_${Date.now()}_${Math.random()
  10137. .toString(36)
  10138. .substr(2, 9)}`;
  10139. window.top.SS.cropper.settings.set(uploaderId, setting);
  10140. window.top.SS.cropper._backupSettings[uploaderId] = setting;
  10141. setting.box = setting.box || "1";
  10142. var winSetting = {
  10143. headerTitle: "图片裁剪",
  10144. src: "/js/cropper/cropper.jsp", //原来在"/newUI/page/cropper.jsp" Ben(20251205)
  10145. width: "900",
  10146. height: "500",
  10147. };
  10148. $(setting.el).change(function () {
  10149. if (!window.SS.cropper.verify(setting)) {
  10150. $(setting.el).val(""); // 清空文件选择
  10151. return false;
  10152. }
  10153. var files = this.files;
  10154. if (files && files.length) {
  10155. if (!window.SS.cropper.verifySize($(setting.el)[0], 5)) {
  10156. $(setting.el).val(""); // 清空文件选择
  10157. alert("文件大小不能超过5M,请重新选择");
  10158. return false;
  10159. }
  10160. var URL = window.URL || window.webkitURL;
  10161. var file = files[0];
  10162. setting.file = file;
  10163. if (
  10164. /^image\/\w+$/.test(file.type) &&
  10165. /\.(jpg|jpeg|png|)$/i.test(file.name)
  10166. ) {
  10167. var uploadedImageURL = URL.createObjectURL(file);
  10168. setting.data = uploadedImageURL;
  10169. setting.fileName = file.name;
  10170. // console.log()
  10171. winSetting.params = {
  10172. ...setting,
  10173. uploaderId,
  10174. };
  10175. console.log("ss-componets中change之后的winSetting", winSetting);
  10176. SS.openDialog(winSetting, {
  10177. success: function (win) {
  10178. console.log("裁剪插件成功");
  10179. // win.cropperSetting = setting;
  10180. },
  10181. end: function () {
  10182. console.log("裁剪插件结束");
  10183. $(setting.el).val(""); // 清空文件选择
  10184. },
  10185. });
  10186. } else {
  10187. alert("请选择图片文件,支持jpg、jpeg、png格式");
  10188. }
  10189. }
  10190. });
  10191. return uploaderId;
  10192. },
  10193. verify: function (setting) {
  10194. if (!setting) {
  10195. console.error(" cropper setting is not undefined! ");
  10196. return false;
  10197. }
  10198. if (!setting.el) {
  10199. console.error(" cropper setting.el is not undefined! ");
  10200. return false;
  10201. }
  10202. if (setting.photoSize) {
  10203. if (
  10204. (!setting.photoSize.width && setting.photoSize.height) ||
  10205. (!setting.photoSize.height && setting.photoSize.width)
  10206. ) {
  10207. console.error(
  10208. " cropper setting.photoSize { width, height } is not undefined! "
  10209. );
  10210. return false;
  10211. }
  10212. }
  10213. if (!setting.box) {
  10214. setting.box = "1";
  10215. }
  10216. if (!setting.aspectRatio) {
  10217. setting.aspectRatio = 1 / 1;
  10218. }
  10219. return true;
  10220. },
  10221. verifySize: function (fileEl, maxSize) {
  10222. // 判断是否为IE浏览器: /msie/i.test(navigator.userAgent) 为一个简单正则
  10223. var isIE = /msie/i.test(navigator.userAgent) && !window.opera;
  10224. var fileSize = 0;
  10225. if (isIE && !fileEl.files) {
  10226. // IE浏览器
  10227. var filePath = fileEl.value; // 获得上传文件的绝对路径
  10228. var fileSystem = new ActiveXObject("Scripting.FileSystemObject");
  10229. var file = fileSystem.GetFile(filePath);
  10230. fileSize = file.Size; // 文件大小,单位:b
  10231. } else {
  10232. // 非IE浏览器
  10233. fileSize = fileEl.files[0].size;
  10234. }
  10235. var size = fileSize / 1024 / 1024;
  10236. return !(size > maxSize);
  10237. },
  10238. // 获取特定上传组件的setting
  10239. getSetting: function (uploaderId) {
  10240. if (!window.top.SS?.cropper) {
  10241. console.warn("顶层窗口中未找到 SS.cropper");
  10242. return null;
  10243. }
  10244. // 优先从 Map 中获取
  10245. let setting = window.top.SS.cropper.settings.get(uploaderId);
  10246. // 如果 Map 中没有,尝试从备份中获取
  10247. if (!setting && window.top.SS.cropper._backupSettings[uploaderId]) {
  10248. console.log("从备份中恢复 setting");
  10249. setting = window.top.SS.cropper._backupSettings[uploaderId];
  10250. // 恢复到 Map 中
  10251. window.top.SS.cropper.settings.set(uploaderId, setting);
  10252. }
  10253. return setting;
  10254. },
  10255. // 清理特定上传组件的setting
  10256. clearSetting: function (uploaderId) {
  10257. if (!window.top.SS?.cropper) return;
  10258. window.top.SS.cropper.settings.delete(uploaderId);
  10259. delete window.top.SS.cropper._backupSettings[uploaderId];
  10260. console.log(
  10261. "清理设置后的 Map size:",
  10262. window.top.SS.cropper.settings.size
  10263. );
  10264. },
  10265. };
  10266. /**
  10267. * 获取url中的参数
  10268. * @returns {Object}
  10269. */
  10270. window.SS.getQueryParams = function () {
  10271. const params = {};
  10272. const queryString = window.location.search.substring(1);
  10273. const pairs = queryString.split("&");
  10274. for (let i = 0; i < pairs.length; i++) {
  10275. const pair = pairs[i].split("=");
  10276. params[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
  10277. }
  10278. if (params.params) {
  10279. try {
  10280. params.params = JSON.parse(params.params);
  10281. } catch (e) {
  10282. console.error("Failed to parse params:", e);
  10283. }
  10284. }
  10285. return params;
  10286. };
  10287. /**
  10288. * 创建vue应用
  10289. * @param {Object} config 配置项
  10290. * @values {String} config.el 挂载的元素
  10291. * @values {Boolean} config.isDialogPage 是否是弹窗页面 决定了是否可以使用顶天立地
  10292. * @values {Object} config.vueOptions vue配置项
  10293. * @returns {Object} vue实例
  10294. */
  10295. window.ss = window.ss || window.SS;
  10296. window.ss.dom = window.ss.dom || window.SS.dom;
  10297. window.SS.dom._vueAppSeq =
  10298. window.SS.dom._vueAppSeq || window.ss.dom._vueAppSeq || 0;
  10299. window.SS.dom.vueApps = window.SS.dom.vueApps || window.ss.dom.vueApps || [];
  10300. window.ss.dom._vueAppSeq = window.SS.dom._vueAppSeq;
  10301. window.ss.dom.vueApps = window.SS.dom.vueApps;
  10302. window.SS.dom._registerVueApp = function ({ vm, el, appKey, scope, href }) {
  10303. if (!vm) return null;
  10304. const registry = window.SS.dom.vueApps || (window.SS.dom.vueApps = []);
  10305. const id = `vue-app-${++window.SS.dom._vueAppSeq}`;
  10306. const record = {
  10307. id,
  10308. el: el || "",
  10309. appKey: appKey || "",
  10310. scope: scope || "current",
  10311. href: href || window.location.href,
  10312. createdAt: Date.now(),
  10313. vm,
  10314. win: window,
  10315. };
  10316. registry.push(record);
  10317. window.SS.dom.currentApp = vm;
  10318. window.SS.dom.currentAppRecord = record;
  10319. if (window.ss && window.ss.dom) {
  10320. window.ss.dom.vueApps = registry;
  10321. window.ss.dom._vueAppSeq = window.SS.dom._vueAppSeq;
  10322. window.ss.dom.currentApp = vm;
  10323. window.ss.dom.currentAppRecord = record;
  10324. }
  10325. return record;
  10326. };
  10327. window.SS.dom._getVmData = function (vm) {
  10328. if (!vm) return null;
  10329. return vm.$data || vm;
  10330. };
  10331. window.SS.dom._hasVmKey = function (vm, key) {
  10332. if (!vm || !key) return false;
  10333. const data = window.SS.dom._getVmData(vm);
  10334. return key in vm || (!!data && key in data);
  10335. };
  10336. window.SS.dom._hasVmMethod = function (vm, methodName) {
  10337. return !!(vm && methodName && typeof vm[methodName] === "function");
  10338. };
  10339. window.SS.dom._normalizeBridgeOptions = function (
  10340. options = {},
  10341. defaults = {}
  10342. ) {
  10343. return {
  10344. scope: "chain",
  10345. ...defaults,
  10346. ...options,
  10347. };
  10348. };
  10349. window.SS.dom._matchVueRecord = function (record, options = {}) {
  10350. if (!record || !record.vm) return false;
  10351. if (options.appKey && record.appKey !== options.appKey) return false;
  10352. if (options.el && record.el !== options.el) return false;
  10353. if (options.id && record.id !== options.id) return false;
  10354. if (
  10355. options.dataKey &&
  10356. !window.SS.dom._hasVmKey(record.vm, options.dataKey)
  10357. ) {
  10358. return false;
  10359. }
  10360. if (
  10361. options.methodName &&
  10362. !window.SS.dom._hasVmMethod(record.vm, options.methodName)
  10363. ) {
  10364. return false;
  10365. }
  10366. if (typeof options.predicate === "function") {
  10367. try {
  10368. if (!options.predicate(record.vm, record)) {
  10369. return false;
  10370. }
  10371. } catch (e) {
  10372. return false;
  10373. }
  10374. }
  10375. return true;
  10376. };
  10377. window.SS.dom._getWindowVueRecord = function (targetWindow, options = {}) {
  10378. if (!targetWindow || !targetWindow.SS || !targetWindow.SS.dom) return null;
  10379. const registry = targetWindow.SS.dom.vueApps || [];
  10380. for (let i = registry.length - 1; i >= 0; i--) {
  10381. if (targetWindow.SS.dom._matchVueRecord(registry[i], options)) {
  10382. return registry[i];
  10383. }
  10384. }
  10385. const currentRecord = targetWindow.SS.dom.currentAppRecord;
  10386. if (targetWindow.SS.dom._matchVueRecord(currentRecord, options)) {
  10387. return currentRecord;
  10388. }
  10389. const currentVm = targetWindow.SS.dom.currentApp;
  10390. const fallbackRecord = currentVm
  10391. ? {
  10392. vm: currentVm,
  10393. win: targetWindow,
  10394. el: "",
  10395. appKey: "",
  10396. id: "",
  10397. }
  10398. : null;
  10399. if (targetWindow.SS.dom._matchVueRecord(fallbackRecord, options)) {
  10400. return fallbackRecord;
  10401. }
  10402. return null;
  10403. };
  10404. window.SS.dom.getVueAppRecord = function (options = {}) {
  10405. const scope = options.scope || "current";
  10406. if (scope === "current") {
  10407. return window.SS.dom._getWindowVueRecord(window, options);
  10408. }
  10409. if (scope === "top") {
  10410. try {
  10411. return window.SS.dom._getWindowVueRecord(window.top, options);
  10412. } catch (e) {
  10413. return null;
  10414. }
  10415. }
  10416. if (scope === "parent") {
  10417. try {
  10418. if (window.parent && window.parent !== window) {
  10419. return window.SS.dom._getWindowVueRecord(window.parent, options);
  10420. }
  10421. } catch (e) {
  10422. return null;
  10423. }
  10424. return null;
  10425. }
  10426. if (scope === "chain") {
  10427. let currentWindow = window;
  10428. while (currentWindow) {
  10429. try {
  10430. const record = window.SS.dom._getWindowVueRecord(
  10431. currentWindow,
  10432. options
  10433. );
  10434. if (record) {
  10435. return record;
  10436. }
  10437. } catch (e) {
  10438. return null;
  10439. }
  10440. if (currentWindow === currentWindow.parent) {
  10441. break;
  10442. }
  10443. currentWindow = currentWindow.parent;
  10444. }
  10445. return null;
  10446. }
  10447. return window.SS.dom._getWindowVueRecord(window, options);
  10448. };
  10449. window.SS.dom.getVueApp = function (options = {}) {
  10450. const record = window.SS.dom.getVueAppRecord(options);
  10451. return record ? record.vm : null;
  10452. };
  10453. window.SS.dom.getVmValue = function (key, options = {}) {
  10454. const vm = window.SS.dom.getVueApp(
  10455. window.SS.dom._normalizeBridgeOptions(options, { dataKey: key })
  10456. );
  10457. if (!vm) return undefined;
  10458. return vm[key];
  10459. };
  10460. window.SS.dom.setVmValue = function (key, value, options = {}) {
  10461. const vm = window.SS.dom.getVueApp(
  10462. window.SS.dom._normalizeBridgeOptions(options, { dataKey: key })
  10463. );
  10464. if (!vm) return false;
  10465. vm[key] = value;
  10466. return true;
  10467. };
  10468. window.SS.dom.callVmMethod = function (methodName, args = [], options = {}) {
  10469. const vm = window.SS.dom.getVueApp(
  10470. window.SS.dom._normalizeBridgeOptions(options, { methodName })
  10471. );
  10472. if (!vm || typeof vm[methodName] !== "function") {
  10473. return undefined;
  10474. }
  10475. return vm[methodName].apply(vm, args);
  10476. };
  10477. window.SS.dom.get = function (key, options = {}) {
  10478. return window.SS.dom.getVmValue(key, options);
  10479. };
  10480. window.SS.dom.set = function (key, value, options = {}) {
  10481. return window.SS.dom.setVmValue(key, value, options);
  10482. };
  10483. window.SS.dom.call = function (methodName, args = [], options = {}) {
  10484. return window.SS.dom.callVmMethod(methodName, args, options);
  10485. };
  10486. [
  10487. "_registerVueApp",
  10488. "_getVmData",
  10489. "_hasVmKey",
  10490. "_hasVmMethod",
  10491. "_normalizeBridgeOptions",
  10492. "_matchVueRecord",
  10493. "_getWindowVueRecord",
  10494. "getVueAppRecord",
  10495. "getVueApp",
  10496. "getVmValue",
  10497. "setVmValue",
  10498. "callVmMethod",
  10499. "get",
  10500. "set",
  10501. "call",
  10502. ].forEach((name) => {
  10503. window.ss.dom[name] = window.SS.dom[name];
  10504. });
  10505. window.SS.dom.initializeFormApp = function (config) {
  10506. const {
  10507. el,
  10508. isDialogPage = false,
  10509. appKey = "",
  10510. scope = "current",
  10511. ...vueOptions
  10512. } = config;
  10513. const app = createApp({
  10514. ...vueOptions,
  10515. });
  10516. if (isDialogPage) {
  10517. function checkScroll() {
  10518. const elements = document.querySelectorAll(".fit-height-content");
  10519. let hasScrollBar = false;
  10520. elements.forEach((el) => {
  10521. if (el.scrollHeight > el.clientHeight) {
  10522. hasScrollBar = true;
  10523. }
  10524. });
  10525. window.parent.postMessage({ hasScrollBar }, "*");
  10526. }
  10527. function addScrollListeners() {
  10528. const elements = document.querySelectorAll("div");
  10529. elements.forEach((el) => {
  10530. el.addEventListener("scroll", checkScroll);
  10531. });
  10532. }
  10533. const observer = new MutationObserver(() => {
  10534. addScrollListeners();
  10535. checkScroll();
  10536. });
  10537. observer.observe(document.body, {
  10538. childList: true,
  10539. subtree: true,
  10540. });
  10541. window.addEventListener("resize", checkScroll);
  10542. }
  10543. app.component("SsLoginIcon", SsLoginIcon);
  10544. app.component("SsMark", SsMark);
  10545. app.component("SsFullStyleHeader", SsFullStyleHeader);
  10546. app.component("SsDialog", SsDialog);
  10547. app.component("SsInp", SsInput);
  10548. app.component("SsObjp", SsObjp);
  10549. app.component("SsHidden", SsHidden);
  10550. app.component("SsCcp", SsCcp);
  10551. app.component("SsDatePicker", SsDatePicker);
  10552. app.component("SsIcon", SsIcon);
  10553. app.component("SsCommonIcon", SsCommonIcon);
  10554. app.component("SsBreadcrumb", SsBreadcrumb);
  10555. app.component("SsEditor", SsEditor);
  10556. app.component("SsDialogIcon", SsDialogIcon);
  10557. app.component("SsBottomButton", SsBottomButton);
  10558. app.component("SsNavIcon", SsNavIcon);
  10559. app.component("SsHeaderIcon", SsHeaderIcon);
  10560. app.component("SsGolbalMenuIcon", SsGolbalMenuIcon);
  10561. app.component("SsCartListIcon", SsCartListIcon);
  10562. app.component("SsQuickIcon", SsQuickIcon);
  10563. app.component("SsFormIcon", SsFormIcon);
  10564. app.component("SsBottomDivIcon", SsBottomDivIcon);
  10565. app.component("SsEditorIcon", SsEditorIcon);
  10566. app.component("SsValidate", SsValidate);
  10567. app.component("SsOnoff", Ssonoff);
  10568. app.component("SsonoffArray", SsonoffArray);
  10569. app.component("SsTextarea", SsTextarea);
  10570. app.component("SsLoginInput", SsLoginInput);
  10571. app.component("SsLoginButton", SsLoginButton);
  10572. app.component("SsSearch", SsSearch);
  10573. app.component("SsCartItem", SsCartItem);
  10574. app.component("SsCartItem2", SsCartItem2);
  10575. app.component("SsListCard", SsListCard);
  10576. app.component("ss-cobj-card-list", SsCObjCardList);
  10577. app.component("SsFolderCard", SsFolderCard);
  10578. app.component("ss-sidebar", SsSidebar);
  10579. app.component("ss-sidebar-buttons", SsSidebarButtons);
  10580. app.component("ss-sidebar-chart", SsSidebarChart);
  10581. app.component("ss-sidebar-chart-hover", SsSidebarChartHover);
  10582. app.component("ss-sidebar-list", SsSidebarList);
  10583. app.component("ss-sidebar-report-table", SsSidebarReportTable);
  10584. app.component("SsFolderCartView", SsFolderCartView);
  10585. app.component("SsPage", SsPage);
  10586. app.component("SsRightInfo", SSRightInfo);
  10587. app.component("SsSuccessPopup", SsSuccessPopup);
  10588. app.component("SsErrorDialog", SsErrorDialog);
  10589. app.component("SsVerify", SsVerify);
  10590. app.component("SsVerifyNode", SsVerifyNode);
  10591. app.component("SsOrcImgBox", SsOrcImgBox);
  10592. app.component("ss-search-input", SsSearchInput);
  10593. app.component("ss-search-date-picker", SsSearchDatePicker);
  10594. app.component("ss-search-button", SsSearchButton);
  10595. app.component("ss-drop-button", SsDropButton);
  10596. app.component("ss-sub-tab", SsSubTab);
  10597. app.component("ss-img", SsImgUpload);
  10598. app.use(ElementPlus, {
  10599. locale: ElementPlusLocaleZhCn,
  10600. });
  10601. for (const componentName in IndexComponents) {
  10602. app.component(componentName, IndexComponents[componentName]);
  10603. }
  10604. for (const componentName in EchartComponents) {
  10605. app.component(componentName, EchartComponents[componentName]);
  10606. }
  10607. let vm;
  10608. try {
  10609. vm = app.mount(el);
  10610. vm.data = vueOptions.data();
  10611. window.SS.dom._registerVueApp({
  10612. vm,
  10613. el,
  10614. appKey,
  10615. scope,
  10616. href: window.location.href,
  10617. });
  10618. console.log("vm:", vm);
  10619. console.log("vueOptions:", vueOptions);
  10620. } catch (error) {
  10621. console.log("Mount failed:" + error);
  10622. }
  10623. return vm;
  10624. };
  10625. })();