ss-components.js 369 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357103581035910360103611036210363103641036510366103671036810369103701037110372103731037410375103761037710378103791038010381103821038310384103851038610387103881038910390103911039210393103941039510396103971039810399104001040110402104031040410405104061040710408104091041010411104121041310414104151041610417104181041910420104211042210423104241042510426104271042810429104301043110432104331043410435104361043710438104391044010441104421044310444104451044610447104481044910450104511045210453104541045510456104571045810459104601046110462104631046410465104661046710468104691047010471104721047310474104751047610477104781047910480104811048210483104841048510486104871048810489104901049110492104931049410495104961049710498104991050010501105021050310504105051050610507105081050910510105111051210513105141051510516105171051810519105201052110522105231052410525105261052710528105291053010531105321053310534105351053610537105381053910540105411054210543105441054510546105471054810549105501055110552105531055410555105561055710558105591056010561105621056310564105651056610567105681056910570105711057210573105741057510576105771057810579105801058110582105831058410585105861058710588105891059010591105921059310594105951059610597105981059910600106011060210603
  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. const showRequired = computed(() => {
  202. // 检查是否有验证规则(通过 window.ssVm 判断)
  203. const hasValidationRule = window.ssVm?.validations?.has(props.name);
  204. if (!hasValidationRule) return false;
  205. if (errMsg.value) return true;
  206. if (!inputValue.value) return true;
  207. return false;
  208. });
  209. // 计算floatdiv应该向上还是向下展开
  210. const calculateFloatingDivPosition = () => {
  211. nextTick(() => {
  212. const textarea = inputRef.value;
  213. if (!textarea) return;
  214. const rect = textarea.getBoundingClientRect();
  215. const viewportHeight = window.innerHeight;
  216. // 预估floatdiv的高度(最多5行 * 20px + 上下padding + border)
  217. const estimatedFloatDivHeight = 20 * 5 + 10 + 2; // 5行 + padding + border = 112px
  218. // 检查下方空间
  219. const spaceBelow = viewportHeight - rect.bottom;
  220. // 如果下方空间不足,且上方空间足够,则向上展开
  221. if (
  222. spaceBelow < estimatedFloatDivHeight &&
  223. rect.top > estimatedFloatDivHeight
  224. ) {
  225. floatingDivPosition.value = "top";
  226. } else {
  227. floatingDivPosition.value = "bottom";
  228. }
  229. });
  230. };
  231. // 计算floatdiv的top偏移量
  232. const getFloatingDivTop = computed(() => {
  233. if (props.height) {
  234. // 有height时,padding是5px
  235. return "5px";
  236. } else {
  237. // 没有height时是单行居中,需要计算居中位置
  238. // 假设input-container高度是32px(或者从CSS读取),单行20px
  239. // 居中偏移 = (容器高度 - 行高) / 2 = (32 - 20) / 2 = 6px
  240. return "6px";
  241. }
  242. });
  243. const validate = () => {
  244. if (window.ssVm) {
  245. const result = window.ssVm.validateField(props.name);
  246. console.log(result);
  247. errMsg.value = result.valid ? "" : result.message;
  248. }
  249. };
  250. // 使用 watch 监听 props.errTip 和 props.modelValue 的变化
  251. watch(
  252. () => props.errTip,
  253. (newVal) => {
  254. errMsg.value = newVal;
  255. },
  256. { immediate: true }
  257. );
  258. watch(
  259. () => props.modelValue,
  260. (newVal) => {
  261. inputValue.value = newVal;
  262. }
  263. );
  264. // 挂载时的逻辑
  265. onMounted(() => {
  266. errMsg.value = props.errTip;
  267. inputValue.value = props.modelValue || props.defaultValue || "";
  268. });
  269. // 计算并调整textarea的高度
  270. const adjustHeight = () => {
  271. nextTick(() => {
  272. const textarea = textareaRef.value;
  273. if (!textarea) return;
  274. // floatDiv的textarea始终自动计算高度,不受props.height影响
  275. // 重置高度以获得正确的scrollHeight
  276. textarea.style.height = "auto";
  277. // 计算新高度 - 统一限制为5行
  278. const lineHeight = parseInt(
  279. getComputedStyle(textarea).lineHeight,
  280. 10
  281. );
  282. const maxHeight = lineHeight * 5; // 统一为5行
  283. const newHeight = Math.min(textarea.scrollHeight, maxHeight);
  284. textarea.style.height = `${newHeight}px`;
  285. });
  286. };
  287. // 检查是否应该显示浮动窗口(需要同时满足:有焦点 + 内容超出)
  288. // 修复新增页面点击就出现floatdiv的问题 by xu 20251212
  289. const checkShouldShowFloatingDiv = () => {
  290. const textarea = inputRef.value;
  291. if (!textarea) return false;
  292. // 首先检查是否有内容,没有内容时不显示floatdiv by xu 20251212
  293. if (!inputValue.value || inputValue.value.toString().trim() === "") {
  294. console.log("[floatdiv] 内容为空,不显示floatdiv");
  295. return false;
  296. }
  297. // 判断内容是否超出 by xu 20251212
  298. // 同时检查横向和纵向溢出,任一方向溢出都应显示floatdiv
  299. // 纵向溢出需要加容差值,避免padding/border导致的误判 by xu 20251212
  300. const verticalTolerance = 5; // 容差值5px
  301. const isHorizontalOverflow =
  302. textarea.scrollWidth > textarea.clientWidth;
  303. const isVerticalOverflow =
  304. textarea.scrollHeight > textarea.clientHeight + verticalTolerance;
  305. const isOverflow = isHorizontalOverflow || isVerticalOverflow;
  306. console.log(
  307. "[floatdiv] 溢出检测 - scrollWidth:",
  308. textarea.scrollWidth,
  309. "clientWidth:",
  310. textarea.clientWidth,
  311. "horizontalOverflow:",
  312. isHorizontalOverflow
  313. );
  314. console.log(
  315. "[floatdiv] 溢出检测 - scrollHeight:",
  316. textarea.scrollHeight,
  317. "clientHeight:",
  318. textarea.clientHeight,
  319. "tolerance:",
  320. verticalTolerance,
  321. "verticalOverflow:",
  322. isVerticalOverflow
  323. );
  324. const shouldShow = isFocused.value && isOverflow;
  325. console.log(
  326. "[floatdiv] 最终判断 - isFocused:",
  327. isFocused.value,
  328. "isOverflow:",
  329. isOverflow,
  330. "shouldShow:",
  331. shouldShow
  332. );
  333. // 需要同时满足:有焦点 + 内容超出
  334. return shouldShow;
  335. };
  336. // 定义事件处理函数
  337. const onInput = (event) => {
  338. const newValue = event.target.value;
  339. inputValue.value = newValue;
  340. emit("update:modelValue", newValue);
  341. validate(); // 输入时验证
  342. nextTick(() => {
  343. // 检查是否需要显示浮动div
  344. contentFloatingDiv.value = checkShouldShowFloatingDiv();
  345. // 如果需要显示floatdiv,计算其位置
  346. if (contentFloatingDiv.value) {
  347. calculateFloatingDivPosition();
  348. }
  349. });
  350. adjustHeight();
  351. };
  352. const onFocus = (event) => {
  353. // 设置焦点状态为true
  354. isFocused.value = true;
  355. adjustHeight();
  356. // 检查是否应该显示浮动窗口
  357. nextTick(() => {
  358. contentFloatingDiv.value = checkShouldShowFloatingDiv();
  359. if (contentFloatingDiv.value) {
  360. calculateFloatingDivPosition();
  361. }
  362. });
  363. };
  364. // 失去焦点时进行验证
  365. const onBlur = (event) => {
  366. emit("blur", event.target);
  367. validate(); // 失焦时验证
  368. nextTick(() => {
  369. // 如果焦点不在 textarea 上,则隐藏浮动 div
  370. if (!document.activeElement.classList.contains("input-control")) {
  371. isFocused.value = false;
  372. contentFloatingDiv.value = false;
  373. }
  374. });
  375. };
  376. const onChange = (event) => {
  377. inputValue.value = event.target.value || "";
  378. emit("change", inputValue.value);
  379. };
  380. const onMouseover = (event) => {
  381. nextTick(() => {
  382. // setTimeout(contentFloatingDiv.value = true, 500)
  383. });
  384. };
  385. const onMouseleave = (event) => {
  386. // contentFloatingDiv.value = false
  387. };
  388. // 功能说明:传了 height 视为多行允许回车;未传 height 拦截回车(单行表现) by xu 20260204
  389. const onKeydown = (event) => {
  390. const allowMultiline =
  391. typeof props.height === "string" && props.height.trim() !== "";
  392. if (!allowMultiline && event.key === "Enter") {
  393. event.preventDefault();
  394. }
  395. };
  396. // 附件按钮点击处理(从 SsEditor 搬运)
  397. const onAttachmentClick = (e) => {
  398. e.preventDefault();
  399. if (!props.param || !props.param.button) {
  400. console.warn("未配置 param 参数");
  401. return;
  402. }
  403. console.log("附件点击了");
  404. console.log("param", props.param);
  405. console.log("cmsAddUrl", props.param.button.cmsAddUrl);
  406. // 如果 fjid 为空,先调用 cmsAddUrl 创建
  407. if (fjid.value == null || fjid.value == "") {
  408. $.ajax({
  409. type: "post",
  410. url: props.param.button.cmsAddUrl,
  411. async: false,
  412. data: {
  413. name: "fjid",
  414. ssNrObjName: "sh",
  415. ssNrObjId: "",
  416. },
  417. success: function (_fjid) {
  418. console.log("cmsAddUrl success", _fjid);
  419. fjid.value = _fjid;
  420. },
  421. });
  422. }
  423. // 构建参数字符串
  424. var str =
  425. "&nrid=T-" +
  426. fjid.value +
  427. "&objectId=" +
  428. fjid.value +
  429. "&objectName=" +
  430. fjName +
  431. "&callback=" +
  432. (window["fjidCallbackName"] || "");
  433. console.log("str", str);
  434. // 打开附件编辑对话框
  435. SS.openDialog({
  436. src: props.param.button.cmsUpdUrl + str,
  437. headerTitle: "编辑",
  438. width: 900,
  439. high: 664,
  440. zIndex: 51,
  441. });
  442. };
  443. return {
  444. errMsg,
  445. inputValue,
  446. showRequired,
  447. onInput,
  448. onBlur,
  449. onChange,
  450. onMouseover,
  451. onMouseleave,
  452. onKeydown, // 新增:键盘事件处理 by xu 20251212
  453. contentFloatingDiv,
  454. floatingDivPosition,
  455. getFloatingDivTop,
  456. inputRef,
  457. textareaRef,
  458. onFocus,
  459. onAttachmentClick,
  460. fjid, // 附件 ID,用于隐藏字段
  461. };
  462. },
  463. render() {
  464. const { resolveComponent, h } = Vue;
  465. const SsIcon = resolveComponent("ss-icon");
  466. const SsEditorIcon = resolveComponent("SsEditorIcon");
  467. // 构建主textarea的样式
  468. const mainTextareaStyle = {};
  469. if (this.height) {
  470. mainTextareaStyle.height = "auto";
  471. // mainTextareaStyle.paddingTop = '5px'; // 有高度时加上padding-top
  472. // mainTextareaStyle.paddingBottom = '5px'; // 有高度时加上padding-bottom
  473. } else {
  474. // 没有指定height时,固定为单行高度
  475. mainTextareaStyle.height = "20px"; // 行高20px
  476. mainTextareaStyle.lineHeight = "20px"; // 确保单行垂直居中
  477. mainTextareaStyle.display = "flex";
  478. mainTextareaStyle.marginBottom = "5px";
  479. }
  480. // 如果有附件按钮,为按钮留出空间
  481. if (this.fj || (this.param && this.param.button)) {
  482. //加上&&this.param.button条件 Ben(20251221)
  483. mainTextareaStyle.paddingRight = "75px";
  484. }
  485. const mainTextareaRows = this.height
  486. ? Math.floor(parseFloat("80px") / 20)
  487. : 1;
  488. return h("div", { class: "input" }, [
  489. h("div", { class: "input-container" }, [
  490. h("div", { class: "input", style: "padding:5px 0" }, [
  491. h("textarea", {
  492. ref: "inputRef",
  493. class: "input-control",
  494. name: this.name,
  495. value: this.inputValue,
  496. onInput: this.onInput,
  497. onFocus: this.onFocus,
  498. onBlur: this.onBlur,
  499. onChange: this.onChange,
  500. onKeydown: this.onKeydown, // 新增:禁止回车换行 by xu 20251212
  501. placeholder: this.placeholder,
  502. onMouseover: this.onMouseover, // 监听鼠标悬停
  503. onMouseleave: this.onMouseleave, // 监听鼠标离开
  504. rows: mainTextareaRows,
  505. ...this.$attrs,
  506. style: mainTextareaStyle,
  507. autocomplete: "off",
  508. }),
  509. // 附件按钮(优先使用 param,兼容旧的 fj)
  510. this.fj || (this.param && this.param.button) //加上&&this.param.button条件 Ben(20251221)
  511. ? h(
  512. "button",
  513. {
  514. type: "button",
  515. class: "fj-button",
  516. onClick: this.param
  517. ? this.onAttachmentClick
  518. : (e) => {
  519. e.preventDefault();
  520. console.log("附件配置:", this.fj);
  521. },
  522. },
  523. [
  524. h(SsEditorIcon, {
  525. class: "editor-icon-link",
  526. }),
  527. h("span", { class: "fj-button-text" }, "附件"),
  528. ]
  529. )
  530. : null,
  531. // this.showRequired ? h("div", { class: "required" }) : null,
  532. ]),
  533. this.contentFloatingDiv || ""
  534. ? h(
  535. "div",
  536. {
  537. class: "floating-div",
  538. style:
  539. this.floatingDivPosition === "bottom"
  540. ? {
  541. // 向下展开: 覆盖原输入框,top对齐首行
  542. top: this.getFloatingDivTop,
  543. bottom: "auto",
  544. }
  545. : {
  546. // 向上展开: 同样覆盖原输入框,但从底部开始计算
  547. top: "auto",
  548. bottom: this.height ? "5px" : "6px", // 对齐到原textarea的底部padding位置
  549. },
  550. },
  551. [
  552. h("textarea", {
  553. ref: "textareaRef",
  554. class: "input-control",
  555. value: this.inputValue,
  556. onInput: this.onInput,
  557. onBlur: this.onBlur,
  558. onFocus: this.onFocus,
  559. onKeydown: this.onKeydown, // 新增:禁止回车换行 by xu 20251212
  560. onMouseover: this.onMouseover, // 监听鼠标悬停
  561. onMouseleave: this.onMouseleave, // 监听鼠标离开
  562. autocomplete: "off",
  563. onVnodeMounted: (vnode) => {
  564. vnode.el.focus();
  565. },
  566. }),
  567. ]
  568. )
  569. : null,
  570. // this.errMsg ? h(SsValidate, { errMsg: this.errMsg }) : null,
  571. ]),
  572. // 附件相关的隐藏字段(仅在有 param 时才渲染)
  573. this.param && [
  574. // fjid 隐藏字段(只有当 fjid 有值时才渲染)
  575. this.fjid &&
  576. this.fjid.value &&
  577. h("input", {
  578. type: "hidden",
  579. name: "fjid",
  580. value: this.fjid.value,
  581. }),
  582. // 其他隐藏字段根据 name 生成
  583. /* 去掉。文本框不需要(这是富文本才有的) Ben 20251205
  584. h("input", {
  585. type: "hidden",
  586. name: this.name.replace(/wj$/, "") + "Edit",
  587. value: this.inputValue
  588. }),
  589. */
  590. // h("input", {
  591. // type: "hidden",
  592. // name: this.name.replace(/wj$/, "") + "wj",
  593. // value: this.inputValue
  594. // }),
  595. /* 去掉。文本框不需要(这是富文本才有的) Ben 20251205
  596. h("input", {
  597. type: "hidden",
  598. name: "ueditorpath",
  599. value: this.name
  600. }),
  601. */
  602. ],
  603. ]);
  604. },
  605. };
  606. // ss-normal-input 登录输入
  607. const SsLoginInput = {
  608. name: "SsLoginInput",
  609. inheritAttrs: false,
  610. props: {
  611. errTip: {
  612. type: String,
  613. },
  614. type: {
  615. type: String,
  616. default: "text",
  617. },
  618. required: {
  619. type: Boolean,
  620. default: false,
  621. },
  622. placeholder: {
  623. type: String,
  624. default: "请输入",
  625. },
  626. name: {
  627. type: String,
  628. default: "",
  629. },
  630. defaultValue: [String, Number],
  631. modelValue: [String, Number],
  632. },
  633. emits: ["update:modelValue", "input", "blur", "change"], // 允许更新 v-model 绑定的值
  634. setup(props, { emit }) {
  635. const errMsg = ref("");
  636. const inputRef = ref(null);
  637. const textareaRef = ref(null);
  638. const inputValue = ref(props.modelValue || props.defaultValue || "");
  639. // 使用 watch 监听 props.errTip 和 props.modelValue 的变化
  640. watch(
  641. () => props.errTip,
  642. (newVal) => {
  643. errMsg.value = newVal;
  644. },
  645. { immediate: true }
  646. );
  647. watch(
  648. () => props.modelValue,
  649. (newVal) => {
  650. inputValue.value = newVal;
  651. }
  652. );
  653. // 挂载时的逻辑
  654. onMounted(() => {
  655. errMsg.value = props.errTip;
  656. inputValue.value = props.modelValue || props.defaultValue || "";
  657. });
  658. // 定义事件处理函数
  659. const onInput = (event) => {
  660. const newValue = event.target.value;
  661. inputValue.value = newValue;
  662. emit("update:modelValue", newValue);
  663. };
  664. return { inputValue, onInput, inputRef, textareaRef };
  665. },
  666. render() {
  667. return h("div", { class: "input" }, [
  668. h("div", { class: "input-container" }, [
  669. h("div", { class: "input" }, [
  670. h("input", {
  671. ref: "inputRef",
  672. class: "input-control",
  673. name: this.name,
  674. value: this.inputValue,
  675. onInput: this.onInput,
  676. type: this.type,
  677. placeholder: this.placeholder,
  678. required: this.required,
  679. ...this.$attrs,
  680. autocomplete: "off",
  681. }),
  682. this.required ? h("div", { class: "required" }) : null,
  683. ]),
  684. ]),
  685. ]);
  686. },
  687. };
  688. // ss-login-button
  689. const SsLoginButton = {
  690. name: "SsLoginButton",
  691. inheritAttrs: false,
  692. props: {
  693. class: {
  694. type: String,
  695. default: "",
  696. },
  697. text: {
  698. type: String,
  699. default: "",
  700. },
  701. type: {
  702. type: String,
  703. default: "button",
  704. },
  705. },
  706. emits: ["click"],
  707. setup(props, { emit }) {
  708. // 定义事件处理函数
  709. const onClick = (event) => {
  710. // 发射一个 'click' 事件,你可以传递所需的参数
  711. emit("click", event);
  712. };
  713. return { props, onClick };
  714. },
  715. render() {
  716. const SsIcon = resolveComponent("ss-icon");
  717. const SsLoginIcon = resolveComponent("ss-login-icon");
  718. return h(
  719. "button",
  720. { class: "login-button", type: this.type, onClick: this.onClick },
  721. [
  722. h("span", [h(SsLoginIcon, { class: this.class })]),
  723. h("span", {}, this.text),
  724. ]
  725. );
  726. },
  727. };
  728. // ss-objp 下拉选择
  729. const SsObjp = {
  730. name: "SsObjp",
  731. inheritAttrs: false,
  732. props: {
  733. onchange: {
  734. //在此属性传入onChange的window全局回调函数,函数唯一参数是当前选中值 Ben(20251217)
  735. type: String,
  736. required: false,
  737. },
  738. filter: {
  739. type: String,
  740. required: false,
  741. },
  742. filterfield: {
  743. type: String,
  744. required: false,
  745. },
  746. // filterField: {
  747. // //此属性为页面表单元素的name用逗号分隔,作用为在向后台查询下拉菜单选项时,会带上这些name的表单元素的value值 Ben(20260313)
  748. // type: String,
  749. // required: false,
  750. // },
  751. cb: {
  752. type: String,
  753. required: true,
  754. },
  755. url: {
  756. type: String,
  757. required: true,
  758. },
  759. name: {
  760. type: String,
  761. required: true,
  762. },
  763. width: {
  764. type: String,
  765. default: "100%",
  766. },
  767. placeholder: {
  768. type: String,
  769. default: "请选择",
  770. },
  771. inp: {
  772. type: Boolean,
  773. default: false,
  774. },
  775. opt: {
  776. type: Array,
  777. default: () => [],
  778. },
  779. errTip: String,
  780. defaultValue: [String, Number],
  781. modelValue: [String, Number],
  782. direction: {
  783. type: String,
  784. default: "bottom",
  785. },
  786. },
  787. emits: ["update:modelValue", "input", "blur", "change"],
  788. setup(props, { emit }) {
  789. const canInput = props.inp;
  790. const errMsg = Vue.ref(props.errTip);
  791. const selectItem = Vue.ref({});
  792. let inputText = Vue.ref(""); // 用于存储输入框的文本
  793. const popupWinVisible = Vue.ref(false);
  794. const filteredOptions = Vue.ref(props.opt);
  795. const popupDirection = Vue.ref("bottom");
  796. const popupMaxHeight = Vue.ref("none"); // popup最大高度,用于空间不足时限制高度并出滚动条 by xu 20251212
  797. const popupContentAreaMaxHeight = Vue.computed(() => {
  798. if (!popupMaxHeight.value || popupMaxHeight.value === "none")
  799. return null;
  800. const maxHeightNum = Number.parseFloat(popupMaxHeight.value);
  801. if (!Number.isFinite(maxHeightNum)) return null;
  802. // 功能说明:滚动条统一落在 .content-area(CSS 默认如此),因此需要扣掉 popup-win padding-top(10) 与 popup-content padding(15*2) by xu 20260126
  803. const contentAreaMaxHeight = Math.max(60, maxHeightNum - 40);
  804. return `${contentAreaMaxHeight}px`;
  805. });
  806. // 修复表格内下拉弹层被 overflow 截断:popup 使用 Teleport 到 body + fixed 定位 by xu 20260126
  807. const selectContainerRef = Vue.ref(null);
  808. const popupRef = Vue.ref(null);
  809. const teleportRootStyle = Vue.ref({
  810. position: "fixed",
  811. left: "0",
  812. top: "0",
  813. width: "0",
  814. height: "0",
  815. zIndex: 9999,
  816. pointerEvents: "none",
  817. });
  818. const popupLayerStyle = Vue.ref({
  819. position: "fixed",
  820. left: "0",
  821. top: "0",
  822. bottom: "auto", // 功能说明:覆盖 .popup-win.top 的 bottom 定位,避免 fixed 场景被撑高/错位 by xu 20260126
  823. minWidth: "0",
  824. zIndex: 9999,
  825. pointerEvents: "auto",
  826. });
  827. // const showRequired = Vue.computed(() => {
  828. // const hasValidationRule = window.ssVm?.validations?.has(props.name);
  829. // if (!hasValidationRule) return false;
  830. // if (errMsg.value) return true;
  831. // if (!selectItem.value?.value) return true;
  832. // return false;
  833. // });
  834. const validate = () => {
  835. if (window.ssVm) {
  836. const result = window.ssVm.validateField(props.name);
  837. // console.log("validate", window.ssVm.validateField(props.name));
  838. errMsg.value = result.valid ? "" : result.message;
  839. }
  840. };
  841. //在objPicker界面,选中value对应的项
  842. const updateSelectItem = () => {
  843. // console.log(props.opt);
  844. const item = props.opt.find((it) => it.value === props.modelValue);
  845. if (item) {
  846. selectItem.value = item;
  847. inputText.value = item.label;
  848. } else {
  849. selectItem.value = { label: "", value: "" };
  850. inputText.value = "";
  851. }
  852. // validate();
  853. };
  854. Vue.watch(
  855. () => props.errTip,
  856. (newVal) => {
  857. errMsg.value = newVal;
  858. }
  859. );
  860. Vue.watch(() => props.modelValue, updateSelectItem, { immediate: true });
  861. Vue.watch(
  862. () => props.opt,
  863. (newVal) => {
  864. updateSelectItem();
  865. filteredOptions.value = [...newVal];
  866. // console.log("filteredOptions", filteredOptions.value);
  867. }
  868. );
  869. //初始化objPicker在页面刚打开时的默认值
  870. async function initDefaultValue() {
  871. try {
  872. if (props.url && props.cb && props.modelValue) {
  873. let objectPickerParam;
  874. let url = props.url;
  875. //如果有定义过滤器
  876. if (props.filter) {
  877. //包含HTML实体的JSON字符串转为JSON对象,如原字符串是{&quot;dwid&quot;:&quot;88&quot;},注意key也必需用单引号包着
  878. // const decodedString = props.filter.replace(/&quot;/g, '"'); // 转换为: {"dwid":"88"}
  879. // objectPickerParam = JSON.parse(decodedString); // 转为json对象
  880. const filterObj = props.filter; // 转为json对象
  881. for (let k in filterObj) {
  882. let v = filterObj[k];
  883. url += "&" + k + "=" + v;
  884. }
  885. objectPickerParam = props.filter; // 转为json对象
  886. objectPickerParam["input"] = props.inp;
  887. objectPickerParam["codebook"] = props.cb;
  888. // alert(url);
  889. } else {
  890. objectPickerParam = { input: props.inp, codebook: props.cb };
  891. }
  892. const objectPickerParamStr = JSON.stringify(objectPickerParam);
  893. const params = new URLSearchParams();
  894. params.append("objectpickerparam", objectPickerParamStr);
  895. params.append("objectpickertype", "2");
  896. params.append("objectpickervalue", props.modelValue); //需回显的值
  897. // alert("1params:"+JSON.stringify(params));
  898. axios
  899. .post(props.url, params, {
  900. headers: {
  901. "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
  902. },
  903. })
  904. .then((response) => {
  905. // alert(JSON.stringify(response.data));
  906. if ("timeout" == response.data.statusText) {
  907. alert("网络超时!");
  908. return;
  909. }
  910. if (response.data.result) {
  911. const keys = Object.keys(response.data.result);
  912. if (keys.length === 1) {
  913. let code = keys[0];
  914. let desc = response.data.result[keys[0]];
  915. if (props.opt)
  916. props.opt.length = 0; //通过修改数组的length属性,直接清空数组元素,内存会被自动释放。这是性能最优的方式
  917. else {
  918. props.opt = [];
  919. }
  920. props.opt.push({ label: desc, value: code });
  921. updateSelectItem();
  922. // alert('props.opt:'+JSON.stringify(props.opt));
  923. }
  924. }
  925. });
  926. }
  927. } catch (error) {
  928. // callback(null, error.message); // 失败回调,传递错误
  929. }
  930. }
  931. // Vue.onMounted(updateSelectItem);
  932. const doSelectItem = (item) => {
  933. emit("update:modelValue", item.value);
  934. selectItem.value = item;
  935. inputText.value = item.label;
  936. hidePopup();
  937. nextTick(() => {
  938. console.log(item.value + "@@@props.modelValue:" + props.modelValue);
  939. validate();
  940. if (window.ssVm) {
  941. // 遍历所有验证规则,找到依赖当前字段的规则
  942. for (const [field, rules] of window.ssVm.validations.entries()) {
  943. for (const rule of rules) {
  944. if (rule.opt?.relField === props.name) {
  945. // console.log("Found dependent field:", field); // 调试日志
  946. window.ssVm.validateField(field);
  947. }
  948. }
  949. }
  950. }
  951. });
  952. callGlobalOnchg(item.value, item.label); // 值变化时尝试调用全局onchange回调函数 Ben(20251217)
  953. };
  954. // 用于调用全局onchange回调函数 Ben(20251217)
  955. const callGlobalOnchg = (value, desc) => {
  956. // 检查 onchange 属性是否提供了有效的函数名
  957. if (props.onchange && typeof props.onchange === "string") {
  958. // 检查 window 对象上是否存在该函数
  959. if (
  960. typeof window !== "undefined" &&
  961. window[props.onchange] &&
  962. typeof window[props.onchange] === "function"
  963. ) {
  964. try {
  965. window[props.onchange](value, desc); // 调用全局函数,并传入当前选中值
  966. } catch (error) {
  967. console.error(`调用全局函数 ${props.onchange} 时出错:`, error);
  968. }
  969. } else {
  970. console.warn(`全局函数 ${props.onchange} 未定义或不是一个函数。`);
  971. }
  972. }
  973. };
  974. //可录入的objPicker,更新下拉菜单选项
  975. async function updateOptionBYInputText(inpTxt) {
  976. try {
  977. let objectPickerParam;
  978. let url = props.url;
  979. if (props.url && props.cb) {
  980. //如果有定义过滤器
  981. if (props.filter||props.filterfield) {
  982. let filterObj = props.filter;
  983. if(!props.filter)
  984. filterObj = {};
  985. if (props.filter){
  986. const filterObj = props.filter; // 转为json对象
  987. for (let k in filterObj) {
  988. let v = filterObj[k];
  989. url += "&" + k + "=" + v;
  990. }
  991. }
  992. if (props.filterfield) {//加上filterfield的值过滤
  993. let filterfieldArr = props.filterfield.split(",");
  994. for(var i=0;i<filterfieldArr.length;i++){
  995. let fieldName = filterfieldArr[i];
  996. let fields = document.getElementsByName(fieldName);
  997. if(!fields||fields.length<1){
  998. alert('下拉菜单配置的过滤条件'+fieldName+'不存在!');
  999. continue;
  1000. }
  1001. let v = null;
  1002. for (let j = 0; j < fields.length; j++) {
  1003. if(fields[j].value){
  1004. v=fields[j].value;
  1005. break;
  1006. }
  1007. }
  1008. // let field = document.getElementsByName(fieldName)[0];
  1009. // let v = field.value;
  1010. if (v) {
  1011. url += "&" + fieldName + "=" + v;
  1012. filterObj[fieldName]=v;
  1013. }
  1014. }
  1015. console.log('filterfield url:'+url);
  1016. }
  1017. //包含HTML实体的JSON字符串转为JSON对象,如原字符串是{&quot;dwid&quot;:&quot;88&quot;},注意key也必需用单引号包着
  1018. // const decodedString = props.filter.replace(/&quot;/g, '"'); // 转换为: {"dwid":"88"}
  1019. // objectPickerParam = JSON.parse(decodedString); // 转为json对象
  1020. objectPickerParam = filterObj;
  1021. objectPickerParam["input"] = props.inp;
  1022. objectPickerParam["codebook"] = props.cb;
  1023. // alert(url);
  1024. } else {
  1025. objectPickerParam = { input: props.inp, codebook: props.cb };
  1026. }
  1027. const objectPickerParamStr = JSON.stringify(objectPickerParam);
  1028. const params = new URLSearchParams();
  1029. params.append("objectpickerparam", objectPickerParamStr);
  1030. params.append("objectpickertype", "1");
  1031. if (props.inp && props.inp === true) {
  1032. //把"true"改为true Ben(20251209)
  1033. params.append("objectpickersearchAll", 0); //只查录入的值
  1034. params.append("objectpickerinput", inpTxt); //录入的值
  1035. } else {
  1036. params.append("objectpickersearchAll", 1);
  1037. }
  1038. axios
  1039. .post(url, params, {
  1040. headers: {
  1041. "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
  1042. },
  1043. })
  1044. .then((response) => {
  1045. if ("timeout" == response.data.statusText) {
  1046. alert("网络超时!");
  1047. return;
  1048. }
  1049. // 先清空选项 by xu 20251212
  1050. if (props.opt) {
  1051. props.opt.length = 0;
  1052. } else {
  1053. props.opt = [];
  1054. }
  1055. if (response.data.result) {
  1056. const keys = Object.keys(response.data.result);
  1057. // console.log("params:"+params+"@@response.data:"+JSON.stringify(response.data));
  1058. if (keys.length > 0) {
  1059. for (let k in response.data.result) {
  1060. props.opt.push({
  1061. label: response.data.result[k],
  1062. value: k,
  1063. });
  1064. }
  1065. // console.log('###inpTxt:'+inpTxt+';');
  1066. if (
  1067. props.inp &&
  1068. props.inp === true && //把"true"改为true Ben(20251209)
  1069. inpTxt.length > 0
  1070. ) {
  1071. //对于可录入的,用已录入的值作过滤
  1072. filteredOptions.value = props.opt.filter((option) =>
  1073. option.label
  1074. .toLowerCase()
  1075. .includes(inputText.value.toLowerCase())
  1076. );
  1077. // 可录入的objPicker,当搜索结果只有一项时,自动选中这一项 by xu 20251212
  1078. if (filteredOptions.value.length === 1) {
  1079. const autoSelectItem = filteredOptions.value[0];
  1080. console.log(
  1081. "[objp] 搜索结果只有一项,自动选中:",
  1082. autoSelectItem
  1083. );
  1084. doSelectItem(autoSelectItem);
  1085. return; // 自动选中后直接返回,不需要显示popup
  1086. }
  1087. filteredOptions.value.unshift({ label: "", value: "" });
  1088. // console.log('###做了过滤:'+inputText.value.toLowerCase()+';');
  1089. } else {
  1090. filteredOptions.value = props.opt;
  1091. filteredOptions.value.unshift({ label: "", value: "" });
  1092. }
  1093. console.log("props.opt11:" + JSON.stringify(props.opt));
  1094. } else {
  1095. // 没有数据时,清空过滤选项 by xu 20251212
  1096. filteredOptions.value = [];
  1097. console.log("[objp] 接口返回空数据");
  1098. }
  1099. } else {
  1100. // result不存在时,清空过滤选项 by xu 20251212
  1101. filteredOptions.value = [];
  1102. console.log("[objp] 接口返回无result");
  1103. }
  1104. // 无论是否有数据,都显示popup by xu 20251212
  1105. openPopup(); // Teleport 场景下统一打开并重定位 by xu 20260126
  1106. });
  1107. }
  1108. } catch (error) {
  1109. // callback(null, error.message); // 失败回调,传递错误
  1110. }
  1111. }
  1112. // 计算弹出方向和最大高度的方法 by xu 20251212
  1113. // 当空间不足时限制popup高度并显示滚动条
  1114. const calculatePopupDirection = () => {
  1115. const triggerEl =
  1116. selectContainerRef.value?.querySelector(".input") ||
  1117. selectContainerRef.value;
  1118. if (!triggerEl) return;
  1119. const selectRect = triggerEl.getBoundingClientRect();
  1120. const viewportHeight = window.innerHeight;
  1121. // 3. 计算上下可用空间
  1122. const spaceBelow = viewportHeight - selectRect.bottom - 10; // 减10px留边距
  1123. const spaceAbove = selectRect.top - 10; // 减10px留边距
  1124. // 4. popup预估高度(假设每项36px,最多显示8项 + padding)
  1125. const estimatedPopupHeight = 300;
  1126. const minPopupHeight = 100; // 最小高度
  1127. console.log(
  1128. "[popup] 空间计算 - spaceAbove:",
  1129. spaceAbove,
  1130. "spaceBelow:",
  1131. spaceBelow,
  1132. "estimatedHeight:",
  1133. estimatedPopupHeight
  1134. );
  1135. // 5. 判断方向和最大高度 by xu 20251212
  1136. if (spaceBelow >= estimatedPopupHeight) {
  1137. // 下方空间足够,向下展开,不限制高度
  1138. popupDirection.value = "bottom";
  1139. popupMaxHeight.value = "none";
  1140. console.log("[popup] 向下展开,空间充足");
  1141. } else if (spaceAbove >= estimatedPopupHeight) {
  1142. // 上方空间足够,向上展开,不限制高度
  1143. popupDirection.value = "top";
  1144. popupMaxHeight.value = "none";
  1145. console.log("[popup] 向上展开,空间充足");
  1146. } else {
  1147. // 上下空间都不足,选择空间大的方向,并限制高度出滚动条
  1148. if (spaceBelow >= spaceAbove) {
  1149. popupDirection.value = "bottom";
  1150. popupMaxHeight.value = Math.max(spaceBelow, minPopupHeight) + "px";
  1151. console.log(
  1152. "[popup] 向下展开,空间不足,限制高度:",
  1153. popupMaxHeight.value
  1154. );
  1155. } else {
  1156. popupDirection.value = "top";
  1157. popupMaxHeight.value = Math.max(spaceAbove, minPopupHeight) + "px";
  1158. console.log(
  1159. "[popup] 向上展开,空间不足,限制高度:",
  1160. popupMaxHeight.value
  1161. );
  1162. }
  1163. }
  1164. };
  1165. // Teleport popup 的定位(fixed) by xu 20260126
  1166. const updatePopupPosition = () => {
  1167. const triggerEl =
  1168. selectContainerRef.value?.querySelector(".input") ||
  1169. selectContainerRef.value;
  1170. if (!triggerEl) return;
  1171. const triggerRect = triggerEl.getBoundingClientRect();
  1172. const margin = 10;
  1173. const viewportWidth = window.innerWidth;
  1174. const viewportHeight = window.innerHeight;
  1175. const popupGap = 0; // 功能说明:定位不再做 -10 重叠,让 padding-top 自然形成间距 by xu 20260126
  1176. // 先给一个初始位置,确保下一帧可以测量弹层尺寸 by xu 20260126
  1177. popupLayerStyle.value = {
  1178. position: "fixed",
  1179. left: `${Math.max(margin, triggerRect.left)}px`,
  1180. top: `${Math.max(margin, triggerRect.bottom + popupGap)}px`, // 功能说明:与输入框底部对齐 by xu 20260126
  1181. bottom: "auto", // 功能说明:fixed 场景显式取消 bottom,避免与 top 同时生效 by xu 20260126
  1182. minWidth: `${Math.max(0, triggerRect.width)}px`,
  1183. zIndex: 9999,
  1184. pointerEvents: "auto",
  1185. };
  1186. Vue.nextTick(() => {
  1187. const popupEl = popupRef.value;
  1188. if (!popupEl) return;
  1189. const popupRect = popupEl.getBoundingClientRect();
  1190. const maxLeft = viewportWidth - popupRect.width - margin;
  1191. const left = Math.min(
  1192. Math.max(margin, triggerRect.left),
  1193. Math.max(margin, maxLeft)
  1194. );
  1195. let top;
  1196. if (popupDirection.value === "top") {
  1197. top = triggerRect.top - popupRect.height - popupGap; // 功能说明:向上展开时与输入框顶部对齐 by xu 20260126
  1198. top = Math.max(margin, top);
  1199. } else {
  1200. top = triggerRect.bottom + popupGap; // 功能说明:向下展开时与输入框底部对齐 by xu 20260126
  1201. if (top + popupRect.height > viewportHeight - margin) {
  1202. top = Math.max(
  1203. margin,
  1204. viewportHeight - popupRect.height - margin
  1205. );
  1206. }
  1207. }
  1208. popupLayerStyle.value = {
  1209. ...popupLayerStyle.value,
  1210. left: `${left}px`,
  1211. top: `${top}px`,
  1212. bottom: "auto", // 功能说明:无论 top/bottom 展开都用 top 定位,禁用 bottom by xu 20260126
  1213. };
  1214. });
  1215. };
  1216. const openPopup = () => {
  1217. if (!popupWinVisible.value) popupWinVisible.value = true;
  1218. Vue.nextTick(() => {
  1219. calculatePopupDirection();
  1220. updatePopupPosition();
  1221. });
  1222. };
  1223. // Teleport 场景下:滚动/缩放重定位 by xu 20260126
  1224. const handleViewportChange = () => {
  1225. if (!popupWinVisible.value) return;
  1226. calculatePopupDirection();
  1227. updatePopupPosition();
  1228. };
  1229. // Teleport 场景下:点击外部关闭 by xu 20260126
  1230. const onDocPointerDown = (event) => {
  1231. if (!popupWinVisible.value) return;
  1232. const target = event.target;
  1233. if (selectContainerRef.value?.contains(target)) return;
  1234. if (popupRef.value?.contains(target)) return;
  1235. hidePopup();
  1236. };
  1237. //点击下拉菜单的文本区域时,会触发的方法
  1238. function togglePopup() {
  1239. // 可录入的 objPicker,更新下拉菜单选项
  1240. updateOptionBYInputText(inputText.value);
  1241. // popupWinVisible.value = !popupWinVisible.value;
  1242. Vue.nextTick(() => {
  1243. calculatePopupDirection();
  1244. updatePopupPosition(); // Teleport 场景下同步定位 by xu 20260126
  1245. });
  1246. }
  1247. const hidePopup = () => {
  1248. popupWinVisible.value = false;
  1249. };
  1250. //点击下拉菜单的三角形时,会触发的方法
  1251. // 添加toggle逻辑,点击时切换显示/隐藏 by xu 20251212
  1252. const suffixClick = () => {
  1253. // 如果popup已显示,则关闭 by xu 20251212
  1254. if (popupWinVisible.value) {
  1255. hidePopup();
  1256. console.log("[objp] 点三角关闭popup");
  1257. return;
  1258. }
  1259. //可录入的objPicker,更新下拉菜单选项
  1260. updateOptionBYInputText("");
  1261. Vue.nextTick(() => {
  1262. calculatePopupDirection();
  1263. updatePopupPosition(); // Teleport 场景下同步定位 by xu 20260126
  1264. });
  1265. console.log("[objp] 点三角打开popup");
  1266. };
  1267. //可录入的objPicker,录入项变化时,会触发
  1268. async function handleInputChange(event) {
  1269. inputText.value = event.target.value;
  1270. if (!inputText.value) {
  1271. inputText.value = "";
  1272. }
  1273. //可录入的objPicker,更新下拉菜单选项
  1274. updateOptionBYInputText(inputText.value);
  1275. // filteredOptions.value = props.opt.filter((option) =>
  1276. // option.label.toLowerCase().includes(inputText.value.toLowerCase())
  1277. // );
  1278. // if (!popupWinVisible.value) {
  1279. // popupWinVisible.value = true; // 确保下拉框在输入时打开
  1280. // }
  1281. }
  1282. Vue.onMounted(() => {
  1283. initDefaultValue();
  1284. // Teleport 场景下:滚动/缩放需要重定位(scroll 用 capture 捕获容器滚动) by xu 20260126
  1285. window.addEventListener("resize", handleViewportChange);
  1286. window.addEventListener("scroll", handleViewportChange, true);
  1287. // 点击外部关闭(原本依赖 mouseleave,Teleport 后会误关) by xu 20260126
  1288. document.addEventListener("pointerdown", onDocPointerDown, true);
  1289. });
  1290. Vue.onUnmounted(() => {
  1291. window.removeEventListener("resize", handleViewportChange);
  1292. window.removeEventListener("scroll", handleViewportChange, true);
  1293. document.removeEventListener("pointerdown", onDocPointerDown, true);
  1294. });
  1295. return {
  1296. errMsg,
  1297. selectItem,
  1298. inputText,
  1299. canInput,
  1300. filteredOptions,
  1301. popupWinVisible,
  1302. popupDirection,
  1303. popupMaxHeight, // 添加popup最大高度 by xu 20251212
  1304. popupContentAreaMaxHeight,
  1305. selectContainerRef,
  1306. popupRef,
  1307. teleportRootStyle,
  1308. popupLayerStyle,
  1309. suffixClick,
  1310. togglePopup,
  1311. hidePopup,
  1312. doSelectItem,
  1313. handleInputChange,
  1314. };
  1315. },
  1316. template: `
  1317. <div class="input" style="position: relative" :style="{width: width}">
  1318. <div class="select-container" ref="selectContainerRef">
  1319. <div class="input" @click="togglePopup">
  1320. <input
  1321. type="hidden"
  1322. :name="name"
  1323. :value="selectItem.value"
  1324. .value="selectItem.value"
  1325. />
  1326. <input
  1327. v-model="inputText"
  1328. @input="handleInputChange"
  1329. v-if="canInput"
  1330. :placeholder="placeholder"
  1331. />
  1332. <input
  1333. v-else
  1334. :placeholder="placeholder"
  1335. :value="selectItem.label"
  1336. disabled
  1337. style="pointer-events: none;"
  1338. />
  1339. <div class="suffix" @click.stop="suffixClick">
  1340. <ss-form-icon :class="popupWinVisible ? 'form-icon-transform-select select' : 'form-icon-select'" />
  1341. </div>
  1342. </div>
  1343. <!-- 修复表格内弹层被截断:popup Teleport 到 body by xu 20260126 -->
  1344. <teleport to="body">
  1345. <div v-show="popupWinVisible" class="select-container ss-objp-teleport-root" :style="teleportRootStyle">
  1346. <!-- popup弹出层,添加maxHeight和overflowY支持空间不足时滚动 by xu 20251212 -->
  1347. <div ref="popupRef" class="popup-win" :class="popupDirection" :style="[popupLayerStyle, { maxHeight: popupMaxHeight !== 'none' ? popupMaxHeight : 'none', overflowY: 'visible' }]">
  1348. <div v-if="opt && opt.length && filteredOptions.length > 0" class="popup-content">
  1349. <div class="content-area" :style="popupContentAreaMaxHeight ? { maxHeight: popupContentAreaMaxHeight, overflowY: 'auto' } : null">
  1350. <div v-for="(item, index) in filteredOptions" :key="index" @click="doSelectItem(item)" :class="{ active: item.value === selectItem.value }">
  1351. <span class="check-icon">
  1352. <ss-form-icon class="form-icon-select-checked" />
  1353. </span>
  1354. <span>{{ item.label }}</span>
  1355. </div>
  1356. </div>
  1357. </div>
  1358. <div v-else class="popup-content"><div class="content-area"><div class="content-area"> <span>无选项</span></div></div></div>
  1359. </div>
  1360. </div>
  1361. </teleport>
  1362. </div>
  1363. </div>
  1364. `,
  1365. };
  1366. // ss-hidden 隐藏字段组件
  1367. const SsHidden = {
  1368. name: "SsHidden",
  1369. props: {
  1370. modelValue: String,
  1371. name: {
  1372. type: String,
  1373. required: true,
  1374. },
  1375. rule: {
  1376. type: String,
  1377. required: true,
  1378. },
  1379. param: {
  1380. type: String,
  1381. required: true,
  1382. },
  1383. url: {
  1384. type: String,
  1385. required: true,
  1386. },
  1387. },
  1388. emits: ["update:modelValue"],
  1389. setup(props, { emit }) {
  1390. const errMsg = Vue.ref("");
  1391. const validate = () => {
  1392. if (window.ssVm) {
  1393. const result = window.ssVm.validateField(props.name);
  1394. console.log("validate", window.ssVm.validateField(props.name));
  1395. errMsg.value = result.valid ? "" : result.message;
  1396. }
  1397. };
  1398. Vue.onMounted(() => {
  1399. /**
  1400. * 初始化级联菜单值初始值思路:
  1401. * 1. 带隐藏字段(即带编码规则)的级联菜单
  1402. * 在隐藏字段这,可以取到要回显的值和编码规则,从而计算出各级下拉菜单要回显的值。
  1403. * 然后通过ajax取各级级联菜单的值回显。
  1404. * 2. 不带隐藏字段的级联,只能在各个下拉菜单的setup事件中通过ajax取回显值回显
  1405. */
  1406. // 当同组级联下拉菜单选中值变化时,会调用本隐藏字段下面这方法设置隐藏字段值
  1407. window.addEventListener(
  1408. "cascader-setHiddenVal-" + props.name,
  1409. (event) => {
  1410. const { value } = event.detail;
  1411. emit("update:modelValue", value);
  1412. console.log(value);
  1413. setTimeout(() => {
  1414. validate();
  1415. }, 50);
  1416. }
  1417. );
  1418. // 如果有初始值,触发回显过程
  1419. if (props.modelValue) {
  1420. console.log("级联隐藏字段,开始回显,初始值:", props.modelValue);
  1421. triggerCascaderEcho(props.modelValue);
  1422. validate();
  1423. }
  1424. });
  1425. // 触发级联回显
  1426. const triggerCascaderEcho = (code) => {
  1427. /**
  1428. * 开始回显,初始值: 440304
  1429. * 解析后的所有值: Array(3)0: "440000"1: "440300"2: "440304"length: 3[[Prototype]]: Array(0)
  1430. */
  1431. const values = parseHiddenCodeForAll(code, props.rule);
  1432. console.log("解析后的所有值:", values);
  1433. // 转换为 JSON 对象
  1434. // const paramObj = JSON.parse(props.param);
  1435. const paramObj = props.param;
  1436. let selectArr = paramObj.fieldOrd; //保存本组级联菜单项的数组,如:['hksheng','hkshi','hkxian']
  1437. if (selectArr.length != values.length) {
  1438. // alert('属性'+props.name+'的值'+code+'与级联菜单中下拉菜单的数目不匹配!');
  1439. return;
  1440. }
  1441. // 按顺序触发回显,并增加延迟确保数据加载
  1442. /**
  1443. * 通过隐藏字段的setup事件,
  1444. * 循环遍历各级下拉菜单,并触发定义在下拉菜单中的'cascader-echo'事件,
  1445. * 在此事件中完成每个下拉菜单回显值操作(只取当前要回显的键值对显示,
  1446. * 下拉菜单所有的值,在点击下拉菜单时,才通过ajax取)。
  1447. */
  1448. values.forEach((value, index) => {
  1449. if (value) {
  1450. setTimeout(() => {
  1451. let upperVal = undefined;
  1452. if (index != 0) {
  1453. upperVal = values[index - 1];
  1454. }
  1455. const echoEvent = new CustomEvent(
  1456. "cascader-echo-" + selectArr[index],
  1457. {
  1458. detail: {
  1459. name: props.name,
  1460. value: value,
  1461. // level: index + 1,
  1462. isAuto: true, // 标记为自动回显
  1463. upperVal: upperVal,
  1464. },
  1465. }
  1466. );
  1467. console.log(props.name + "--upperValue:" + upperVal);
  1468. window.dispatchEvent(echoEvent);
  1469. }, index * 500); // 每级增加500ms延迟
  1470. }
  1471. });
  1472. };
  1473. // 解析所有级别的代码
  1474. const parseHiddenCodeForAll = (code, rule) => {
  1475. if (!code || !rule) return [];
  1476. // 获取规则中每段的长度
  1477. const segments = [];
  1478. let currentChar = rule[0];
  1479. let currentLength = 1;
  1480. for (let i = 1; i < rule.length; i++) {
  1481. if (rule[i] === currentChar) {
  1482. currentLength++;
  1483. } else {
  1484. segments.push(currentLength);
  1485. currentChar = rule[i];
  1486. currentLength = 1;
  1487. }
  1488. }
  1489. segments.push(currentLength);
  1490. // 解析每一级的值
  1491. const values = [];
  1492. let position = 0;
  1493. segments.forEach((length, index) => {
  1494. const value = code
  1495. .substring(0, position + length)
  1496. .padEnd(rule.length, "0");
  1497. values.push(value);
  1498. position += length;
  1499. });
  1500. return values;
  1501. };
  1502. watchEffect(() => {});
  1503. return {};
  1504. },
  1505. template: `<input type="hidden" :name="name" :value="modelValue">`,
  1506. };
  1507. // ss-cascader 级联选择器
  1508. const SsCcp = {
  1509. name: "SsCcp",
  1510. inheritAttrs: false,
  1511. props: {
  1512. modelValue: String,
  1513. name: {
  1514. type: String,
  1515. required: true,
  1516. },
  1517. level: {
  1518. type: Number,
  1519. required: true,
  1520. },
  1521. opt: {
  1522. type: Array,
  1523. default: () => [],
  1524. },
  1525. placeholder: {
  1526. type: String,
  1527. default: "请选择",
  1528. },
  1529. width: {
  1530. type: String,
  1531. default: "150px",
  1532. },
  1533. direction: {
  1534. type: String,
  1535. default: "bottom",
  1536. },
  1537. mode: {
  1538. type: String,
  1539. default: "1",
  1540. },
  1541. //级联菜单配置参数,如果是数组,则代表本下拉菜单是多套级联菜单共用的第一级菜单。如果是对象,则只有一套级联菜单用此下拉菜单。
  1542. param: {
  1543. type: String,
  1544. required: true,
  1545. },
  1546. //向后台拿数据的url
  1547. url: {
  1548. type: String,
  1549. required: true,
  1550. },
  1551. },
  1552. emits: ["update:modelValue", "change"],
  1553. setup(props, { emit }) {
  1554. // alert('级联菜单初始化:'+props.name+':--:'+props.modelValue);
  1555. const selectItem = Vue.ref({ label: props.placeholder, value: "" });
  1556. const popupWinVisible = Vue.ref(false);
  1557. const isAutoEcho = Vue.ref(false); // 用于标记是否是自动回显
  1558. const upperValue = Vue.ref(""); //上级下拉菜单当前值,在初始化下拉菜单默认值时,和上级下拉菜单的值变化时,修改此upperValue变量
  1559. const popupDirection = Vue.ref("bottom");
  1560. const popupMaxHeight = Vue.ref("none"); // popup最大高度,用于空间不足时限制高度并出滚动条 by xu 20251212
  1561. //有隐藏字段的下拉菜单,加载菜单项并展开事件
  1562. // 被上级下拉菜单选中值后,触发本下拉菜单刷新菜单项并弹出显示
  1563. window.addEventListener("cascader-open-" + props.name, async (event) => {
  1564. const { upperVal } = event.detail;
  1565. upperValue.value = upperVal;
  1566. console.log(
  1567. "22props.name:" +
  1568. props.name +
  1569. ",22props.upperValue:" +
  1570. upperValue.value
  1571. );
  1572. selectItem.value = ""; //清除本下拉菜单当前选中的值
  1573. emit("update:modelValue", ""); //通知父级
  1574. //清空下拉菜单,并设置第一项的值为placeholder
  1575. clearAndInit1stOpt();
  1576. //下个下拉菜单名
  1577. let nextSelName = getNextSel(props.name, props.param.fieldOrd);
  1578. if (nextSelName) {
  1579. //清下个下拉菜单选中值和选项
  1580. event = new CustomEvent("cascader-cleanOpt-" + nextSelName, {
  1581. detail: {},
  1582. });
  1583. window.dispatchEvent(event);
  1584. }
  1585. showPopup();
  1586. });
  1587. //设置mode2的下级下拉菜单的上级菜单当前值
  1588. function setNextSelectUpperValue() {
  1589. //设置下级菜单的上级菜单当前值upperValue
  1590. let paramArr = undefined;
  1591. if (Array.isArray(props.param)) {
  1592. paramArr = props.param;
  1593. } else {
  1594. paramArr = [];
  1595. paramArr.push(props.param);
  1596. }
  1597. for (const oneParam of paramArr) {
  1598. //下个下拉菜单名
  1599. const nextSelName = getNextSel(props.name, oneParam.fieldOrd);
  1600. if (nextSelName) {
  1601. setTimeout(() => {
  1602. const openNextEvent = new CustomEvent(
  1603. "cascade-setUpperVal-" + nextSelName,
  1604. {
  1605. detail: {
  1606. upperVal: props.modelValue,
  1607. },
  1608. }
  1609. );
  1610. window.dispatchEvent(openNextEvent);
  1611. }, 100);
  1612. }
  1613. } // end for
  1614. }
  1615. // 把上级 级联下拉菜单的值,设置进本组件的事件
  1616. window.addEventListener("cascade-setUpperVal-" + props.name, (event) => {
  1617. // alert('props.name:'+props.name+',props.upperValue:'+event.detail.upperVal);
  1618. const { upperVal } = event.detail;
  1619. upperValue.value = upperVal;
  1620. // console.log('props.name:'+props.name+',props.upperValue:'+upperValue.value);
  1621. });
  1622. //清空下拉菜单,并设置第一项的值为空
  1623. function clearAndInit1stOpt() {
  1624. if (props.opt)
  1625. props.opt.length = 0; //通过修改数组的length属性,直接清空数组元素,内存会被自动释放。这是性能最优的方式
  1626. else {
  1627. props.opt = [];
  1628. }
  1629. props.opt.push({ label: "", value: "" });
  1630. }
  1631. //获取下一级下拉菜单,如果下一级下拉菜单不存在,则返回undefined
  1632. function getNextSel(selName, selNameArr) {
  1633. // 检查参数有效性
  1634. if (!Array.isArray(selNameArr) || selNameArr.length === 0) {
  1635. return undefined;
  1636. }
  1637. // 查找当前元素的索引
  1638. const currentIndex = selNameArr.indexOf(selName);
  1639. // 如果元素不存在或已经是最后一个元素,返回undefined
  1640. if (currentIndex === -1 || currentIndex === selNameArr.length - 1) {
  1641. return undefined;
  1642. }
  1643. // 返回下一个元素
  1644. return selNameArr[currentIndex + 1];
  1645. }
  1646. const validate = () => {
  1647. if (window.ssVm) {
  1648. return window.ssVm.validateField(props.name);
  1649. }
  1650. return { valid: true };
  1651. };
  1652. // 处理选择事件
  1653. const doSelectItem = (item) => {
  1654. selectItem.value = item;
  1655. emit("update:modelValue", item.value); //修改本下拉菜单在vue中保存的值
  1656. // alert('item.value:'+item.value);
  1657. if (props.mode === "1") {
  1658. // mode 1 模式:修改隐藏字段值
  1659. let event = new CustomEvent(
  1660. "cascader-setHiddenVal-" + props.param.combField,
  1661. {
  1662. detail: {
  1663. value: item.value,
  1664. },
  1665. }
  1666. );
  1667. window.dispatchEvent(event);
  1668. }
  1669. emit("change", item.value); //触发配置的change方法
  1670. nextTick(() => {
  1671. validate();
  1672. if (window.ssVm) {
  1673. for (const [field, rules] of window.ssVm.validations.entries()) {
  1674. for (const rule of rules) {
  1675. if (rule.options?.relField) {
  1676. const relFields = String(rule.options.relField)
  1677. .split(",")
  1678. .map((name) => name.trim())
  1679. .filter(Boolean);
  1680. if (relFields.includes(props.name)) {
  1681. window.ssVm.validateField(field);
  1682. }
  1683. }
  1684. }
  1685. }
  1686. }
  1687. });
  1688. //设置下级菜单的上级菜单当前值upperValue
  1689. let paramArr = undefined;
  1690. if (Array.isArray(props.param)) {
  1691. paramArr = props.param;
  1692. } else {
  1693. paramArr = [];
  1694. paramArr.push(props.param);
  1695. }
  1696. for (const oneParam of paramArr) {
  1697. //下个下拉菜单名
  1698. const nextSelName = getNextSel(props.name, oneParam.fieldOrd);
  1699. if (nextSelName) {
  1700. setTimeout(() => {
  1701. const openNextEvent = new CustomEvent(
  1702. "cascader-open-" + nextSelName,
  1703. {
  1704. detail: {
  1705. upperVal: item.value,
  1706. },
  1707. }
  1708. );
  1709. window.dispatchEvent(openNextEvent);
  1710. }, 100);
  1711. }
  1712. } // end for
  1713. hidePopup();
  1714. //下个下拉菜单名
  1715. // let nextSelName = getNextSel(props.name, props.param.fieldOrd);
  1716. // if(nextSelName){
  1717. // // //设置下一级下拉菜单中保存的本下拉菜单值(upperValue)
  1718. // // event = new CustomEvent('cascade-setUpperVal-'+nextSelName, {
  1719. // // detail: {
  1720. // // value: item.value
  1721. // // }
  1722. // // });
  1723. // // window.dispatchEvent(event);
  1724. //
  1725. // //触发下一级下拉菜单,重新初始化下拉菜单项并弹出显示
  1726. // event = new CustomEvent('cascader-open-' +nextSelName, {
  1727. // detail: {
  1728. // upperVal: item.value
  1729. // }
  1730. // });
  1731. // window.dispatchEvent(event);
  1732. // }
  1733. // 只在手动选择时自动展开下一级
  1734. // if (!isAutoEcho.value) {
  1735. // const nextLevel = props.level + 1;
  1736. // setTimeout(() => {
  1737. // const openNextEvent = new CustomEvent('open-next-cascader', {
  1738. // detail: {
  1739. // name: props.name,
  1740. // level: nextLevel
  1741. // }
  1742. // });
  1743. // window.dispatchEvent(openNextEvent);
  1744. // }, 100);
  1745. // }
  1746. };
  1747. // 监听下一级展开事件 (仅 mode 2)
  1748. window.addEventListener("cascade-open", (event) => {
  1749. if (props.mode === "2") {
  1750. const { level } = event.detail;
  1751. if (level === props.level) {
  1752. popupWinVisible.value = true;
  1753. }
  1754. }
  1755. });
  1756. if (props.mode === "1") {
  1757. //如果是有隐藏字段的下拉菜单
  1758. // 监听回显事件
  1759. window.addEventListener(
  1760. "cascader-echo-" + props.name,
  1761. async (event) => {
  1762. const { name, value, isAuto, upperVal } = event.detail;
  1763. // level,
  1764. if (upperVal) {
  1765. upperValue.value = upperVal;
  1766. console.log(
  1767. "value:" +
  1768. value +
  1769. ",upperValue:" +
  1770. upperValue +
  1771. ",初始化级联组件时props.name:" +
  1772. props.name
  1773. );
  1774. }
  1775. // if (name === props.name && level === props.level) {
  1776. // 设置自动回显标记
  1777. isAutoEcho.value = true;
  1778. // if (props.opt.length === 0) {
  1779. // const loadDataEvent = new CustomEvent('cascader-load-data', {
  1780. // detail: {
  1781. // name: props.name,
  1782. // level: props.level,
  1783. // value: value
  1784. // }
  1785. // });
  1786. // window.dispatchEvent(loadDataEvent);
  1787. //下面的代码只用于页面刚打开时,初始化级联菜单的回显值。
  1788. //Vue.watch用于监听数据的变化,并在数据变化时执行特定的回调函数。
  1789. //这段代码使用了 Vue.js 的 watch API 来监听 props.opt 的变化,如果props.opt有变化,则自动
  1790. // const unwatch = Vue.watch(
  1791. // () => props.opt, // 监听的数据源(props.opt)
  1792. // (newOptions) => { // 回调函数
  1793. // if (newOptions.length > 0) { // 条件判断
  1794. // matchAndSelect(value); // 执行逻辑
  1795. // unwatch(); // 停止监听
  1796. // }
  1797. // },
  1798. // { immediate: true } // 配置:立即触发一次
  1799. // );
  1800. // } else {
  1801. // matchAndSelect(value);
  1802. // }
  1803. // 初始化级联菜单在页面刚打开时的默认值
  1804. async function initDefaultValue(value) {
  1805. try {
  1806. // alert(1);
  1807. if (
  1808. props.url &&
  1809. props.param
  1810. // && props.modelValue 对于有rule编码规则的级联菜单(即mode=1),modelValue一定是空的,所以注释掉,修复mode=1的级联菜单无法回显问题。Ben(20251124)
  1811. ) {
  1812. // alert(2);
  1813. /**
  1814. * let objectPickerParam=
  1815. * {"objectpickerparam":"{\"input\":\"false\",\"cascadingLevel\":\"hksheng,hkshi,hkxian\"," +
  1816. * "\"name\":\"hksheng\"," +
  1817. * "\"cascadingName\":\"dq\",\"cascadingInputsName\":\"hkdqm\"," +
  1818. * "\"codebook\":\"sheng\"}",
  1819. * "objectpickertype":2,
  1820. * "objectpickervalue":"440000"
  1821. * };
  1822. */
  1823. const objectPickerParam = {
  1824. input: "false",
  1825. cascadingLevel: props.param.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian"
  1826. name: props.name, //本下拉菜单名
  1827. cascadingName: props.param.name, //级联菜单名
  1828. cascadingInputsName: props.param.combField, //对象属性,即隐藏字段名,如:hkdqm
  1829. codebook: props.param.codebook,
  1830. };
  1831. const objectPickerParamStr =
  1832. JSON.stringify(objectPickerParam);
  1833. const params = new URLSearchParams();
  1834. params.append("objectpickerparam", objectPickerParamStr);
  1835. params.append("objectpickertype", "2");
  1836. params.append("objectpickervalue", value); //需回显的值
  1837. axios
  1838. .post(props.url, params, {
  1839. headers: {
  1840. "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
  1841. },
  1842. })
  1843. .then((response) => {
  1844. // alert(JSON.stringify(response.data));
  1845. if ("timeout" == response.data.statusText) {
  1846. alert("网络超时!");
  1847. return;
  1848. }
  1849. if (response.data.result) {
  1850. const keys = Object.keys(response.data.result);
  1851. if (keys.length === 1) {
  1852. let code = keys[0];
  1853. let desc = response.data.result[keys[0]];
  1854. clearAndInit1stOpt();
  1855. props.opt.push({ label: desc, value: code });
  1856. if (value) matchAndSelect(value);
  1857. // updateSelectItem();
  1858. // alert('props.opt:'+JSON.stringify(props.opt));
  1859. }
  1860. }
  1861. });
  1862. }
  1863. } catch (error) {
  1864. alert(error);
  1865. // callback(null, error.message); // 失败回调,传递错误
  1866. }
  1867. }
  1868. //下面的代码只用于页面刚打开时,初始化级联菜单的回显值。
  1869. initDefaultValue(value);
  1870. // 延迟重置自动回显标记
  1871. setTimeout(() => {
  1872. isAutoEcho.value = false;
  1873. }, 500);
  1874. }
  1875. );
  1876. // 被上级下拉菜单触发的,清除选中值和下拉菜单选项
  1877. window.addEventListener(
  1878. "cascader-cleanOpt-" + props.name,
  1879. async (event) => {
  1880. upperValue.value = "";
  1881. selectItem.value = ""; //清除本下拉菜单当前选中的值
  1882. emit("update:modelValue", ""); //通知父级
  1883. //清空所有下拉菜单项
  1884. if (props.opt) {
  1885. props.opt.length = 0;
  1886. } else {
  1887. props.opt = [];
  1888. }
  1889. //下个下拉菜单名
  1890. let nextSelName = getNextSel(props.name, props.param.fieldOrd);
  1891. // alert('nextSelName:'+nextSelName+'--,props.name:'+props.name);
  1892. if (nextSelName) {
  1893. //清下个下拉菜单选中值和选项
  1894. event = new CustomEvent("cascader-cleanOpt-" + nextSelName, {
  1895. detail: {},
  1896. });
  1897. window.dispatchEvent(event);
  1898. }
  1899. }
  1900. );
  1901. } else if (props.mode === "2") {
  1902. //没隐藏字段的下拉菜单,在这初始化默认值
  1903. let needInitParam = undefined;
  1904. if (Array.isArray(props.param)) {
  1905. needInitParam = props.param[props.param.length - 1]; //只初始化数组最后一项
  1906. console.log("needInitParam最后一项:" + JSON.stringify(needInitParam));
  1907. } else {
  1908. needInitParam = props.param;
  1909. }
  1910. // 初始化级联菜单在页面刚打开时的默认值
  1911. async function initDefaultValue(value, param) {
  1912. try {
  1913. // alert(1);
  1914. if (props.url && param && props.modelValue) {
  1915. // alert(2);
  1916. /**
  1917. * let param=
  1918. * {"objectpickerparam":"{\"input\":\"false\",\"cascadingLevel\":\"rylbm,gwid\"," +
  1919. * "\"name\":\"gwid\",\"cascadingName\":\"rylb_gw\"," +
  1920. * "\"codebook\":\"gwByRylb\"}",
  1921. * "objectpickertype":2,
  1922. * "objectpickervalue":"102121"};
  1923. */
  1924. // alert('props.name:'+props.name+',props.param.fieldOrd:'+props.param.fieldOrd);
  1925. const objectPickerParam = {
  1926. input: "false",
  1927. cascadingLevel: param.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian"
  1928. name: props.name, //本下拉菜单名
  1929. cascadingName: param.name, //级联菜单名
  1930. codebook: param.codebook,
  1931. };
  1932. const objectPickerParamStr = JSON.stringify(objectPickerParam);
  1933. const sendParams = new URLSearchParams();
  1934. sendParams.append("objectpickerparam", objectPickerParamStr);
  1935. sendParams.append("objectpickertype", "2");
  1936. sendParams.append("objectpickervalue", value); //需回显的值
  1937. axios
  1938. .post(props.url, sendParams, {
  1939. headers: {
  1940. "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
  1941. },
  1942. })
  1943. .then((response) => {
  1944. // alert(JSON.stringify(response.data));
  1945. if ("timeout" == response.data.statusText) {
  1946. alert("网络超时!");
  1947. return;
  1948. }
  1949. if (response.data.result) {
  1950. const keys = Object.keys(response.data.result);
  1951. console.log(
  1952. "name:" +
  1953. props.name +
  1954. ",@@级联初始化默认值value:" +
  1955. value +
  1956. "--param:" +
  1957. JSON.stringify(param) +
  1958. "--objectPickerParamStr:" +
  1959. objectPickerParamStr +
  1960. "--response.data:" +
  1961. JSON.stringify(response.data)
  1962. );
  1963. if (keys.length === 1) {
  1964. let code = keys[0];
  1965. let desc = response.data.result[keys[0]];
  1966. if (props.opt)
  1967. props.opt.length = 0; //通过修改数组的length属性,直接清空数组元素,内存会被自动释放。这是性能最优的方式
  1968. else {
  1969. props.opt = [];
  1970. }
  1971. props.opt.push({ label: desc, value: code });
  1972. if (value) matchAndSelect(value);
  1973. console.log(
  1974. "GOOD mode2回显的默认值:" +
  1975. JSON.stringify({ label: desc, value: code }) +
  1976. "--props.param:" +
  1977. JSON.stringify(param)
  1978. );
  1979. // updateSelectItem();
  1980. // alert('props.opt:'+JSON.stringify(props.opt));
  1981. }
  1982. }
  1983. });
  1984. }
  1985. } catch (error) {
  1986. alert(error);
  1987. // callback(null, error.message); // 失败回调,传递错误
  1988. }
  1989. // 重置自动回显标记
  1990. isAutoEcho.value = false;
  1991. }
  1992. // 初始化级联菜单在页面刚打开时的默认值
  1993. initDefaultValue(props.modelValue, needInitParam);
  1994. //设置mode2的下级下拉菜单的上级菜单当前值
  1995. setNextSelectUpperValue();
  1996. }
  1997. //选中要回显的默认值
  1998. const matchAndSelect = (value) => {
  1999. const matchedOption = props.opt.find((opt) => opt.value === value);
  2000. if (matchedOption) {
  2001. selectItem.value = matchedOption;
  2002. emit("update:modelValue", value);
  2003. emit("change", value);
  2004. }
  2005. };
  2006. // 计算弹出方向和最大高度的方法 by xu 20251212
  2007. // 当空间不足时限制popup高度并显示滚动条
  2008. const calculatePopupDirection = () => {
  2009. // 1. 获取select容器元素
  2010. const selectEl = document.querySelector(
  2011. `[name="${props.name}"]`
  2012. )?.nextElementSibling;
  2013. console.log("selectEl:" + selectEl, props.name);
  2014. if (!selectEl) return;
  2015. // 2. 获取位置信息
  2016. const selectRect = selectEl.getBoundingClientRect();
  2017. const viewportHeight = window.innerHeight;
  2018. // 3. 计算上下可用空间 by xu 20251212
  2019. const spaceBelow = viewportHeight - selectRect.bottom - 10; // 减10px留边距
  2020. const spaceAbove = selectRect.top - 10; // 减10px留边距
  2021. // 4. popup预估高度(假设每项36px,最多显示8项 + padding)
  2022. const estimatedPopupHeight = 300;
  2023. const minPopupHeight = 100; // 最小高度
  2024. console.log(
  2025. "[popup] 空间计算 - spaceAbove:",
  2026. spaceAbove,
  2027. "spaceBelow:",
  2028. spaceBelow,
  2029. "estimatedHeight:",
  2030. estimatedPopupHeight
  2031. );
  2032. // 5. 判断方向和最大高度 by xu 20251212
  2033. if (spaceBelow >= estimatedPopupHeight) {
  2034. // 下方空间足够,向下展开,不限制高度
  2035. popupDirection.value = "bottom";
  2036. popupMaxHeight.value = "none";
  2037. console.log("[popup] 向下展开,空间充足");
  2038. } else if (spaceAbove >= estimatedPopupHeight) {
  2039. // 上方空间足够,向上展开,不限制高度
  2040. popupDirection.value = "top";
  2041. popupMaxHeight.value = "none";
  2042. console.log("[popup] 向上展开,空间充足");
  2043. } else {
  2044. // 上下空间都不足,选择空间大的方向,并限制高度出滚动条
  2045. if (spaceBelow >= spaceAbove) {
  2046. popupDirection.value = "bottom";
  2047. popupMaxHeight.value = Math.max(spaceBelow, minPopupHeight) + "px";
  2048. console.log(
  2049. "[popup] 向下展开,空间不足,限制高度:",
  2050. popupMaxHeight.value
  2051. );
  2052. } else {
  2053. popupDirection.value = "top";
  2054. popupMaxHeight.value = Math.max(spaceAbove, minPopupHeight) + "px";
  2055. console.log(
  2056. "[popup] 向上展开,空间不足,限制高度:",
  2057. popupMaxHeight.value
  2058. );
  2059. }
  2060. }
  2061. };
  2062. //级联菜单点击事件
  2063. const togglePopup = () => {
  2064. if (!popupWinVisible.value) {
  2065. //如果当前下拉菜单是隐藏的,先ajax重新加载下拉菜单项,再显示。
  2066. showPopup();
  2067. } else {
  2068. hidePopup();
  2069. }
  2070. };
  2071. //显示下拉菜单,在此之前先清除下拉菜单项
  2072. const showPopup = () => {
  2073. //清空下拉菜单,并设置第一项的值为空
  2074. clearAndInit1stOpt();
  2075. Vue.nextTick(() => {
  2076. calculatePopupDirection();
  2077. });
  2078. let url = props.url;
  2079. let filterObj = props.param.filter;
  2080. if (filterObj) {
  2081. for (let k in filterObj) {
  2082. let v = filterObj[k];
  2083. url += "&" + k + "=" + v;
  2084. }
  2085. }
  2086. if (props.mode === "1") {
  2087. //如果是有隐藏字段的下拉菜单
  2088. console.log("666url:" + url);
  2089. // alert('url:'+url);
  2090. // 获取级联菜单所有下拉菜单项
  2091. async function getSelectItems(value) {
  2092. try {
  2093. // alert(1);
  2094. if (props.url && props.param) {
  2095. // alert(2);
  2096. /**
  2097. * param={"objectpickerparam":"{\"input\":\"false\",\"cascadingLevel\":\"hksheng,hkshi,hkxian\"," +
  2098. * "\"name\":\"hksheng\",\"cascadingName\":\"dq\"," +
  2099. * "\"cascadingInputsName\":\"hkdqm\",\"codebook\":\"sheng\"}",
  2100. * "objectpickertype":1,//2表示获取要回显的一项,1表示获取所有下拉菜单项
  2101. * "upperValue":"440000"
  2102. * };
  2103. */
  2104. const objectPickerParam = {
  2105. input: "false",
  2106. cascadingLevel: props.param.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian"
  2107. name: props.name, //本下拉菜单名
  2108. cascadingName: props.param.name, //级联菜单名
  2109. cascadingInputsName: props.param.combField, //对象属性,即隐藏字段名,如:hkdqm
  2110. codebook: props.param.codebook,
  2111. };
  2112. console.log("mode1 upperValue.value:" + upperValue.value);
  2113. const objectPickerParamStr = JSON.stringify(objectPickerParam);
  2114. const params = new URLSearchParams();
  2115. params.append("objectpickerparam", objectPickerParamStr);
  2116. params.append("objectpickertype", "1");
  2117. if (upperValue.value) {
  2118. params.append("upperValue", upperValue.value);
  2119. }
  2120. // params.append('objectpickervalue', value); //需回显的值
  2121. axios
  2122. .post(url, params, {
  2123. headers: {
  2124. "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
  2125. },
  2126. })
  2127. .then((response) => {
  2128. if ("timeout" == response.data.statusText) {
  2129. alert("网络超时!");
  2130. return;
  2131. }
  2132. if (response.data.result) {
  2133. const keys = Object.keys(response.data.result);
  2134. console.log(
  2135. "params:" +
  2136. params +
  2137. "@@response.data:" +
  2138. JSON.stringify(response.data)
  2139. );
  2140. if (keys.length > 0) {
  2141. for (let k in response.data.result) {
  2142. props.opt.push({
  2143. label: response.data.result[k],
  2144. value: k,
  2145. });
  2146. }
  2147. console.log("props.opt11:" + JSON.stringify(props.opt));
  2148. } else {
  2149. // 没有数据时打印日志 by xu 20251212
  2150. console.log("[ccp mode1] 接口返回空数据");
  2151. }
  2152. } else {
  2153. // result不存在时打印日志 by xu 20251212
  2154. console.log("[ccp mode1] 接口返回无result");
  2155. }
  2156. // 无论是否有数据,都显示popup by xu 20251212
  2157. if (!popupWinVisible.value) {
  2158. popupWinVisible.value = true;
  2159. }
  2160. });
  2161. }
  2162. } catch (error) {
  2163. alert(error);
  2164. // callback(null, error.message); // 失败回调,传递错误
  2165. }
  2166. }
  2167. getSelectItems(props.modelValue);
  2168. } else if (props.mode === "2") {
  2169. //没隐藏字段的下拉菜单
  2170. let needInitParam = undefined;
  2171. if (Array.isArray(props.param)) {
  2172. needInitParam = props.param[props.param.length - 1]; //只初始化数组最后一项
  2173. console.log(
  2174. "needInitParam最后一项:" + JSON.stringify(needInitParam)
  2175. );
  2176. } else {
  2177. needInitParam = props.param;
  2178. }
  2179. // 获取级联菜单所有下拉菜单项
  2180. async function getSelectItems(value, sendParam) {
  2181. try {
  2182. // alert(1);
  2183. if (props.url && sendParam) {
  2184. // alert(2);
  2185. /**
  2186. * param="{\"input\":\"false\",\"cascadingLevel\":\"dwid,sjryid\",
  2187. * \"ryid\":\"111121\",\"name\":\"sjryid\",
  2188. * \"cascadingName\":\"dw_sjry\",\"codebook\":\"sjryByDw\"}"
  2189. */
  2190. const objectPickerParam = {
  2191. input: "false",
  2192. cascadingLevel: sendParam.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian"
  2193. name: props.name, //本下拉菜单名
  2194. cascadingName: sendParam.name, //级联菜单名
  2195. codebook: sendParam.codebook,
  2196. };
  2197. console.log("mode2 upperValue.value:" + upperValue.value);
  2198. const objectPickerParamStr = JSON.stringify(objectPickerParam);
  2199. const params = new URLSearchParams();
  2200. params.append("objectpickerparam", objectPickerParamStr);
  2201. params.append("objectpickertype", "1");
  2202. if (upperValue.value) {
  2203. params.append("upperValue", upperValue.value);
  2204. }
  2205. // params.append('objectpickervalue', value); //需回显的值
  2206. axios
  2207. .post(url, params, {
  2208. headers: {
  2209. "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
  2210. },
  2211. })
  2212. .then((response) => {
  2213. if ("timeout" == response.data.statusText) {
  2214. alert("网络超时!");
  2215. return;
  2216. }
  2217. if (response.data.result) {
  2218. const keys = Object.keys(response.data.result);
  2219. console.log(
  2220. "params:" +
  2221. params +
  2222. "@@response.data:" +
  2223. JSON.stringify(response.data)
  2224. );
  2225. if (keys.length > 0) {
  2226. for (let k in response.data.result) {
  2227. props.opt.push({
  2228. label: response.data.result[k],
  2229. value: k,
  2230. });
  2231. }
  2232. console.log("props.opt11:" + JSON.stringify(props.opt));
  2233. } else {
  2234. // 没有数据时打印日志 by xu 20251212
  2235. console.log("[ccp mode2] 接口返回空数据");
  2236. }
  2237. } else {
  2238. // result不存在时打印日志 by xu 20251212
  2239. console.log("[ccp mode2] 接口返回无result");
  2240. }
  2241. // 无论是否有数据,都显示popup by xu 20251212
  2242. if (!popupWinVisible.value) {
  2243. popupWinVisible.value = true;
  2244. }
  2245. });
  2246. }
  2247. } catch (error) {
  2248. alert(error);
  2249. // callback(null, error.message); // 失败回调,传递错误
  2250. }
  2251. }
  2252. getSelectItems(props.modelValue, needInitParam);
  2253. // popupWinVisible.value = !popupWinVisible.value;
  2254. }
  2255. };
  2256. const hidePopup = () => {
  2257. popupWinVisible.value = false;
  2258. };
  2259. // 合并所有的 onMounted 逻辑
  2260. Vue.onMounted(() => {
  2261. window.addEventListener("resize", calculatePopupDirection);
  2262. // 1. 监听展开下一级事件
  2263. window.addEventListener("open-next-cascader", (event) => {
  2264. const { name, level } = event.detail;
  2265. if (name === props.name && level === props.level) {
  2266. popupWinVisible.value = true;
  2267. }
  2268. });
  2269. // 2. 监听级联事件
  2270. window.addEventListener("cascader-change", (event) => {
  2271. const { name, level, value } = event.detail;
  2272. if (name === props.name && level < props.level) {
  2273. selectItem.value = { label: "", value: "" };
  2274. emit("update:modelValue", "");
  2275. if (ssHidden) {
  2276. ssHidden.updateValue(value);
  2277. }
  2278. }
  2279. });
  2280. });
  2281. Vue.onUnmounted(() => {
  2282. window.removeEventListener("resize", calculatePopupDirection);
  2283. });
  2284. // 监听值变化,处理回显 (mode 2)
  2285. Vue.watch(
  2286. () => props.modelValue,
  2287. (newVal) => {
  2288. if (props.mode === "2" && newVal) {
  2289. // 使用 watchEffect 替代嵌套的 watch
  2290. Vue.watchEffect(() => {
  2291. if (props.opt.length > 0) {
  2292. const matchedOption = props.opt.find(
  2293. (opt) => opt.value === newVal
  2294. );
  2295. if (matchedOption) {
  2296. selectItem.value = matchedOption;
  2297. }
  2298. }
  2299. });
  2300. } else {
  2301. // 原有的值变化处理
  2302. const item = props.opt.find((it) => it.value === newVal);
  2303. if (item) {
  2304. selectItem.value = item;
  2305. } else {
  2306. selectItem.value = { label: "", value: "" };
  2307. }
  2308. }
  2309. },
  2310. { immediate: true }
  2311. );
  2312. // 监听选项变化,当数据加载完成时进行匹配
  2313. Vue.watch(
  2314. () => props.opt,
  2315. (newOptions) => {
  2316. if (newOptions.length > 0) {
  2317. const matchedOption = newOptions.find(
  2318. (opt) => opt.value === selectItem.value.value
  2319. );
  2320. if (matchedOption) {
  2321. selectItem.value = matchedOption;
  2322. emit("update:modelValue", matchedOption.value);
  2323. emit("change", matchedOption.value);
  2324. }
  2325. }
  2326. }
  2327. );
  2328. return {
  2329. selectItem,
  2330. popupWinVisible,
  2331. popupDirection,
  2332. popupMaxHeight, // 添加popup最大高度 by xu 20251212
  2333. togglePopup,
  2334. hidePopup,
  2335. doSelectItem,
  2336. };
  2337. },
  2338. template: `
  2339. <div class="input ss-ccp-container" style="position: relative" :style="{width: width}">
  2340. <input type="hidden" :name="name" :value="modelValue">
  2341. <div class="select-container" @mouseleave="hidePopup">
  2342. <div class="input" @click="togglePopup">
  2343. <input
  2344. type="hidden"
  2345. :name="name"
  2346. :value="selectItem.value"
  2347. />
  2348. <input
  2349. :placeholder="placeholder"
  2350. :value="selectItem.label"
  2351. disabled
  2352. style="pointer-events: none;"
  2353. />
  2354. <div class="suffix">
  2355. <ss-form-icon :class="popupWinVisible ? 'form-icon-transform-select select' : 'form-icon-select'" />
  2356. </div>
  2357. </div>
  2358. <!-- popup弹出层,添加maxHeight和overflowY支持空间不足时滚动 by xu 20251212 -->
  2359. <div v-show="popupWinVisible" class="popup-win " :class="popupDirection" :style="{ maxHeight: popupMaxHeight, overflowY: popupMaxHeight !== 'none' ? 'auto' : 'visible' }">
  2360. <div v-if="opt && opt.length > 0" class="popup-content">
  2361. <div class="content-area">
  2362. <div
  2363. v-for="(item, index) in opt"
  2364. :key="index"
  2365. @click="doSelectItem(item)"
  2366. :class="{ active: item.value === selectItem.value }"
  2367. >
  2368. <span class="check-icon">
  2369. <ss-form-icon class="form-icon-select-checked" />
  2370. </span>
  2371. <span>{{ item.label }}</span>
  2372. </div>
  2373. </div>
  2374. </div>
  2375. <div v-else class="popup-content">
  2376. <div class="content-area">
  2377. <div class="content-area">
  2378. <span>无选项</span>
  2379. </div>
  2380. </div>
  2381. </div>
  2382. </div>
  2383. </div>
  2384. </div>
  2385. `,
  2386. };
  2387. // ss-date-picker 日期时间选择器组件
  2388. const SsDatePicker = {
  2389. name: "SsDatePicker",
  2390. props: {
  2391. modelValue: {
  2392. type: [String, Number, Date],
  2393. default: "",
  2394. },
  2395. name: {
  2396. type: String,
  2397. required: true,
  2398. },
  2399. type: {
  2400. type: String,
  2401. default: "date",
  2402. validator: (value) => ["date", "datetime", "time"].includes(value),
  2403. },
  2404. fmt: {
  2405. type: String,
  2406. default: null,
  2407. },
  2408. placeholder: {
  2409. type: String,
  2410. default: "",
  2411. },
  2412. width: {
  2413. type: String,
  2414. default: "100%",
  2415. },
  2416. },
  2417. emits: ["update:modelValue"],
  2418. setup(props, { emit }) {
  2419. const errMsg = ref("");
  2420. const validate = () => {
  2421. if (window.ssVm) {
  2422. const result = window.ssVm.validateField(props.name);
  2423. console.log("validate", window.ssVm.validateField(props.name));
  2424. errMsg.value = result.valid ? "" : result.message;
  2425. }
  2426. };
  2427. // 根据type确定默认格式
  2428. const defaultFormat = computed(() => {
  2429. switch (props.type) {
  2430. case "datetime":
  2431. return "YYYY-MM-DD HH:mm:ss";
  2432. case "date":
  2433. return "YYYY-MM-DD";
  2434. case "time":
  2435. return "HH:mm:ss";
  2436. }
  2437. });
  2438. const convertJavaFormatToElement = (javaFormat) => {
  2439. if (!javaFormat) return null;
  2440. return javaFormat
  2441. .replace("yyyy", "YYYY")
  2442. .replace("MM", "MM")
  2443. .replace("dd", "DD")
  2444. .replace("HH", "HH")
  2445. .replace("mm", "mm")
  2446. .replace("ss", "ss");
  2447. };
  2448. const finalFormat = computed(() => {
  2449. if (props.fmt) {
  2450. return convertJavaFormatToElement(props.fmt);
  2451. }
  2452. return defaultFormat.value;
  2453. });
  2454. // 使用 resolveComponent 获取组件
  2455. const ElDatePicker = resolveComponent("ElDatePicker");
  2456. const ElTimePicker = resolveComponent("ElTimePicker");
  2457. const SsFormIcon = resolveComponent("SsFormIcon");
  2458. const ElIcon = resolveComponent("ElIcon");
  2459. const handleValueUpdate = (val) => {
  2460. emit("update:modelValue", val);
  2461. emit("change", val); // 同时触发 change 事件
  2462. setTimeout(() => {
  2463. validate();
  2464. }, 50);
  2465. };
  2466. const dateType = computed(() => {
  2467. const fmt = props.fmt || "";
  2468. if (fmt.includes("HH:mm:ss")) {
  2469. return "datetime";
  2470. } else if (fmt.includes("HH:mm")) {
  2471. return "datetime";
  2472. } else if (fmt.includes("mm:ss")) {
  2473. return "time";
  2474. }
  2475. return "date";
  2476. });
  2477. let useTimePicker = true;
  2478. //"yyyy-MM-dd HH:mm:ss"; "日期字符串格式在java的写法",传到本组件fmt属性也是按这个格式
  2479. if (props.fmt) {
  2480. //有fmt属性,则以fmt属性优先判断类型
  2481. if (/[dMy]/.test(props.fmt)) {
  2482. //如果有传入日期格式,且含年月日
  2483. useTimePicker = false;
  2484. } else {
  2485. useTimePicker = true;
  2486. }
  2487. } else if (props.type !== "time") {
  2488. useTimePicker = false;
  2489. }
  2490. return () =>
  2491. h("div", { class: "ss-date-picker", style: { width: props.width } }, [
  2492. h("input", {
  2493. type: "hidden",
  2494. name: props.name,
  2495. value: props.modelValue,
  2496. }),
  2497. // 选择组件
  2498. h(useTimePicker ? ElTimePicker : ElDatePicker, {
  2499. modelValue: props.modelValue,
  2500. "onUpdate:modelValue": handleValueUpdate,
  2501. type: dateType.value,
  2502. format: finalFormat.value,
  2503. "value-format": finalFormat.value,
  2504. clearable: true,
  2505. placeholder: props.placeholder,
  2506. class: "custom-date-picker", // 用于自定义样式
  2507. "time-arrow-control": props.type === "datetime", // 修改这里
  2508. size: "large", // 添加这一行,改为 large 尺寸
  2509. style: { width: "100%" },
  2510. "prefix-icon": h(SsFormIcon, { class: "form-icon-time" }),
  2511. }),
  2512. ]);
  2513. },
  2514. };
  2515. // ss-icon 图标
  2516. // v3.0 增加 class 属性分支:有 class 走新逻辑,否则走 v1.0 逻辑 by xu 20251212
  2517. // v3.0 用法: <ss-icon class="icon-obj-ry menu-icon" />
  2518. // v1.0 用法: <ss-icon name="setting" size="20px" />
  2519. const SsIcon = {
  2520. name: "SsIcon",
  2521. // v3.0 禁用 class 透传,手动处理 by xu 20251215
  2522. inheritAttrs: false,
  2523. props: {
  2524. // v1.0: 以下为旧属性
  2525. name: { type: String },
  2526. size: { type: [Number, String], default: 16 },
  2527. unit: { type: String, default: "px" },
  2528. color: String,
  2529. type: {
  2530. type: String,
  2531. default: ssIcon.name,
  2532. validator: function (value) {
  2533. return [ssIcon, commonIcon].some((icon) => icon.name === value);
  2534. },
  2535. },
  2536. },
  2537. emits: ["update:modelValue", "input", "blur", "change"],
  2538. setup(props, { emit, attrs }) {
  2539. // v3.0 分支:有 class 属性时直接渲染(从 attrs 获取) by xu 20251215
  2540. if (attrs.class) {
  2541. return () =>
  2542. h("i", { ...attrs, class: attrs.class + " icon-container" });
  2543. }
  2544. // v1.0 分支:原有逻辑
  2545. const useIconType = computed(() => {
  2546. return [ssIcon, commonIcon].find(
  2547. (iconConfig) => iconConfig.name === props.type
  2548. );
  2549. });
  2550. const iconName = computed(() => {
  2551. const iconConfig = useIconType.value; // 注意:使用 .value 来访问响应式引用的值
  2552. if (!iconConfig) {
  2553. console.error(`Icon type "${props.type}" not found.`);
  2554. return "";
  2555. }
  2556. const iconType = iconConfig.types[props.name];
  2557. if (!iconType) {
  2558. console.error(
  2559. `Icon name "${props.name}" not found in type "${props.type}".`
  2560. );
  2561. return "";
  2562. }
  2563. return `${iconConfig.prefix}${iconType}`;
  2564. });
  2565. // 类似地,你可以计算 fontFamily 和 style
  2566. const fontFamily = computed(() => {
  2567. return useIconType.value ? useIconType.value.family : "";
  2568. });
  2569. // console.log(iconName.value,fontFamily.value)
  2570. const style = computed(() => {
  2571. const sizeStyle = isNum(props.size)
  2572. ? `${props.size}${props.unit}`
  2573. : props.size;
  2574. const styleObj = {
  2575. fontSize: sizeStyle,
  2576. color: props.color || "",
  2577. };
  2578. return toStyleStr(styleObj);
  2579. });
  2580. // 使用渲染函数定义模板逻辑
  2581. return () =>
  2582. h("i", {
  2583. class: ["icon-container", iconName.value, fontFamily.value],
  2584. style: style.value,
  2585. });
  2586. },
  2587. };
  2588. // 通用icon
  2589. const SsCommonIcon = {
  2590. name: "SsCommonIcon",
  2591. props: {
  2592. class: {
  2593. type: String,
  2594. required: true,
  2595. },
  2596. },
  2597. setup(props) {
  2598. return () =>
  2599. h("i", {
  2600. class: props.class + " common-icon",
  2601. });
  2602. },
  2603. };
  2604. // 登录页icon
  2605. const SsLoginIcon = {
  2606. name: "SsLoginIcon",
  2607. props: {
  2608. class: {
  2609. type: String,
  2610. required: true,
  2611. },
  2612. },
  2613. setup(props) {
  2614. return () =>
  2615. h("div", {
  2616. class: props.class + " login-icon",
  2617. });
  2618. },
  2619. };
  2620. // 弹窗icon
  2621. const SsDialogIcon = {
  2622. name: "SsDialogIcon",
  2623. props: {
  2624. class: {
  2625. type: String,
  2626. required: true,
  2627. },
  2628. },
  2629. setup(props) {
  2630. return () =>
  2631. h("i", {
  2632. class: props.class + " dialog-icon",
  2633. });
  2634. },
  2635. };
  2636. // 全局左侧导航图标组件
  2637. const SsNavIcon = {
  2638. name: "SsNavIcon",
  2639. props: {
  2640. class: {
  2641. type: String,
  2642. required: true,
  2643. },
  2644. },
  2645. setup(props) {
  2646. return () =>
  2647. h("div", {
  2648. class: props.class + " nav-icon",
  2649. });
  2650. },
  2651. };
  2652. // 顶部工具栏图标组件
  2653. const SsHeaderIcon = {
  2654. name: "SsHeaderIcon",
  2655. props: {
  2656. class: {
  2657. type: String,
  2658. required: true,
  2659. },
  2660. },
  2661. setup(props) {
  2662. return () =>
  2663. h("div", {
  2664. class: props.class + " header-icon",
  2665. });
  2666. },
  2667. };
  2668. // 全局菜单图标组件
  2669. const SsGolbalMenuIcon = {
  2670. name: "SsGolbalMenuIcon",
  2671. props: {
  2672. class: {
  2673. type: String,
  2674. required: true,
  2675. },
  2676. },
  2677. setup(props) {
  2678. return () =>
  2679. h("div", {
  2680. class: props.class + " global-menu-icon",
  2681. });
  2682. },
  2683. };
  2684. // 全局查询列表卡片图标
  2685. const SsCartListIcon = {
  2686. name: "SsCartListIcon",
  2687. props: {
  2688. class: {
  2689. type: String,
  2690. required: true,
  2691. },
  2692. },
  2693. setup(props) {
  2694. return () =>
  2695. h("div", {
  2696. class: props.class + " cart-list-icon",
  2697. });
  2698. },
  2699. };
  2700. // 全局底部工具栏图标组件
  2701. const SsQuickIcon = {
  2702. name: "SsQuickIcon",
  2703. props: {
  2704. class: {
  2705. type: String,
  2706. required: true,
  2707. },
  2708. },
  2709. setup(props) {
  2710. return () =>
  2711. h("div", {
  2712. class: props.class + " quick-icon",
  2713. });
  2714. },
  2715. };
  2716. // 表单组件icon
  2717. const SsFormIcon = {
  2718. name: "SsFormIcon",
  2719. props: {
  2720. class: {
  2721. type: String,
  2722. required: true,
  2723. },
  2724. },
  2725. setup(props) {
  2726. return () =>
  2727. h("div", {
  2728. class: props.class + " form-icon",
  2729. });
  2730. },
  2731. };
  2732. // 弹窗底部按钮icon
  2733. const SsBottomDivIcon = {
  2734. name: "SsBottomDivIcon",
  2735. props: {
  2736. class: {
  2737. type: String,
  2738. required: true,
  2739. },
  2740. },
  2741. setup(props) {
  2742. return () =>
  2743. h("div", {
  2744. class: props.class + " bottom-div-icon",
  2745. });
  2746. },
  2747. };
  2748. // editor组件icon
  2749. const SsEditorIcon = {
  2750. name: "SsEditorIcon",
  2751. props: {
  2752. class: {
  2753. type: String,
  2754. required: true,
  2755. },
  2756. },
  2757. setup(props) {
  2758. return () =>
  2759. h("i", {
  2760. class: props.class + " editor-icon",
  2761. });
  2762. },
  2763. };
  2764. // ss-validate校验器
  2765. const SsValidate = {
  2766. name: "SsValidate",
  2767. props: {
  2768. errMsg: { type: String },
  2769. textAlign: { type: String, default: "left" },
  2770. style: { type: Object, default: () => ({}) },
  2771. },
  2772. template: `<div class="validate-vline"></div>
  2773. <div class="validate-tip" :style="style">
  2774. <div class="tip" :style="{ textAlign: textAlign }">{{ errMsg }}</div>
  2775. <div class="tip-more" :style="{ textAlign: textAlign }">{{ errMsg }}</div>
  2776. </div>`,
  2777. };
  2778. // ss-onoff-array 多选按钮 数组形式
  2779. const SsonoffArray = {
  2780. name: "SsonoffArray",
  2781. props: {
  2782. name: {
  2783. type: String,
  2784. required: true,
  2785. },
  2786. opt: {
  2787. type: Array,
  2788. default: () => [],
  2789. },
  2790. defaultValue: [String, Number, Array],
  2791. modelValue: [String, Number, Array],
  2792. multiple: {
  2793. // 新增多选模式属性
  2794. type: Boolean,
  2795. default: false,
  2796. },
  2797. // 是否允许一项都不选,默认true允许 by xu 20251212
  2798. null: {
  2799. type: Boolean,
  2800. default: true,
  2801. },
  2802. },
  2803. emits: ["update:modelValue"], // 允许更新 v-model 绑定的值
  2804. setup(props, { emit }) {
  2805. console.log("多选按钮", props.opt);
  2806. // 使用数组来存储选中值
  2807. const checkedValue = ref(
  2808. props.multiple
  2809. ? Array.isArray(props.defaultValue)
  2810. ? props.defaultValue
  2811. : []
  2812. : props.defaultValue
  2813. );
  2814. const errMsg = ref(props.errTip);
  2815. // 生成icon名字
  2816. const genIconName = (itemValue) => {
  2817. if (props.multiple) {
  2818. return checkedValue.value.includes(itemValue)
  2819. ? "form-icon-onoff-checked"
  2820. : "form-icon-onoff-unchecked";
  2821. }
  2822. return checkedValue.value === itemValue
  2823. ? "form-icon-onoff-checked"
  2824. : "form-icon-onoff-unchecked";
  2825. };
  2826. // 选中项
  2827. const selectItem = (value) => {
  2828. if (props.multiple) {
  2829. // 多选模式
  2830. const index = checkedValue.value.indexOf(value);
  2831. if (index === -1) {
  2832. checkedValue.value = [...checkedValue.value, value];
  2833. } else {
  2834. // 取消选中当前项
  2835. const newValue = checkedValue.value.filter((v) => v !== value);
  2836. // 如果不允许为空且取消后为空,则阻止取消操作 by xu 20251212
  2837. if (!props.null && newValue.length === 0) {
  2838. return; // 阻止取消最后一项
  2839. }
  2840. checkedValue.value = newValue;
  2841. }
  2842. } else {
  2843. // 单选模式
  2844. // 如果点击的是当前已选中的项,判断是否允许取消 by xu 20251212
  2845. if (checkedValue.value === value) {
  2846. if (!props.null) {
  2847. return; // 不允许为空时,阻止取消
  2848. }
  2849. checkedValue.value = ""; // 允许为空时,取消选中
  2850. } else {
  2851. checkedValue.value = value;
  2852. }
  2853. }
  2854. emit("update:modelValue", checkedValue.value);
  2855. nextTick(() => {
  2856. // 触发验证
  2857. if (window.ssVm) {
  2858. window.ssVm.validateField(props.name);
  2859. }
  2860. });
  2861. };
  2862. return { checkedValue, genIconName, selectItem };
  2863. },
  2864. // 使用渲染函数定义模板逻辑
  2865. render() {
  2866. const SsFormIcon = resolveComponent("ss-form-icon");
  2867. return h("div", { class: "radio-container" }, [
  2868. // 根据情况创建 input
  2869. this.multiple
  2870. ? this.checkedValue.length
  2871. ? // 多选且有选中值:为选中项创建 input
  2872. this.checkedValue.map((value) =>
  2873. h("input", {
  2874. type: "checkbox",
  2875. name: this.name,
  2876. value: value,
  2877. checked: true,
  2878. style: { display: "none" },
  2879. })
  2880. )
  2881. : // 多选但没有选中值:创建一个空值 input
  2882. h("input", {
  2883. type: "hidden",
  2884. name: this.name,
  2885. value: "",
  2886. })
  2887. : // 单选模式:创建一个 input
  2888. h("input", {
  2889. type: "hidden",
  2890. name: this.name,
  2891. value: this.checkedValue || "",
  2892. }),
  2893. this.opt.map((item, i) =>
  2894. h(
  2895. "div",
  2896. {
  2897. key: i,
  2898. class: {
  2899. checked: this.multiple
  2900. ? this.checkedValue.includes(item.value)
  2901. : this.checkedValue === item.value,
  2902. },
  2903. style: { width: item.width },
  2904. onClick: () => this.selectItem(item.value),
  2905. },
  2906. [
  2907. h("span", null, item.label),
  2908. h("div", { class: "mark" }, [
  2909. h(SsFormIcon, {
  2910. class: this.genIconName(item.value),
  2911. }),
  2912. ]),
  2913. ]
  2914. )
  2915. ),
  2916. ]);
  2917. },
  2918. };
  2919. // ss-onoff 一个按钮
  2920. const Ssonoff = {
  2921. name: "Ssonoff",
  2922. props: {
  2923. name: {
  2924. type: String,
  2925. required: true,
  2926. },
  2927. label: {
  2928. type: String,
  2929. required: true,
  2930. },
  2931. value: {
  2932. type: [String, Number],
  2933. required: true,
  2934. },
  2935. width: {
  2936. type: String,
  2937. default: "",
  2938. },
  2939. onchange: {
  2940. // 在此属性传入 onChange 的 window 全局回调函数,第一参数是当前整组值 by Ben/xu(20260320)
  2941. type: String,
  2942. required: false,
  2943. },
  2944. modelValue: [String, Number, Array],
  2945. multiple: {
  2946. type: Boolean,
  2947. default: false,
  2948. },
  2949. null: {
  2950. type: Boolean,
  2951. default: true,
  2952. },
  2953. },
  2954. emits: ["update:modelValue", "change"],
  2955. setup(props, { emit }) {
  2956. const parseModelValue = (val) => {
  2957. if (Array.isArray(val)) {
  2958. return val
  2959. .map((item) => (item == null ? "" : item.toString()))
  2960. .filter(Boolean);
  2961. }
  2962. if (val == null || val === "") return [];
  2963. // 如果以逗号开头,去掉开头的逗号
  2964. const cleanValue = val.toString().replace(/^,+/, "");
  2965. if (!cleanValue) return [];
  2966. if (cleanValue.includes("|")) {
  2967. return cleanValue.split("|").filter(Boolean);
  2968. }
  2969. if (cleanValue.includes(",")) {
  2970. return cleanValue.split(",").filter(Boolean);
  2971. }
  2972. return [cleanValue];
  2973. };
  2974. const callGlobalOnchg = (groupValue) => {
  2975. if (props.onchange && typeof props.onchange === "string") {
  2976. if (
  2977. typeof window !== "undefined" &&
  2978. window[props.onchange] &&
  2979. typeof window[props.onchange] === "function"
  2980. ) {
  2981. try {
  2982. window[props.onchange](groupValue, props.value, props.label);
  2983. } catch (error) {
  2984. console.error(`调用全局函数 ${props.onchange} 时出错:`, error);
  2985. }
  2986. } else {
  2987. console.warn(`全局函数 ${props.onchange} 未定义或不是一个函数。`);
  2988. }
  2989. }
  2990. };
  2991. // 判断当前按钮是否选中
  2992. const isChecked = computed(() => {
  2993. if (props.multiple) {
  2994. const currentValue = parseModelValue(props.modelValue);
  2995. return currentValue.includes(props.value.toString());
  2996. }
  2997. return props.modelValue + "" === props.value + ""; //强转为字符串类型再比较(改之前是数字类型和字符串类型作比较,永远为false) Ben 20251206
  2998. });
  2999. // 切换选中状态
  3000. const toggleSelect = () => {
  3001. let newModelValue;
  3002. if (props.multiple) {
  3003. const currentValue = parseModelValue(props.modelValue);
  3004. const currentButtonValue = props.value.toString();
  3005. const index = currentValue.indexOf(currentButtonValue);
  3006. let newValue;
  3007. if (index === -1) {
  3008. // 选中当前项
  3009. newValue = [...currentValue, currentButtonValue];
  3010. } else {
  3011. // 取消选中当前项
  3012. const filteredValue = currentValue.filter(
  3013. (value) => value !== currentButtonValue
  3014. );
  3015. // 如果不允许为空且取消后为空,则阻止取消操作
  3016. if (!props.null && filteredValue.length === 0) {
  3017. return; // 阻止取消最后一项
  3018. }
  3019. newValue = filteredValue;
  3020. }
  3021. newModelValue = newValue.join(",");
  3022. } else {
  3023. // 单选模式
  3024. const currentValue = parseModelValue(props.modelValue);
  3025. const isCurrentlySelected = currentValue.includes(
  3026. props.value.toString()
  3027. );
  3028. if (!isCurrentlySelected) {
  3029. // 选中当前项
  3030. newModelValue = props.value;
  3031. } else {
  3032. // 取消选中当前项
  3033. // 如果不允许为空且当前只有这一项被选中,则阻止取消操作
  3034. if (!props.null && currentValue.length === 1) {
  3035. return; // 阻止取消唯一选中项
  3036. }
  3037. newModelValue = "";
  3038. }
  3039. }
  3040. emit("update:modelValue", newModelValue);
  3041. emit("change", newModelValue, props.value, props.label);
  3042. callGlobalOnchg(newModelValue);
  3043. nextTick(() => {
  3044. // 触发验证
  3045. if (window.ssVm) {
  3046. window.ssVm.validateField(props.name);
  3047. }
  3048. });
  3049. };
  3050. return { isChecked, toggleSelect };
  3051. },
  3052. render() {
  3053. const SsFormIcon = resolveComponent("ss-form-icon");
  3054. return h("div", { class: "radio-container2" }, [
  3055. // 隐藏的表单元素
  3056. this.multiple
  3057. ? h("input", {
  3058. type: "hidden",
  3059. name: `${this.name}`, // 多选模式下使用数组形式的 name
  3060. value: this.isChecked ? this.value : "",
  3061. })
  3062. : this.isChecked &&
  3063. h("input", {
  3064. // 只有当前按钮被选中时才创建 input
  3065. type: "hidden",
  3066. name: this.name,
  3067. value: this.value,
  3068. }),
  3069. // 按钮显示
  3070. h(
  3071. "div",
  3072. {
  3073. class: { checked: this.isChecked },
  3074. style: { width: this.width },
  3075. onClick: this.toggleSelect,
  3076. },
  3077. [
  3078. h("span", null, this.label),
  3079. h("div", { class: "mark" }, [
  3080. h(SsFormIcon, {
  3081. class: this.isChecked
  3082. ? "form-icon-onoff-checked"
  3083. : "form-icon-onoff-unchecked",
  3084. }),
  3085. ]),
  3086. ]
  3087. ),
  3088. ]);
  3089. },
  3090. };
  3091. // ss-textarea
  3092. const SsTextarea = {
  3093. name: "SsTextarea",
  3094. props: {
  3095. name: {
  3096. type: String,
  3097. required: true,
  3098. },
  3099. placeholder: {
  3100. type: String,
  3101. default: "请输入",
  3102. },
  3103. defaultValue: [String, Number],
  3104. modelValue: [String, Number],
  3105. },
  3106. emits: ["update:modelValue"],
  3107. setup(props, { emit }) {
  3108. const inputValue = ref(props.modelValue || props.defaultValue || "");
  3109. // 监听 modelValue 变化
  3110. watch(
  3111. () => props.modelValue,
  3112. (newVal) => {
  3113. inputValue.value = newVal;
  3114. }
  3115. );
  3116. // 输入事件处理
  3117. const onInput = (event) => {
  3118. const newValue = event.target.value;
  3119. inputValue.value = newValue;
  3120. emit("update:modelValue", newValue);
  3121. // 触发验证
  3122. if (window.ssVm) {
  3123. window.ssVm.validateField(props.name);
  3124. }
  3125. };
  3126. // 失焦时验证
  3127. const onBlur = () => {
  3128. if (window.ssVm) {
  3129. window.ssVm.validateField(props.name);
  3130. }
  3131. };
  3132. return { inputValue, onInput, onBlur };
  3133. },
  3134. render() {
  3135. return h("div", { class: "textarea-container" }, [
  3136. h("div", { class: "textarea" }, [
  3137. // 添加隐藏的 input 用于验证
  3138. h("input", {
  3139. type: "hidden",
  3140. name: this.name,
  3141. value: this.inputValue || "",
  3142. }),
  3143. h("textarea", {
  3144. placeholder: this.placeholder,
  3145. value: this.inputValue,
  3146. onInput: this.onInput,
  3147. onBlur: this.onBlur,
  3148. }),
  3149. ]),
  3150. ]);
  3151. },
  3152. };
  3153. // ss-editor 富文本编辑器 基于Jodit
  3154. const SsEditor = {
  3155. name: "SsEditor",
  3156. props: {
  3157. modelValue: {
  3158. type: String,
  3159. default: "",
  3160. },
  3161. name: {
  3162. type: String,
  3163. default: "",
  3164. },
  3165. url: {
  3166. type: String,
  3167. default: "",
  3168. },
  3169. height: {
  3170. type: [Number, String],
  3171. default: 400,
  3172. },
  3173. placeholder: {
  3174. type: String,
  3175. default: "请输入内容",
  3176. },
  3177. readonly: {
  3178. type: Boolean,
  3179. default: false,
  3180. },
  3181. uploadUrl: {
  3182. type: String,
  3183. default: "/ulByHttp", //原值为“upload” Ben(20251205)
  3184. },
  3185. param: {
  3186. type: Object,
  3187. default: () => ({}),
  3188. },
  3189. },
  3190. emits: ["update:modelValue", "ready", "change"],
  3191. setup(props, { emit }) {
  3192. const SsEditorIcon = resolveComponent("SsEditorIcon");
  3193. const editorRef = ref(null);
  3194. let editorContent = ""; //保存富文本编辑器里面的富文本内容
  3195. const uniqueId = "editor-" + Date.now();
  3196. const errMsg = Vue.ref("");
  3197. let fjid = ref(props.param.button.val);
  3198. let fjName = props.param.button.desc;
  3199. let mode = props.param.mode;
  3200. const validate = () => {
  3201. if (window.ssVm) {
  3202. const result = window.ssVm.validateField(props.name);
  3203. console.log("validate", window.ssVm.validateField(props.name));
  3204. errMsg.value = result.valid ? "" : result.message;
  3205. }
  3206. };
  3207. onMounted(() => {
  3208. validate();
  3209. const editor = Jodit.make(`#${uniqueId}`, {
  3210. height: props.height,
  3211. placeholder: props.placeholder,
  3212. readonly: props.readonly,
  3213. language: "zh_cn",
  3214. i18n: {
  3215. zh_cn: {
  3216. Link: "链接",
  3217. URL: "链接",
  3218. "No follow": "无跟踪",
  3219. "Class name": "类名",
  3220. Image: "图片",
  3221. File: "文件",
  3222. "Line height": "行高",
  3223. Alternative: "描述",
  3224. "Alternative text": "描述",
  3225. "Lower Alpha": "小写字母",
  3226. "Upper Alpha": "大写字母",
  3227. "Upper Roman": "大写罗马数字",
  3228. "Lower Roman": "小写罗马数字",
  3229. "Lower Greek": "小写希腊字母",
  3230. "Lower Letter": "小写字母",
  3231. "Upper Letter": "大写字母",
  3232. },
  3233. },
  3234. showXPathInStatusbar: false,
  3235. showCharsCounter: false,
  3236. showWordsCounter: false,
  3237. allowResizeY: false,
  3238. toolbarSticky: false,
  3239. statusbar: false,
  3240. uploader: {
  3241. url: props.uploadUrl,
  3242. format: "json",
  3243. method: "POST",
  3244. filesVariableName: function (i) {
  3245. return "imgs[" + i + "]";
  3246. },
  3247. headers: {
  3248. Accept: "application/json",
  3249. },
  3250. prepareData: function (formData) {
  3251. // 这里可以在发送前处理表单数据
  3252. return formData;
  3253. },
  3254. isSuccess: function (resp) {
  3255. console.log("isSuccess resp:", resp);
  3256. return resp.code === 0;
  3257. },
  3258. getMessage: function (resp) {
  3259. console.log("getMessage resp:", resp);
  3260. return resp.msg || "上传失败";
  3261. },
  3262. process: function (resp) {
  3263. console.log("process resp:", resp);
  3264. return resp.data.url;
  3265. },
  3266. error: function (e) {
  3267. console.error("上传失败:", e.message);
  3268. },
  3269. defaultHandlerSuccess: function (resp) {
  3270. console.log("上传成功:", resp);
  3271. },
  3272. defaultHandlerError: function (err) {
  3273. console.error("上传错误:", err);
  3274. },
  3275. contentType: function (requestData) {
  3276. // 可以根据需要修改 Content-Type
  3277. return false; // 让浏览器自动设置
  3278. },
  3279. },
  3280. // 自定义字体列表
  3281. controls: {
  3282. font: {
  3283. list: {
  3284. Arial: "Arial",
  3285. SimSun: "宋体",
  3286. SimHei: "黑体",
  3287. "Microsoft YaHei": "微软雅黑",
  3288. KaiTi: "楷体",
  3289. FangSong: "仿宋",
  3290. "Times New Roman": "Times New Roman",
  3291. "Courier New": "Courier New",
  3292. },
  3293. },
  3294. customLinkButton: {
  3295. name: "link",
  3296. tooltip: "附件",
  3297. exec: function (editor) {
  3298. // 按钮点击时的处理函数
  3299. console.log("附件点击了");
  3300. console.log("param", props.param);
  3301. console.log("cmsAddUrl", props.param.button.cmsAddUrl);
  3302. if (fjid.value == null || fjid.value == "") {
  3303. $.ajax({
  3304. type: "post",
  3305. url: props.param.button.cmsAddUrl,
  3306. async: false,
  3307. data: {
  3308. name: "fjid",
  3309. ssNrObjName: "sh",
  3310. ssNrObjId: "",
  3311. },
  3312. success: function (_fjid) {
  3313. console.log("cmsAddUrl success", _fjid);
  3314. fjid.value = _fjid;
  3315. },
  3316. });
  3317. }
  3318. var str =
  3319. "&nrid=T-" +
  3320. fjid.value +
  3321. "&objectId=" +
  3322. fjid.value +
  3323. "&objectName=" +
  3324. fjName +
  3325. "&callback=" +
  3326. (window["fjidCallbackName"] || "");
  3327. console.log("str", str);
  3328. SS.openDialog({
  3329. src: props.param.button.cmsUpdUrl + str,
  3330. headerTitle: "编辑",
  3331. width: 900,
  3332. high: 664,
  3333. zIndex: 51,
  3334. });
  3335. // ss.display.showComponent({
  3336. // show: ["wdDialog"],
  3337. // url: props.param.button.cmsUpdUrl + str,
  3338. // title: "编辑",
  3339. // width: 900,
  3340. // high: 664,
  3341. // });
  3342. },
  3343. },
  3344. },
  3345. toolbarAdaptive: true,
  3346. buttons: [
  3347. "fullsize",
  3348. "bold",
  3349. "italic",
  3350. "underline",
  3351. "strikethrough",
  3352. "eraser",
  3353. "|",
  3354. "font",
  3355. "fontsize",
  3356. "brush",
  3357. "paragraph",
  3358. "|",
  3359. "left",
  3360. "center",
  3361. "right",
  3362. "justify",
  3363. "|",
  3364. "ul",
  3365. "ol",
  3366. "indent",
  3367. "outdent",
  3368. "|",
  3369. "image",
  3370. "table",
  3371. "customLinkButton",
  3372. "print",
  3373. "|",
  3374. "undo",
  3375. "redo",
  3376. "find",
  3377. ],
  3378. // 中等宽度时显示的按钮
  3379. buttonsMD: [
  3380. "fullsize",
  3381. "bold",
  3382. "italic",
  3383. "underline",
  3384. "strikethrough",
  3385. "eraser",
  3386. "|",
  3387. "font",
  3388. "fontsize",
  3389. "brush",
  3390. "paragraph",
  3391. "|",
  3392. "font",
  3393. "fontsize",
  3394. "|",
  3395. "left",
  3396. "center",
  3397. "right",
  3398. "justify",
  3399. "|",
  3400. "image",
  3401. "customLinkButton",
  3402. "|",
  3403. "dots", // 其余按钮会自动进入 dots 菜单
  3404. ],
  3405. // 小屏幕时显示的按钮
  3406. buttonsSM: ["fullsize", "bold", "italic", "|", "image", "|", "dots"],
  3407. // 超小屏幕时显示的按钮
  3408. buttonsXS: ["fullsize", "bold", "|", "dots"],
  3409. // 设置响应式断点
  3410. sizeLG: 1024, // 大屏幕
  3411. sizeMD: 768, // 中等屏幕
  3412. sizeSM: 576, // 小屏幕
  3413. // 自定义图标
  3414. getIcon: function (name, clearName) {
  3415. // 定义图标映射
  3416. const iconMap = {
  3417. bold: "editor-icon-bold",
  3418. italic: "editor-icon-italic",
  3419. underline: "editor-icon-underline",
  3420. strikethrough: "editor-icon-strikethrough",
  3421. eraser: "editor-icon-eraser",
  3422. copyformat: "editor-icon-copyformat",
  3423. font: "editor-icon-font",
  3424. fontsize: "editor-icon-fontsize",
  3425. brush: "editor-icon-brush",
  3426. paragraph: "editor-icon-paragraph",
  3427. left: "editor-icon-align-left",
  3428. center: "editor-icon-align-center",
  3429. right: "editor-icon-align-right",
  3430. justify: "editor-icon-align-justify",
  3431. ul: "editor-icon-ul",
  3432. ol: "editor-icon-ol",
  3433. indent: "editor-icon-indent",
  3434. outdent: "editor-icon-outdent",
  3435. image: "editor-icon-image",
  3436. file: "editor-icon-file",
  3437. video: "editor-icon-video",
  3438. table: "editor-icon-table",
  3439. link: "editor-icon-link",
  3440. source: "editor-icon-source",
  3441. eye: "editor-icon-preview",
  3442. fullsize: "editor-icon-fullsize",
  3443. shrink: "editor-icon-fullsize-exit", // 添加退出全屏图标
  3444. print: "editor-icon-print",
  3445. undo: "editor-icon-undo",
  3446. redo: "editor-icon-redo",
  3447. search: "editor-icon-find",
  3448. selectall: "editor-icon-selectall",
  3449. };
  3450. // 获取对应的图标类名
  3451. const iconClass = iconMap[clearName] || iconMap[name];
  3452. if (iconClass) {
  3453. // 返回带有我们自定义图标类的 span 元素
  3454. return `<span class="editor-icon ${iconClass}"></span>`;
  3455. }
  3456. return null;
  3457. },
  3458. });
  3459. // 设置初始值
  3460. editor.value = editorContent;
  3461. // editor.value = props.modelValue;
  3462. // 监听变化
  3463. editor.events.on("change", () => {
  3464. // emit("update:modelValue", editor.value);
  3465. editorContent = editor.value;
  3466. // alert('editorContent:'+editorContent);
  3467. let contentElements = document.getElementsByName(
  3468. props.name.replace(/wj$/, "") + "Edit"
  3469. );
  3470. if (contentElements.length > 0) {
  3471. contentElements[0].value = editorContent;
  3472. }
  3473. emit("change", editor.value);
  3474. setTimeout(() => {
  3475. validate();
  3476. }, 50);
  3477. });
  3478. // 保存编辑器实例
  3479. editorRef.value = editor;
  3480. emit("ready", editor);
  3481. //回显编辑器富文本文件
  3482. if (props.url) {
  3483. const params = new URLSearchParams();
  3484. if (mode) params.append("mode", mode);
  3485. if (props.modelValue) params.append("path", props.modelValue);
  3486. // alert('props.url:'+props.url+',props.modelValue:'+props.modelValue);
  3487. axios
  3488. .post(props.url, params, {
  3489. headers: {
  3490. "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
  3491. },
  3492. })
  3493. .then((response) => {
  3494. // alert(JSON.stringify(response.data));
  3495. if ("timeout" == response.data.statusText) {
  3496. alert("网络超时!");
  3497. return;
  3498. }
  3499. let content = response.data.content;
  3500. if (content) {
  3501. // editor.value = content;
  3502. editorRef.value.value = content;
  3503. editorContent = content;
  3504. // alert('editor.value:'+editor.value)
  3505. }
  3506. let filePath = response.data.path;
  3507. // alert('response.data:'+JSON.stringify(response.data));
  3508. if (filePath) {
  3509. props.modelValue = filePath;
  3510. emit("update:modelValue", filePath);
  3511. }
  3512. });
  3513. }
  3514. });
  3515. // 监听值变化
  3516. watch(
  3517. // () => props.modelValue,
  3518. () => editorContent,
  3519. (newValue) => {
  3520. if (editorRef.value && newValue !== editorRef.value.value) {
  3521. editorRef.value.value = newValue || "";
  3522. }
  3523. }
  3524. );
  3525. // 监听只读状态变化
  3526. watch(
  3527. () => props.readonly,
  3528. (newValue) => {
  3529. if (editorRef.value) {
  3530. editorRef.value.setReadOnly(newValue);
  3531. }
  3532. }
  3533. );
  3534. // 组件销毁时清理
  3535. onBeforeUnmount(() => {
  3536. if (editorRef.value) {
  3537. editorRef.value.destruct();
  3538. }
  3539. });
  3540. return () =>
  3541. h("div", { class: "ss-editor-container" }, [
  3542. fjid.value &&
  3543. h("input", {
  3544. type: "hidden",
  3545. name: "fjid",
  3546. value: fjid.value,
  3547. }),
  3548. h("input", {
  3549. type: "hidden",
  3550. name: props.name.replace(/wj$/, "") + "Edit",
  3551. value: editorContent,
  3552. // value: props.modelValue,
  3553. }),
  3554. h("input", {
  3555. type: "hidden",
  3556. name: props.name.replace(/wj$/, "") + "wj",
  3557. value: props.modelValue,
  3558. // value: props.url
  3559. }),
  3560. h("input", { type: "hidden", name: "ueditorpath", value: "mswj" }),
  3561. h("textarea", { id: uniqueId }),
  3562. ]);
  3563. },
  3564. };
  3565. // 弹窗右边图标
  3566. const SsFullStyleHeader = {
  3567. name: "SsFullStyleHeader",
  3568. props: {
  3569. title: {
  3570. type: String,
  3571. default: "标题",
  3572. },
  3573. },
  3574. emits: ["close"],
  3575. setup(props, { emit }) {
  3576. // console.log(props.title)
  3577. const onClose = () => {
  3578. emit("close");
  3579. };
  3580. const SsIcon = resolveComponent("ss-icon");
  3581. return () =>
  3582. h("div", { class: "header-container" }, [
  3583. h("div", { class: "title" }, props.title),
  3584. h("div", { class: "handle-bar" }, [
  3585. h("div", { class: "left-bar" }, [
  3586. h(SsDialogIcon, { class: "dialog-icon-download" }),
  3587. h(SsDialogIcon, { class: "dialog-icon-print" }),
  3588. h(SsDialogIcon, { class: "dialog-icon-setting" }),
  3589. h(SsDialogIcon, { class: "dialog-icon-collect" }),
  3590. h(SsDialogIcon, { class: "dialog-icon-help" }),
  3591. h(SsDialogIcon, { class: "dialog-icon-full-screen" }),
  3592. h(SsDialogIcon, { class: "dialog-icon-lock" }),
  3593. ]),
  3594. h("div", { class: "close-bar", onClick: onClose }, [
  3595. h(SsDialogIcon, { class: "dialog-icon-close" }),
  3596. ]),
  3597. ]),
  3598. ]);
  3599. },
  3600. };
  3601. // ss-dialog弹窗
  3602. const SsDialog = {
  3603. name: "SsDialog",
  3604. props: {
  3605. src: {
  3606. type: String,
  3607. },
  3608. headerTitle: {
  3609. type: String,
  3610. // required: true,
  3611. default: "弹窗",
  3612. },
  3613. width: {
  3614. type: String,
  3615. default: "1400",
  3616. },
  3617. height: {
  3618. type: String,
  3619. default: "600",
  3620. },
  3621. params: {
  3622. type: Object,
  3623. default: () => ({}),
  3624. },
  3625. zIndex: {
  3626. type: Number,
  3627. default: 1000,
  3628. },
  3629. },
  3630. emits: ["close"],
  3631. setup(props, { slots, emit }) {
  3632. // 关闭窗口方法
  3633. const onClose = () => {
  3634. emit("close");
  3635. };
  3636. const showHeader = ref(true);
  3637. const headerVisible = ref(false);
  3638. const popupHieght = ref(props.height);
  3639. // 状态:存储位置信息
  3640. const position = reactive({
  3641. // 页面居中
  3642. x: (window.innerWidth - props.width) / 2,
  3643. y: (window.innerHeight - popupHieght.value) / 2,
  3644. isDragging: false,
  3645. offsetX: 0,
  3646. offsetY: 0,
  3647. });
  3648. // 鼠标按下时设置起始坐标并开始拖拽
  3649. const startDrag = (event) => {
  3650. position.isDragging = true;
  3651. position.offsetX = event.clientX - position.x;
  3652. position.offsetY = event.clientY - position.y;
  3653. };
  3654. // 鼠标移动时更新位置
  3655. const onDrag = (event) => {
  3656. if (position.isDragging) {
  3657. position.x = event.clientX - position.offsetX;
  3658. position.y = event.clientY - position.offsetY;
  3659. }
  3660. };
  3661. // 鼠标放开时结束拖拽
  3662. const endDrag = () => {
  3663. position.isDragging = false;
  3664. };
  3665. // 监听来自 iframe 的消息
  3666. const handleMessage = (event) => {
  3667. // 顶天立地
  3668. if (event.data && typeof event.data.hasScrollBar !== "undefined") {
  3669. if (event.data.hasScrollBar) {
  3670. // console.log(event);
  3671. position.y = 10;
  3672. showHeader.value = false;
  3673. headerVisible.value = true;
  3674. popupHieght.value = window.innerHeight - 20;
  3675. // console.log(popupHieght.value);
  3676. document.querySelector(".body").style.height = "100%";
  3677. document.querySelector(".body").style.paddingTop = "30px";
  3678. document.querySelector(".header-container ").style.position =
  3679. "absolute";
  3680. document.querySelector(".header-container ").style.zIndex = "10";
  3681. }
  3682. }
  3683. };
  3684. // 鼠标移入关闭按钮区域时显示头部
  3685. const onMouseEnterCloseButton = () => {
  3686. headerVisible.value = false;
  3687. };
  3688. // 鼠标移出关闭按钮区域时隐藏头部
  3689. const onMouseLeaveCloseButton = () => {
  3690. headerVisible.value = true;
  3691. };
  3692. // 在组件挂载时添加全局事件监听器
  3693. Vue.onMounted(() => {
  3694. // 如果传过来的高度大于窗口高度,则设置为窗口高度减去20 否则保持传过来的高度
  3695. popupHieght.value =
  3696. popupHieght.value > window.innerHeight
  3697. ? window.innerHeight - 20
  3698. : popupHieght.value;
  3699. const container = document.querySelector(".header-container");
  3700. if (container) {
  3701. container.addEventListener("mousedown", startDrag);
  3702. }
  3703. document.addEventListener("mousemove", onDrag);
  3704. document.addEventListener("mouseup", endDrag);
  3705. window.addEventListener("message", handleMessage);
  3706. });
  3707. // 在组件卸载时移除全局事件监听器
  3708. Vue.onUnmounted(() => {
  3709. document.removeEventListener("mousemove", onDrag);
  3710. document.removeEventListener("mouseup", endDrag);
  3711. window.removeEventListener("message", handleMessage);
  3712. });
  3713. const SsMark = resolveComponent("ss-mark");
  3714. const SsFullStyleHeader = resolveComponent("ss-full-style-header");
  3715. // render函数定义组件结构
  3716. return () =>
  3717. h(
  3718. Teleport,
  3719. { to: "body" }, // 使用 Teleport 将弹窗内容挂载到 body
  3720. h(SsMark, {}, [
  3721. h(
  3722. "div",
  3723. {
  3724. class: "popup-container",
  3725. style: {
  3726. position: "absolute",
  3727. left: `${position.x}px`,
  3728. top: `${position.y}px`,
  3729. width: props.width + "px",
  3730. height: popupHieght.value + "px",
  3731. zIndex: props.zIndex, // 确保弹窗在最上层
  3732. },
  3733. },
  3734. [
  3735. h(SsFullStyleHeader, {
  3736. class: "header",
  3737. title: props.headerTitle,
  3738. onClose: onClose,
  3739. onMousedown: startDrag, // 绑定拖动事件
  3740. onMouseUp: endDrag,
  3741. ...(!showHeader.value && {
  3742. onMouseenter: onMouseEnterCloseButton,
  3743. onMouseleave: onMouseLeaveCloseButton,
  3744. }),
  3745. style: {
  3746. cursor: position.isDragging ? "grabbing" : "grab",
  3747. visibility: headerVisible.value ? "hidden" : "visible",
  3748. },
  3749. }),
  3750. h(
  3751. "div",
  3752. {
  3753. class: "body",
  3754. style: {},
  3755. },
  3756. [
  3757. h("iframe", {
  3758. src: props.src,
  3759. frameborder: 0,
  3760. style: { width: "100%", height: "100%" },
  3761. }),
  3762. ]
  3763. ),
  3764. headerVisible.value &&
  3765. h("div", {
  3766. class: "close-button",
  3767. onMouseenter: onMouseEnterCloseButton,
  3768. onMouseleave: onMouseLeaveCloseButton,
  3769. style: {
  3770. position: "absolute",
  3771. top: "0",
  3772. right: "0",
  3773. // background: 'black',
  3774. width: "60px",
  3775. height: "60px",
  3776. cursor: "pointer",
  3777. },
  3778. }),
  3779. ]
  3780. ),
  3781. ])
  3782. );
  3783. },
  3784. };
  3785. // ss-mark遮罩层
  3786. const SsMark = {
  3787. name: "SsMark",
  3788. setup(props, { slots, emit }) {
  3789. return () =>
  3790. h("div", { class: "dialog-container" }, [
  3791. h("div", { class: "mark-content" }, [
  3792. h("div", { class: "dialog-contianer" }, [
  3793. slots.default ? slots.default() : "",
  3794. ]),
  3795. ]),
  3796. ]);
  3797. },
  3798. };
  3799. // ss-bottom-button 底部按钮
  3800. // 修改支持更多按钮 by xu 20251211
  3801. const SsBottomButton = {
  3802. name: "SsBottomButton",
  3803. props: {
  3804. text: {
  3805. type: String,
  3806. required: false,
  3807. },
  3808. type: {
  3809. type: String,
  3810. default: "button",
  3811. },
  3812. iconClass: {
  3813. type: String,
  3814. },
  3815. class: {
  3816. type: String,
  3817. default: "",
  3818. },
  3819. onclick: {
  3820. type: [Function, String],
  3821. default: null,
  3822. },
  3823. // 修改支持更多按钮 by xu 20251211
  3824. more: {
  3825. type: [Boolean, String],
  3826. default: false,
  3827. },
  3828. },
  3829. setup(props, { emit }) {
  3830. const SsBottomDivIcon = Vue.resolveComponent("ss-bottom-div-icon");
  3831. const showDropdown = Vue.ref(false);
  3832. // 修改支持更多按钮 by xu 20251211
  3833. const moreKey = Vue.computed(() => {
  3834. const val = props.more;
  3835. if (val === false || val === null || typeof val === "undefined") {
  3836. return null;
  3837. }
  3838. if (val === true || val === "" || val === "true") {
  3839. return "moreChg";
  3840. }
  3841. return val;
  3842. });
  3843. // 从配置中读取按钮信息和下拉选项
  3844. const config = Vue.computed(() => {
  3845. if (
  3846. moreKey.value &&
  3847. window.ss &&
  3848. window.ss.dom &&
  3849. window.ss.dom.btnElemConfig
  3850. ) {
  3851. return window.ss.dom.btnElemConfig[moreKey.value] || {};
  3852. }
  3853. return {};
  3854. });
  3855. const buttonText = Vue.computed(() => {
  3856. return props.text || config.value.desc || "";
  3857. });
  3858. const dropOptions = Vue.computed(() => {
  3859. return config.value.dropOptions || [];
  3860. });
  3861. const hasDropdown = Vue.computed(() => {
  3862. return dropOptions.value.length > 0;
  3863. });
  3864. const handleMouseEnter = () => {
  3865. if (hasDropdown.value) {
  3866. showDropdown.value = true;
  3867. }
  3868. };
  3869. const handleMouseLeave = () => {
  3870. showDropdown.value = false;
  3871. };
  3872. const handleDropItemClick = (option) => {
  3873. if (option.callback && typeof option.callback === "function") {
  3874. option.callback();
  3875. }
  3876. showDropdown.value = false;
  3877. };
  3878. return () =>
  3879. h(
  3880. "div",
  3881. {
  3882. class: "ss-bottom-button-wrapper",
  3883. onMouseenter: handleMouseEnter,
  3884. onMouseleave: handleMouseLeave,
  3885. },
  3886. [
  3887. h(
  3888. "button",
  3889. {
  3890. class: props.class,
  3891. onClick: (e) => {
  3892. e.stopPropagation();
  3893. emit("click", e);
  3894. if (props.onclick) {
  3895. // 如果是函数直接调用
  3896. if (typeof props.onclick === "function") {
  3897. props.onclick(e);
  3898. } else if (typeof props.onclick === "string") {
  3899. // 如果是字符串,使用直接的方法执行
  3900. // 临时存储按钮元素到全局变量
  3901. window.__ss_current_button = e.currentTarget;
  3902. // 直接执行代码,使用eval以保留原始上下文
  3903. try {
  3904. eval(props.onclick);
  3905. } finally {
  3906. // 清理全局变量
  3907. delete window.__ss_current_button;
  3908. }
  3909. }
  3910. }
  3911. },
  3912. type: props.type,
  3913. },
  3914. [
  3915. h("span", null, [
  3916. h(SsBottomDivIcon, {
  3917. class: props.iconClass,
  3918. }),
  3919. ]),
  3920. h("span", null, buttonText.value),
  3921. ]
  3922. ),
  3923. // 渲染下拉菜单
  3924. hasDropdown.value && showDropdown.value
  3925. ? h(
  3926. "div",
  3927. {
  3928. class: "ss-bottom-button-dropdown",
  3929. },
  3930. dropOptions.value.map((option) =>
  3931. h(
  3932. "div",
  3933. {
  3934. class: "ss-bottom-button-dropdown-item",
  3935. onClick: (e) => {
  3936. e.stopPropagation();
  3937. handleDropItemClick(option);
  3938. },
  3939. },
  3940. option.desc
  3941. )
  3942. )
  3943. )
  3944. : null,
  3945. ]
  3946. );
  3947. },
  3948. };
  3949. // ss-search搜索框
  3950. const SsSearch = {
  3951. name: "SsSearch",
  3952. props: {
  3953. theme: {
  3954. type: String,
  3955. default: "light",
  3956. validator: function (value) {
  3957. return ["dark", "light"].includes(value);
  3958. },
  3959. },
  3960. placeholder: {
  3961. type: String,
  3962. default: "请输入搜索条件",
  3963. },
  3964. },
  3965. setup(props, { emit }) {
  3966. const onClick = () => {
  3967. console.log("Search clicked");
  3968. emit("click");
  3969. };
  3970. const SsIcon = Vue.resolveComponent("ss-icon");
  3971. return () =>
  3972. Vue.h(
  3973. "div",
  3974. {
  3975. class: ["search-container", props.theme],
  3976. onClick: onClick,
  3977. },
  3978. [
  3979. Vue.h("input", {
  3980. placeholder: props.placeholder,
  3981. disabled: true,
  3982. }),
  3983. Vue.h(SsIcon, {
  3984. name: "search-result",
  3985. size: "20px",
  3986. }),
  3987. ]
  3988. );
  3989. },
  3990. };
  3991. // ss-cart-item 菜单页面的卡片 左右结构
  3992. const SsCartItem = {
  3993. name: "SsCartItem",
  3994. props: {
  3995. active: Boolean,
  3996. item: {
  3997. type: Object,
  3998. default: () => ({
  3999. thumb: "images/example/project-img.png",
  4000. title: "广州(国际)科技成果转化天河基地专",
  4001. description: "佳能中国广州分公司",
  4002. all: 50,
  4003. finish: 5,
  4004. }),
  4005. },
  4006. },
  4007. setup(props, { emit }) {
  4008. const item = props.item;
  4009. const itemWidth = Vue.computed(() => {
  4010. const containerWidth =
  4011. document.body.clientWidth || document.body.scrollWidth - 520;
  4012. const halfWidth = containerWidth / 2;
  4013. if (halfWidth < 480) {
  4014. return Math.min(containerWidth, 702) + "px";
  4015. } else {
  4016. return Math.min(halfWidth, 702) + "px";
  4017. }
  4018. });
  4019. const onItemClick = (e) => {
  4020. emit("click", e);
  4021. };
  4022. return {
  4023. item,
  4024. itemWidth,
  4025. onItemClick,
  4026. };
  4027. },
  4028. render() {
  4029. const SsIcon = Vue.resolveComponent("ss-icon");
  4030. return Vue.h(
  4031. "div",
  4032. {
  4033. class: { "item-container": true, active: this.active },
  4034. onClick: this.onItemClick,
  4035. style: { width: this.itemWidth },
  4036. },
  4037. [
  4038. Vue.h("div", { class: "header" }, [
  4039. Vue.h(SsIcon, { name: "setting", size: "20px" }),
  4040. ]),
  4041. Vue.h("div", { class: "body" }, [
  4042. Vue.h("div", { class: "left" }, [
  4043. Vue.h("img", {
  4044. src: this.item.thumb,
  4045. alt: "Thumbnail",
  4046. class: "imgUnHandle",
  4047. style: { "object-fit": "cover", width: "100%", height: "100%" },
  4048. }),
  4049. ]),
  4050. Vue.h("div", { class: "right" }, [
  4051. Vue.h("div", { class: "title" }, this.item.title),
  4052. Vue.h("div", { class: "desc" }, this.item.description),
  4053. Vue.h("div", { class: "progress" }, [
  4054. Vue.h(
  4055. "div",
  4056. {
  4057. style: {
  4058. width: `${(this.item.finish / this.item.all) * 100}%`,
  4059. },
  4060. },
  4061. [Vue.h("div", `${this.item.finish}/${this.item.all}`)]
  4062. ),
  4063. ]),
  4064. ]),
  4065. ]),
  4066. ]
  4067. );
  4068. },
  4069. };
  4070. // ss-cart-item2 菜单页面的卡片 上下结构
  4071. const SsCartItem2 = {
  4072. name: "SsCartItem2",
  4073. props: {
  4074. active: Boolean,
  4075. item: {
  4076. type: Object,
  4077. default: () => ({
  4078. thumb: "images/example/project-img.png",
  4079. title: "广州(国际)科技成果转化天河基地专",
  4080. description: "佳能中国广州分公司",
  4081. all: 50,
  4082. finish: 5,
  4083. }),
  4084. },
  4085. },
  4086. setup(props, { emit }) {
  4087. const item = props.item;
  4088. const itemWidth = Vue.computed(() => {
  4089. const containerWidth =
  4090. document.body.clientWidth || document.body.scrollWidth - 520;
  4091. const halfWidth = containerWidth / 2;
  4092. if (halfWidth < 480) {
  4093. return Math.min(containerWidth, 702) + "px";
  4094. } else {
  4095. return Math.min(halfWidth, 702) + "px";
  4096. }
  4097. });
  4098. const onItemClick = (e) => {
  4099. emit("click", e);
  4100. };
  4101. return {
  4102. item,
  4103. itemWidth,
  4104. onItemClick,
  4105. };
  4106. },
  4107. render() {
  4108. const SsIcon = Vue.resolveComponent("ss-icon");
  4109. return Vue.h(
  4110. "div",
  4111. {
  4112. class: { "item-container2": true, active: this.active },
  4113. onClick: this.onItemClick,
  4114. style: { width: this.itemWidth },
  4115. },
  4116. [
  4117. Vue.h("div", { class: "action-bar" }, [
  4118. Vue.h(SsIcon, { name: "setting", size: "20px" }),
  4119. ]),
  4120. Vue.h("div", { class: "header" }, [
  4121. Vue.h("div", { class: "title" }, `${this.item.title}`),
  4122. ]),
  4123. Vue.h("div", { class: "body" }, [
  4124. Vue.h("div", { class: "left" }, [
  4125. Vue.h("img", {
  4126. src: this.item.thumb,
  4127. alt: "Thumbnail",
  4128. class: "imgUnHandle",
  4129. style: { "object-fit": "cover", width: "100%", height: "100%" },
  4130. }),
  4131. ]),
  4132. Vue.h("div", { class: "right" }, [
  4133. Vue.h("div", { class: "content" }, this.item.description),
  4134. Vue.h("div", { class: "tip" }, [
  4135. Vue.h("div", { class: "progress" }, [
  4136. Vue.h(
  4137. "div",
  4138. {
  4139. style: {
  4140. width: `${(this.item.finish / this.item.all) * 100}%`,
  4141. },
  4142. },
  4143. [Vue.h("div", `${this.item.finish}/${this.item.all}`)]
  4144. ),
  4145. ]),
  4146. ]),
  4147. ]),
  4148. ]),
  4149. ]
  4150. );
  4151. },
  4152. };
  4153. /**
  4154. * SsListCard - 列表卡片组件
  4155. *
  4156. * @description 用于显示列表项的卡片组件,支持缩略图、标签、状态、操作按钮和选择功能
  4157. *
  4158. * @prop {Object} item - 卡片数据对象
  4159. * @prop {String} item.title - 卡片标题
  4160. * @prop {String} [item.thumb] - 缩略图 URL(可选)
  4161. * @prop {String} [item.thumbType] - 缩略图类型:'thumbnail'(缩略图)或默认(证件照)
  4162. * @prop {String} [item.status] - 卡片状态:'available'(可用-绿色)、'unavailable'(不可用-黄色)、'disabled'(禁用-红色)
  4163. * @prop {Array} item.tags - 标签数组,格式:[{键: 值}, ...]
  4164. * @prop {Function} item.onclick - 点击卡片的回调函数
  4165. * @prop {Array} [item.buttons] - 操作按钮数组(可选),显示在右上角齿轮
  4166. * @prop {Array} [item.statusIcons] - 状态图标数组(可选),显示在右上角,格式:[{class: '图标类名', title: '提示文字'}, ...]
  4167. *
  4168. * @example
  4169. * // 基础用法
  4170. * const item = {
  4171. * title: "卡片标题",
  4172. * tags: [
  4173. * { 类型: '文档' },
  4174. * { 状态: '进行中' }
  4175. * ],
  4176. * onclick: () => console.log('点击了卡片')
  4177. * };
  4178. *
  4179. * @example
  4180. * // 带缩略图和状态
  4181. * const item = {
  4182. * title: "场地预定",
  4183. * thumbType: 'thumbnail',
  4184. * thumb: "https://example.com/image.jpg",
  4185. * status: "available", // 绿色背景
  4186. * tags: [{ 容量: '50人' }],
  4187. * onclick: () => {}
  4188. * };
  4189. *
  4190. * @example
  4191. * // 带操作按钮和状态图标
  4192. * const item = {
  4193. * title: "会议室A",
  4194. * tags: [{ 楼层: '3F' }],
  4195. * onclick: () => {},
  4196. * // 右上角操作按钮(齿轮)
  4197. * buttons: [{
  4198. * class: 'cart-list-setting',
  4199. * title: '编辑',
  4200. * onclick: () => console.log('编辑')
  4201. * }],
  4202. * // 右上角状态图标(在齿轮右边)
  4203. * statusIcons: [{
  4204. * class: 'icon-emoji',
  4205. * title: '清洁中'
  4206. * }]
  4207. * };
  4208. *
  4209. * @features
  4210. * - 卡片选择:鼠标悬停右下角显示选择角标,点击切换选中状态,选中后显示底部深灰色线条
  4211. * - 状态颜色:根据 status 字段显示不同背景色(可用/不可用/禁用)
  4212. * - 图片类型:支持证件照(73×100px)和缩略图(180×100px)两种尺寸
  4213. * - 操作按钮:右上角齿轮,hover 显示,支持多个按钮下拉菜单
  4214. * - 状态图标:右上角显示状态图标,齿轮会根据图标数量自动左移
  4215. *
  4216. * @author xu
  4217. * @date 20260105
  4218. */
  4219. // 组件文档补全(JSDoc) by xu 20260108
  4220. /**
  4221. * SsListCard(左侧对象卡片)
  4222. *
  4223. * 用途:
  4224. * - 渲染左侧卡片(标题 + tags)
  4225. * - 右下角“角标”用于选中/取消选中(不会触发卡片 click)
  4226. *
  4227. * 调用示例:
  4228. * ```html
  4229. * <ss-list-card :item="item" @toggle-select="handleToggleSelect" @click="openDetail"></ss-list-card>
  4230. * ```
  4231. *
  4232. * Props:
  4233. * - `item`:卡片数据对象(建议含 `id/title/tags[]`;内部会读写 `item._ssSelected` 作为选中态)
  4234. *
  4235. * Emits:
  4236. * - `toggle-select`:点击角标触发,参数 `{ item, selected }`
  4237. * - `click`:点击卡片主体触发(用于打开详情等)
  4238. */
  4239. const SsListCard = {
  4240. name: "SsListCard",
  4241. props: {
  4242. ssObjName: { type: String, default: "" }, // 功能:业务对象名(用于默认缩略图 icon) by xu 20260109
  4243. cardClickAction: { type: String, default: "view" }, // 功能:卡片主体点击动作(view=查看;single=单选互斥) by xu 20260109
  4244. item: {
  4245. type: Object,
  4246. required: true,
  4247. },
  4248. },
  4249. emits: ["click", "change", "toggle-select"],
  4250. setup(props, { emit }) {
  4251. const item = props.item;
  4252. // 移除 itemWidth 计算属性,不再需要 by xu 20260105
  4253. // 判断卡片类型 by xu 20260105
  4254. const cardType = Vue.computed(() => {
  4255. // 支持“无图但保留缩略图占位”(同一业务列表图片形态一致) by xu 20260109
  4256. if (!item.thumb && !item.thumbType) return ""; // 业务列表“无图”形态
  4257. // 根据 thumbType 字段判断,如果没有则默认为证件照
  4258. return item.thumbType === "thumbnail" ? "card-thumbnail" : "card-photo";
  4259. });
  4260. // 判断状态类型 - 场地预定状态 by xu 20260105
  4261. const statusClass = Vue.computed(() => {
  4262. if (!item.status) return ""; // 无状态,默认白色
  4263. // 映射状态值到 CSS 类名
  4264. const statusMap = {
  4265. available: "status-available",
  4266. unavailable: "status-unavailable",
  4267. disabled: "status-disabled",
  4268. };
  4269. return statusMap[item.status] || "";
  4270. });
  4271. const onItemClick = (e) => {
  4272. // 清除所有类型卡片的 active 状态(卡片主体点击仅做 active 高亮) by xu 20260109
  4273. const allListCards = document.querySelectorAll(
  4274. ".knowledge-item-container"
  4275. );
  4276. const allFolderCards = document.querySelectorAll(".ss-folder-list");
  4277. allListCards.forEach((card) => card.classList.remove("active"));
  4278. allFolderCards.forEach((card) => card.classList.remove("active"));
  4279. // 设置当前项的 active 状态
  4280. e.currentTarget.classList.add("active");
  4281. // 卡片主体点击动作由页面级配置控制 by xu 20260109
  4282. if (props.cardClickAction === "view") {
  4283. props.item.onclick?.();
  4284. // 通知父级:卡片点击(查看) by xu 20260109
  4285. emit("click", props.item);
  4286. }
  4287. };
  4288. const onItemChange = (e, icon, index) => {
  4289. e.stopPropagation(); // 阻止事件冒泡到卡片
  4290. props.item.buttons[0].onclick();
  4291. // emit("change", { item: props.item, icon, index });
  4292. };
  4293. return {
  4294. item,
  4295. cardType,
  4296. statusClass,
  4297. onItemClick,
  4298. onItemChange,
  4299. };
  4300. },
  4301. data() {
  4302. return {
  4303. showButtons: false,
  4304. selected: false, // 选择状态 by xu 20260105
  4305. showTextPopover: false, // 功能:右侧文字区 hover 展示全量 by xu 20260108
  4306. textPopoverType: "", // second-summary / second-tags / third / third-full by xu 20260108
  4307. textPopoverBottom: 0, // 功能:popover 从当前省略行位置向上展开 by xu 20260108
  4308. hideTextPopoverTimer: null, // 功能:鼠标从省略行移到浮层的缓冲 by xu 20260108
  4309. textPopoverPayload: null, // { kind, text?, lines? } by xu 20260108
  4310. ellipsisVisible: {
  4311. // 功能:只在真实出现 ... 时才显示命中区/允许 goheight by xu 20260109
  4312. secondSummary: false,
  4313. secondTags: false,
  4314. third: false,
  4315. thirdFull: false,
  4316. },
  4317. };
  4318. },
  4319. methods: {
  4320. __allowSelect() {
  4321. // 功能说明:无 rbarObj 时禁用选中能力(隐藏右下角勾选并禁止 toggle) by xu 20260122
  4322. try {
  4323. if (this?.$root && this.$root.hasObjPanel === false) return false;
  4324. if (
  4325. typeof window !== "undefined" &&
  4326. window.__objListVm &&
  4327. window.__objListVm.hasObjPanel === false
  4328. )
  4329. return false;
  4330. } catch (_) {}
  4331. return true;
  4332. },
  4333. // 切换选择状态(对外 emit,支持方案A父级 state 中转) by xu 20260106
  4334. toggleSelect(e) {
  4335. e.stopPropagation();
  4336. if (!this.__allowSelect()) return; // 功能说明:无 rbarObj 时禁止选中 by xu 20260122
  4337. // 使用 item 上的状态,便于父级/右侧边栏反向同步 by xu 20260106
  4338. this.item._ssSelected = !this.item?._ssSelected;
  4339. this.$emit("toggle-select", {
  4340. item: this.item,
  4341. selected: !!this.item?._ssSelected,
  4342. });
  4343. },
  4344. // 卡片主体点击=单选互斥:只有“本次切到选中”才清理其他选中 by xu 20260109
  4345. toggleSelectExclusive(e) {
  4346. e?.stopPropagation?.();
  4347. if (!this.__allowSelect()) return; // 功能说明:无 rbarObj 时禁止选中 by xu 20260122
  4348. this.item._ssSelected = !this.item?._ssSelected;
  4349. const selected = !!this.item?._ssSelected;
  4350. this.$emit("toggle-select", {
  4351. item: this.item,
  4352. selected,
  4353. exclusive: true,
  4354. });
  4355. },
  4356. // 功能:无缩略图时,用业务对象 icon 做默认图(ss-icon + icon-obj-xx) by xu 20260109
  4357. getBizThumbIconClass() {
  4358. const name = String(
  4359. this.ssObjName ||
  4360. this.item?.ssObjName ||
  4361. this.$root?.ssObjName ||
  4362. window?.ss?.dom?.ssObjName ||
  4363. ""
  4364. ).trim();
  4365. if (!name) return "";
  4366. return `icon-obj-${name}`;
  4367. },
  4368. // 功能:构造右侧文字区 4 行(摘要/类目或标签/对象号) by xu 20260108
  4369. buildRightTextLines() {
  4370. const item = this.item || {};
  4371. const summary = String(item?.desc ?? "").trim(); // 后端字段后续映射 by xu 20260108
  4372. const objNum = String(item?.objNum ?? "").trim(); // 后端字段后续映射 by xu 20260108
  4373. const categoryArr = Array.isArray(item?.category) ? item.category : [];
  4374. const tagsArr = Array.isArray(item?.tags) ? item.tags : [];
  4375. const hasTags = tagsArr.length > 0;
  4376. const hasCategory = categoryArr.length > 0;
  4377. // 第二部分(L1-L3):摘要优先,其次物品参数 by xu 20260108
  4378. const secondKind = summary ? "summary" : hasTags ? "tags" : "";
  4379. const hasSecond = !!secondKind;
  4380. // 第三部分(L4):对象号优先,其次类目 by xu 20260108
  4381. const thirdKind = objNum ? "objNum" : hasCategory ? "category" : "";
  4382. const hasThird = !!thirdKind;
  4383. const thirdFull = !hasSecond && hasThird; // 第二部分为空则第三部分占满 4 行 by xu 20260108
  4384. const secondFull = hasSecond && !hasThird && secondKind === "tags"; // 仅标签时占满 4 行(不留空行) by xu 20260109
  4385. const toValueText = (obj) => {
  4386. // 功能说明:类目/物品参数回显展示 key: value(否则只显示值看不懂) by xu 20260114
  4387. const [k, v] = Object.entries(obj || {})[0] || ["", ""];
  4388. const key = k !== undefined && k !== null ? String(k).trim() : "";
  4389. const val = v !== undefined && v !== null ? String(v).trim() : "";
  4390. if (key && val) return `${key}:${val}`;
  4391. if (val) return val;
  4392. if (key) return key;
  4393. return "";
  4394. };
  4395. const toLineList = (arr) =>
  4396. (arr || [])
  4397. .map(toValueText)
  4398. .map((s) => String(s ?? "").trim())
  4399. .filter(Boolean);
  4400. const flat = (arr) => toLineList(arr).join(" ");
  4401. // 第二部分 tags:默认 3 行;仅标签时占满 4 行(避免底部空一行) by xu 20260109
  4402. const secondTagsMaxLines = secondFull ? 4 : 3;
  4403. const secondTagsLinesFull = toLineList(tagsArr);
  4404. const secondTagsHead = secondTagsLinesFull.slice(
  4405. 0,
  4406. Math.max(0, secondTagsMaxLines - 1)
  4407. );
  4408. const secondTagsTail = secondTagsLinesFull.slice(
  4409. Math.max(0, secondTagsMaxLines - 1)
  4410. );
  4411. const secondTagsLast =
  4412. secondTagsTail.length <= 1
  4413. ? secondTagsTail[0] || ""
  4414. : secondTagsTail.join(" "); // 功能:最后一行平铺剩余 by xu 20260108
  4415. // 第三部分类目:卡片上串成一行;goheight 展开时一条一行 by xu 20260109
  4416. const categoryLinesFull = toLineList(categoryArr);
  4417. const categoryLine = categoryLinesFull.join(" ");
  4418. const thirdText =
  4419. thirdKind === "objNum"
  4420. ? objNum
  4421. : thirdKind === "category"
  4422. ? categoryLine
  4423. : "";
  4424. return {
  4425. secondKind,
  4426. thirdKind,
  4427. hasSecond,
  4428. hasThird,
  4429. thirdFull,
  4430. secondFull,
  4431. summary,
  4432. secondTagsMaxLines,
  4433. secondTagsHead,
  4434. secondTagsLast,
  4435. secondTagsLinesFull,
  4436. objNum,
  4437. categoryLine,
  4438. categoryLinesFull,
  4439. thirdText,
  4440. };
  4441. },
  4442. measureTextOverflowByLines(text, maxLines, width) {
  4443. const w = Number(width) || 0;
  4444. if (!w || !text) return false;
  4445. const probe = document.createElement("div");
  4446. probe.style.position = "fixed";
  4447. probe.style.left = "-99999px";
  4448. probe.style.top = "0";
  4449. probe.style.width = w + "px";
  4450. probe.style.fontSize = "18px";
  4451. probe.style.lineHeight = "24px";
  4452. probe.style.whiteSpace = "normal";
  4453. probe.style.wordBreak = "break-word";
  4454. probe.style.visibility = "hidden";
  4455. probe.textContent = text;
  4456. document.body.appendChild(probe);
  4457. const h = probe.getBoundingClientRect().height || 0;
  4458. document.body.removeChild(probe);
  4459. return h > maxLines * 24 + 1;
  4460. },
  4461. measureSingleLineOverflow(text, width) {
  4462. const w = Number(width) || 0;
  4463. if (!w || !text) return false;
  4464. const probe = document.createElement("span");
  4465. probe.style.position = "fixed";
  4466. probe.style.left = "-99999px";
  4467. probe.style.top = "0";
  4468. probe.style.display = "inline-block";
  4469. probe.style.maxWidth = w + "px";
  4470. probe.style.fontSize = "18px";
  4471. probe.style.lineHeight = "24px";
  4472. probe.style.whiteSpace = "nowrap";
  4473. probe.style.visibility = "hidden";
  4474. probe.textContent = text;
  4475. document.body.appendChild(probe);
  4476. const overflow =
  4477. (probe.scrollWidth || 0) > (probe.clientWidth || w) + 1;
  4478. document.body.removeChild(probe);
  4479. return overflow;
  4480. },
  4481. // 功能:根据当前卡片宽度刷新「是否出现 ...」状态(用于控制命中区显示) by xu 20260109
  4482. refreshEllipsisVisible() {
  4483. try {
  4484. const right = this.$el?.querySelector?.(".right");
  4485. const rawWidth =
  4486. right?.getBoundingClientRect?.().width || right?.clientWidth || 0;
  4487. const width = Math.max(0, Math.round(rawWidth)); // 修复:内容区不再使用 padding-right 预留,测量按真实宽度 by xu 20260109
  4488. const model = this.buildRightTextLines();
  4489. const next = {
  4490. secondSummary: false,
  4491. secondTags: false,
  4492. third: false,
  4493. thirdFull: false,
  4494. };
  4495. if (model.secondKind === "summary" && model.summary) {
  4496. next.secondSummary = this.measureTextOverflowByLines(
  4497. model.summary,
  4498. 3,
  4499. width
  4500. );
  4501. }
  4502. if (model.secondKind === "tags") {
  4503. // 功能说明:tags 采用「最后一行平铺剩余」策略,是否出现 ... 仅取决于最后一行是否溢出(数量多但平铺放得下不算溢出) by xu 20260114
  4504. next.secondTags = this.measureSingleLineOverflow(
  4505. model.secondTagsLast,
  4506. width
  4507. );
  4508. }
  4509. if (model.hasThird && !model.thirdFull) {
  4510. next.third = this.measureSingleLineOverflow(model.thirdText, width);
  4511. }
  4512. if (model.hasThird && model.thirdFull) {
  4513. next.thirdFull = this.measureTextOverflowByLines(
  4514. model.thirdText,
  4515. 4,
  4516. width
  4517. );
  4518. }
  4519. const prev = this.ellipsisVisible || {};
  4520. const changed =
  4521. prev.secondSummary !== next.secondSummary ||
  4522. prev.secondTags !== next.secondTags ||
  4523. prev.third !== next.third ||
  4524. prev.thirdFull !== next.thirdFull;
  4525. if (changed) this.ellipsisVisible = next;
  4526. } catch (e) {
  4527. // ignore by xu 20260109
  4528. }
  4529. },
  4530. showTextPopoverFor(el, kind) {
  4531. // 调试开关:window.__SS_LISTCARD_DEBUG__ = true 时打印 hover/溢出判断日志 by xu 20260108
  4532. const debug =
  4533. typeof window !== "undefined" && !!window.__SS_LISTCARD_DEBUG__;
  4534. if (debug) {
  4535. console.log("[SsListCard] ellipsis hover", {
  4536. kind,
  4537. el: el?.className,
  4538. });
  4539. }
  4540. const model = this.buildRightTextLines();
  4541. const lineEl = el?.closest?.(".ss-card-text__line") || el;
  4542. const right = lineEl?.closest?.(".right") || el?.closest?.(".right");
  4543. const rawWidth =
  4544. right?.getBoundingClientRect?.().width || right?.clientWidth || 0;
  4545. const width = Math.max(0, Math.round(rawWidth)); // 修复:内容区不再预留 padding-right,测量按真实宽度 by xu 20260109
  4546. const textEl =
  4547. kind === "second-summary"
  4548. ? lineEl?.querySelector?.(".ss-card-text__secondSummary")
  4549. : kind === "second-tags"
  4550. ? lineEl?.querySelector?.(".ss-card-text__tagLineLast")
  4551. : kind === "third"
  4552. ? lineEl?.querySelector?.(".ss-card-text__thirdLine")
  4553. : lineEl?.querySelector?.(".ss-card-text__thirdFull");
  4554. let payload = null;
  4555. // 仅当真实会出现 ... 时才允许 goheight(避免“没超出也能出 goheight”) by xu 20260109
  4556. const overflowed =
  4557. kind === "second-summary"
  4558. ? this.measureTextOverflowByLines(model.summary, 3, width)
  4559. : kind === "second-tags"
  4560. ? this.measureSingleLineOverflow(model.secondTagsLast, width) // 功能说明:同 refreshEllipsisVisible,tags 仅看最后一行是否溢出 by xu 20260114
  4561. : kind === "third"
  4562. ? this.measureSingleLineOverflow(model.thirdText, width)
  4563. : this.measureTextOverflowByLines(model.thirdText, 4, width);
  4564. if (!overflowed) return;
  4565. if (kind === "second-summary") {
  4566. if (model.summary) payload = { kind, text: model.summary };
  4567. } else if (kind === "second-tags") {
  4568. if (
  4569. Array.isArray(model.secondTagsLinesFull) &&
  4570. model.secondTagsLinesFull.length
  4571. ) {
  4572. payload = { kind, lines: model.secondTagsLinesFull };
  4573. }
  4574. } else if (kind === "third") {
  4575. if (
  4576. model.thirdKind === "category" &&
  4577. Array.isArray(model.categoryLinesFull) &&
  4578. model.categoryLinesFull.length
  4579. ) {
  4580. payload = { kind, lines: model.categoryLinesFull }; // 功能:类目展开一条一行 by xu 20260109
  4581. } else if (model.thirdText) {
  4582. payload = { kind, text: model.thirdText };
  4583. }
  4584. } else if (kind === "third-full") {
  4585. if (
  4586. model.thirdKind === "category" &&
  4587. Array.isArray(model.categoryLinesFull) &&
  4588. model.categoryLinesFull.length
  4589. ) {
  4590. payload = { kind, lines: model.categoryLinesFull }; // 功能:类目占满模式展开一条一行 by xu 20260109
  4591. } else if (model.thirdText) {
  4592. payload = { kind, text: model.thirdText };
  4593. }
  4594. }
  4595. if (debug) {
  4596. console.log("[SsListCard] ellipsis decide", {
  4597. kind,
  4598. rawWidth: Math.round(rawWidth),
  4599. width,
  4600. hasPayload: !!payload,
  4601. textEl: textEl?.className,
  4602. textClient: textEl
  4603. ? {
  4604. cw: textEl.clientWidth,
  4605. ch: textEl.clientHeight,
  4606. sw: textEl.scrollWidth,
  4607. sh: textEl.scrollHeight,
  4608. }
  4609. : null,
  4610. });
  4611. }
  4612. if (!payload) return;
  4613. this.clearHideTextPopoverTimer();
  4614. const container = lineEl?.closest?.(".right");
  4615. const containerRect = container?.getBoundingClientRect?.();
  4616. const lineRect = lineEl?.getBoundingClientRect?.();
  4617. if (containerRect && lineRect) {
  4618. const bottom = Math.max(
  4619. 0,
  4620. Math.round(containerRect.bottom - lineRect.bottom)
  4621. );
  4622. this.textPopoverBottom = bottom;
  4623. } else {
  4624. this.textPopoverBottom = 0;
  4625. }
  4626. this.textPopoverPayload = payload;
  4627. this.textPopoverType = kind;
  4628. this.showTextPopover = true;
  4629. if (debug) console.log("[SsListCard] goheight show", payload);
  4630. },
  4631. isOverflowing(el) {
  4632. if (!el) return false;
  4633. // 单行/多行省略统一判断:scroll 尺寸大于 client 尺寸即认为有 ... by xu 20260108
  4634. return (
  4635. (el.scrollWidth &&
  4636. el.clientWidth &&
  4637. el.scrollWidth > el.clientWidth + 1) ||
  4638. (el.scrollHeight &&
  4639. el.clientHeight &&
  4640. el.scrollHeight > el.clientHeight + 1)
  4641. );
  4642. },
  4643. isSummaryOverflowing(el) {
  4644. if (!el) return false;
  4645. // -webkit-line-clamp 场景下 scrollHeight 不稳定,改用“无 clamp 的离屏测量”判断是否超过 2 行 by xu 20260108
  4646. const text = String(this.item?.desc ?? "").trim();
  4647. if (!text) return false;
  4648. const rect = el.getBoundingClientRect?.();
  4649. const width = rect?.width || el.clientWidth || 0;
  4650. if (!width) return false;
  4651. const probe = document.createElement("div");
  4652. probe.style.position = "fixed";
  4653. probe.style.left = "-99999px";
  4654. probe.style.top = "0";
  4655. probe.style.width = width + "px";
  4656. probe.style.fontSize = "18px";
  4657. probe.style.lineHeight = "24px";
  4658. probe.style.whiteSpace = "normal";
  4659. probe.style.wordBreak = "break-word";
  4660. probe.style.visibility = "hidden";
  4661. probe.textContent = text;
  4662. document.body.appendChild(probe);
  4663. const h = probe.getBoundingClientRect().height || 0;
  4664. document.body.removeChild(probe);
  4665. return h > 48 + 1;
  4666. },
  4667. clearHideTextPopoverTimer() {
  4668. if (this.hideTextPopoverTimer) {
  4669. clearTimeout(this.hideTextPopoverTimer);
  4670. this.hideTextPopoverTimer = null;
  4671. }
  4672. },
  4673. // 修复 goheight hover 无响应:移除重复方法覆盖,统一使用上面的 showTextPopoverFor(el, kind) by xu 20260109
  4674. hideTextPopoverLater() {
  4675. this.clearHideTextPopoverTimer();
  4676. this.hideTextPopoverTimer = setTimeout(() => {
  4677. this.showTextPopover = false;
  4678. this.textPopoverType = "";
  4679. this.textPopoverPayload = null;
  4680. }, 120);
  4681. },
  4682. hideTextPopover() {
  4683. this.clearHideTextPopoverTimer();
  4684. this.showTextPopover = false;
  4685. this.textPopoverType = "";
  4686. this.textPopoverPayload = null;
  4687. },
  4688. // 功能:新需求下不在 updated 内做测量,避免死循环 by xu 20260108
  4689. },
  4690. mounted() {
  4691. // 无需在 mounted/updated 里做 overflow 测量(避免死循环),只在 hover 触发时判断 by xu 20260108
  4692. // 仅用于控制“...命中区是否显示”,不会触发循环更新 by xu 20260109
  4693. this.$nextTick?.(() => {
  4694. requestAnimationFrame(() => this.refreshEllipsisVisible?.());
  4695. });
  4696. this.__ssListCardResizeHandler = () => this.refreshEllipsisVisible?.(); // 功能:窗口变化时刷新 ... 显示 by xu 20260109
  4697. window.addEventListener?.("resize", this.__ssListCardResizeHandler);
  4698. },
  4699. updated() {
  4700. // 卡片数据更新后刷新一次 ... 显示状态(避免“宽度/内容变了但命中区不变”) by xu 20260109
  4701. this.$nextTick?.(() => {
  4702. requestAnimationFrame(() => this.refreshEllipsisVisible?.());
  4703. });
  4704. },
  4705. beforeUnmount() {
  4706. // 清理 timer,避免残留导致异常 by xu 20260108
  4707. this.clearHideTextPopoverTimer?.();
  4708. if (this.__ssListCardResizeHandler) {
  4709. window.removeEventListener?.("resize", this.__ssListCardResizeHandler);
  4710. this.__ssListCardResizeHandler = null;
  4711. }
  4712. },
  4713. render() {
  4714. const SsCartListIcon = Vue.resolveComponent("ss-cart-list-icon");
  4715. const SsIcon = Vue.resolveComponent("ss-icon");
  4716. const hasThumbArea = !!(this.item?.thumb || this.item?.thumbType); // 功能:无图但有 thumbType 时仍保留占位 by xu 20260109
  4717. const thumbSrc = (() => {
  4718. // 功能说明:兼容 thumb 为 raw path 或 {val}/{value},组件内统一转为 dlByHttp URL by xu 20260122
  4719. const t = this.item?.thumb;
  4720. if (!t) return "";
  4721. if (typeof t === "string") {
  4722. const s = t.trim();
  4723. if (!s) return "";
  4724. // 已经是 URL/绝对路径则直接使用;否则按 path 构建 dlByHttp by xu 20260122
  4725. if (
  4726. /^https?:\/\//i.test(s) ||
  4727. s.startsWith("/service?") ||
  4728. s.startsWith("/")
  4729. )
  4730. return s;
  4731. return buildThumbUrl(s);
  4732. }
  4733. return buildThumbUrl(t);
  4734. })();
  4735. return Vue.h(
  4736. "div",
  4737. {
  4738. class: {
  4739. "knowledge-item-container": true,
  4740. active: this.item.active,
  4741. [this.cardType]: !!this.cardType, // 动态添加卡片类型类名 by xu 20260105
  4742. [this.statusClass]: !!this.statusClass,
  4743. },
  4744. onClick: (e) => {
  4745. this.onItemClick?.(e);
  4746. if (this.__allowSelect() && this.cardClickAction === "single") {
  4747. // 功能说明:无 rbarObj 时不允许单选互斥 by xu 20260122
  4748. this.toggleSelectExclusive?.(e);
  4749. }
  4750. }, // 功能:卡片主体点击动作(view/single) by xu 20260109
  4751. // 移除固定宽度,由 CSS min-width 控制 by xu 20260105
  4752. },
  4753. [
  4754. // 右上角状态图标区域 by xu 20260105
  4755. this.item?.statusIcons?.length > 0 &&
  4756. Vue.h(
  4757. "div",
  4758. { class: "card-status-icons" },
  4759. this.item.statusIcons.map((icon) =>
  4760. Vue.h(SsIcon, {
  4761. class: `status-icon ${icon.class}`,
  4762. title: icon.title,
  4763. })
  4764. )
  4765. ),
  4766. this.item?.buttons?.length > 0 &&
  4767. Vue.h(
  4768. "div",
  4769. {
  4770. class: "header",
  4771. style:
  4772. this.item?.statusIcons?.length > 0
  4773. ? {
  4774. right: `${this.item.statusIcons.length * 48}px`,
  4775. borderTopRightRadius: "0",
  4776. }
  4777. : {},
  4778. onMouseenter: () => (this.showButtons = true),
  4779. onMouseleave: () => (this.showButtons = false),
  4780. onClick: (e) => this.onItemChange(e, this.item.buttons[0], 0),
  4781. },
  4782. [
  4783. // 只在有按钮时渲染设置图标
  4784. // this.item?.buttons?.length > 0 &&
  4785. Vue.h("div", {
  4786. class: "cart-list-setting cart-list-icon",
  4787. title: this.item?.buttons?.[0]?.title,
  4788. }),
  4789. // 鼠标移入时显示按钮列表,与图标同级
  4790. // this.item?.buttons?.length > 0 &&
  4791. this.showButtons &&
  4792. this.item?.buttons?.length > 1 &&
  4793. Vue.h(
  4794. "div",
  4795. {
  4796. class: "cart-list-button-popup",
  4797. },
  4798. this.item.buttons.map((btn) =>
  4799. Vue.h(
  4800. "div",
  4801. {
  4802. onClick: (e) => {
  4803. e.stopPropagation();
  4804. btn.onclick?.();
  4805. },
  4806. },
  4807. [
  4808. // 如果有 class,显示对应的图标
  4809. btn.class &&
  4810. Vue.h(SsCartListIcon, {
  4811. class: [btn.class],
  4812. }),
  4813. // 显示按钮文本
  4814. Vue.h("span", null, btn.title),
  4815. ]
  4816. )
  4817. )
  4818. ),
  4819. ]
  4820. ),
  4821. Vue.h("div", { class: "body" }, [
  4822. Vue.h("div", { class: "box-header" }, [
  4823. Vue.h("div", `${this.item.title}`),
  4824. ]),
  4825. Vue.h(
  4826. "div",
  4827. {
  4828. class: !hasThumbArea ? "no-thumb box-body" : "box-body",
  4829. },
  4830. [
  4831. hasThumbArea
  4832. ? thumbSrc
  4833. ? Vue.h("div", { class: "left" }, [
  4834. Vue.h("img", {
  4835. src: thumbSrc,
  4836. alt: "Thumbnail",
  4837. class: "imgUnHandle",
  4838. style: {
  4839. "object-fit": "cover",
  4840. width: "100%",
  4841. height: "100%",
  4842. },
  4843. }),
  4844. ])
  4845. : Vue.h(
  4846. // 功能:无图占位(ss-icon + biz icon,居中) by xu 20260109
  4847. "div",
  4848. { class: "left ss-objlist-thumbPlaceholder" },
  4849. [
  4850. Vue.h(SsIcon, {
  4851. class: `${this.getBizThumbIconClass()} ss-objlist-thumbIcon`,
  4852. }),
  4853. ]
  4854. )
  4855. : null,
  4856. Vue.h(
  4857. "div",
  4858. {
  4859. class: "right",
  4860. },
  4861. (() => {
  4862. const model = this.buildRightTextLines(); // 功能:右侧文字区新规则(第二部分/第三部分优先级) by xu 20260108
  4863. const hasAny = !!(model?.hasSecond || model?.hasThird);
  4864. if (!hasAny) return [];
  4865. const children = [];
  4866. // 第二部分:L1-L3(摘要优先,其次 tags;不足留空;超出 L3 ...) by xu 20260108
  4867. if (model.hasSecond) {
  4868. if (model.secondKind === "summary") {
  4869. children.push(
  4870. Vue.h(
  4871. "div",
  4872. {
  4873. class:
  4874. "ss-card-text__line ss-card-text__secondBlock",
  4875. },
  4876. [
  4877. Vue.h(
  4878. "div",
  4879. {
  4880. class: "ss-card-text__secondSummary",
  4881. title: model.summary,
  4882. },
  4883. model.summary
  4884. ),
  4885. Vue.h("span", {
  4886. class: [
  4887. "ss-card-text__ellipsisHit",
  4888. "ss-card-text__ellipsisHit--second",
  4889. this.ellipsisVisible?.secondSummary
  4890. ? "is-on"
  4891. : "",
  4892. ],
  4893. title: "查看完整摘要",
  4894. onMouseenter: (e) =>
  4895. this.showTextPopoverFor(
  4896. e?.currentTarget,
  4897. "second-summary"
  4898. ),
  4899. onClick: (e) => {
  4900. e?.stopPropagation?.();
  4901. this.showTextPopoverFor(
  4902. e?.currentTarget,
  4903. "second-summary"
  4904. );
  4905. }, // 功能:点击 ... 也可展开(避免 hover 受遮挡) by xu 20260109
  4906. onMouseleave: () => this.hideTextPopoverLater(),
  4907. }),
  4908. ]
  4909. )
  4910. );
  4911. } else if (model.secondKind === "tags") {
  4912. children.push(
  4913. Vue.h(
  4914. "div",
  4915. {
  4916. class: [
  4917. "ss-card-text__line",
  4918. model.secondFull
  4919. ? "ss-card-text__secondFullBlock"
  4920. : "ss-card-text__secondBlock",
  4921. ],
  4922. },
  4923. [
  4924. // 功能:仅标签时占满 4 行 by xu 20260109
  4925. Vue.h(
  4926. "div",
  4927. { class: "ss-card-text__secondTags" },
  4928. [
  4929. ...model.secondTagsHead.map((t) =>
  4930. Vue.h(
  4931. "div",
  4932. {
  4933. class: "ss-card-text__tagLine",
  4934. title: t,
  4935. },
  4936. t
  4937. )
  4938. ),
  4939. // 第三行:平铺剩余(可能为空) by xu 20260108
  4940. Vue.h(
  4941. "div",
  4942. {
  4943. class:
  4944. "ss-card-text__tagLine is-last ss-card-text__tagLineLast",
  4945. title: model.secondTagsLast,
  4946. },
  4947. model.secondTagsLast
  4948. ),
  4949. ].filter(Boolean)
  4950. ),
  4951. // 只在最后一行出现 ... 时才触发 goheight by xu 20260108
  4952. Vue.h("span", {
  4953. class: [
  4954. "ss-card-text__ellipsisHit",
  4955. "ss-card-text__ellipsisHit--second",
  4956. this.ellipsisVisible?.secondTags
  4957. ? "is-on"
  4958. : "",
  4959. ],
  4960. title: "查看完整物品参数",
  4961. onMouseenter: (e) =>
  4962. this.showTextPopoverFor(
  4963. e?.currentTarget,
  4964. "second-tags"
  4965. ),
  4966. onClick: (e) => {
  4967. e?.stopPropagation?.();
  4968. this.showTextPopoverFor(
  4969. e?.currentTarget,
  4970. "second-tags"
  4971. );
  4972. }, // 功能:点击 ... 也可展开(避免 hover 受遮挡) by xu 20260109
  4973. onMouseleave: () => this.hideTextPopoverLater(),
  4974. }),
  4975. ]
  4976. )
  4977. );
  4978. }
  4979. }
  4980. // 第三部分:默认 L4;第二部分为空则占满 L1-L4 by xu 20260108
  4981. if (model.hasThird) {
  4982. if (model.thirdFull) {
  4983. children.push(
  4984. Vue.h(
  4985. "div",
  4986. {
  4987. class:
  4988. "ss-card-text__line ss-card-text__thirdFullBlock",
  4989. },
  4990. [
  4991. Vue.h(
  4992. "div",
  4993. {
  4994. class: "ss-card-text__thirdFull",
  4995. title: model.thirdText,
  4996. },
  4997. model.thirdText
  4998. ),
  4999. Vue.h("span", {
  5000. class: [
  5001. "ss-card-text__ellipsisHit",
  5002. "ss-card-text__ellipsisHit--third",
  5003. this.ellipsisVisible?.thirdFull
  5004. ? "is-on"
  5005. : "",
  5006. ],
  5007. title: "查看完整信息",
  5008. onMouseenter: (e) =>
  5009. this.showTextPopoverFor(
  5010. e?.currentTarget,
  5011. "third-full"
  5012. ),
  5013. onClick: (e) => {
  5014. e?.stopPropagation?.();
  5015. this.showTextPopoverFor(
  5016. e?.currentTarget,
  5017. "third-full"
  5018. );
  5019. }, // 功能:点击 ... 也可展开(避免 hover 受遮挡) by xu 20260109
  5020. onMouseleave: () => this.hideTextPopoverLater(),
  5021. }),
  5022. ]
  5023. )
  5024. );
  5025. } else {
  5026. children.push(
  5027. Vue.h(
  5028. "div",
  5029. {
  5030. class:
  5031. "ss-card-text__line ss-card-text__thirdLineWrap",
  5032. },
  5033. [
  5034. Vue.h(
  5035. "div",
  5036. {
  5037. class: "ss-card-text__thirdLine",
  5038. title: model.thirdText,
  5039. },
  5040. model.thirdText
  5041. ),
  5042. Vue.h("span", {
  5043. class: [
  5044. "ss-card-text__ellipsisHit",
  5045. "ss-card-text__ellipsisHit--third",
  5046. this.ellipsisVisible?.third ? "is-on" : "",
  5047. ],
  5048. title: "查看完整信息",
  5049. onMouseenter: (e) =>
  5050. this.showTextPopoverFor(
  5051. e?.currentTarget,
  5052. "third"
  5053. ),
  5054. onClick: (e) => {
  5055. e?.stopPropagation?.();
  5056. this.showTextPopoverFor(
  5057. e?.currentTarget,
  5058. "third"
  5059. );
  5060. }, // 功能:点击 ... 也可展开(避免 hover 受遮挡) by xu 20260109
  5061. onMouseleave: () => this.hideTextPopoverLater(),
  5062. }),
  5063. ]
  5064. )
  5065. );
  5066. }
  5067. }
  5068. // hover 展开浮层:宽度=右侧文字区,底对齐向上展开,带阴影 by xu 20260108
  5069. // popover 作为 `.right` 的 sibling 渲染,避免被 `.ss-card-text{overflow:hidden}` 裁剪 by xu 20260108
  5070. const popover =
  5071. this.showTextPopover &&
  5072. Vue.h(
  5073. "div",
  5074. {
  5075. class: "ss-card-text-popover",
  5076. style: { bottom: this.textPopoverBottom + "px" },
  5077. onMouseenter: () => {
  5078. this.clearHideTextPopoverTimer();
  5079. this.showTextPopover = true;
  5080. },
  5081. onMouseleave: () => this.hideTextPopoverLater(),
  5082. },
  5083. (() => {
  5084. const p = this.textPopoverPayload || {};
  5085. if (p.kind === "second-summary" && p.text) {
  5086. return [
  5087. Vue.h(
  5088. "div",
  5089. { class: "ss-card-text-popover__summary" },
  5090. p.text
  5091. ),
  5092. ];
  5093. }
  5094. if (Array.isArray(p.lines)) {
  5095. return [
  5096. Vue.h(
  5097. "div",
  5098. { class: "ss-card-text-popover__kvlist" },
  5099. p.lines.map((t) =>
  5100. Vue.h(
  5101. "div",
  5102. { class: "ss-card-text-popover__kv" },
  5103. t
  5104. )
  5105. )
  5106. ),
  5107. ];
  5108. }
  5109. if (
  5110. (p.kind === "third" || p.kind === "third-full") &&
  5111. p.text
  5112. ) {
  5113. return [
  5114. Vue.h(
  5115. "div",
  5116. { class: "ss-card-text-popover__objno" },
  5117. p.text
  5118. ),
  5119. ];
  5120. }
  5121. return [];
  5122. })()
  5123. );
  5124. return [
  5125. Vue.h("div", { class: "ss-card-text" }, children),
  5126. popover,
  5127. ];
  5128. })()
  5129. ),
  5130. ]
  5131. ),
  5132. ]),
  5133. // 右下角卡片选择图标 by xu 20260105
  5134. this.__allowSelect()
  5135. ? Vue.h(SsIcon, {
  5136. class: this.item?._ssSelected
  5137. ? "card-icon icon-cardChk-on"
  5138. : "card-icon icon-cardChk",
  5139. onClick: this.toggleSelect,
  5140. })
  5141. : null,
  5142. // 选中后底部线条 by xu 20260105
  5143. this.__allowSelect() &&
  5144. this.item?._ssSelected &&
  5145. Vue.h("div", { class: "select-bottom-line" }),
  5146. ]
  5147. );
  5148. },
  5149. };
  5150. // 二级对象卡片:复用一级对象新卡片布局/省略浮层,但去掉勾选与 single 选中,仅支持点击查看 by xu 20260115
  5151. const SsCObjCardList = {
  5152. name: "SsCObjCardList",
  5153. props: {
  5154. ssObjName: { type: String, default: "" }, // 功能说明:业务对象名(用于默认缩略图 icon) by xu 20260115
  5155. item: {
  5156. type: Object,
  5157. required: true,
  5158. },
  5159. },
  5160. emits: ["click", "change"],
  5161. setup(props, { emit }) {
  5162. const item = props.item;
  5163. const cardType = Vue.computed(() => {
  5164. if (!item.thumb && !item.thumbType) return "";
  5165. return item.thumbType === "thumbnail" ? "card-thumbnail" : "card-photo";
  5166. });
  5167. const statusClass = Vue.computed(() => {
  5168. if (!item.status) return "";
  5169. const statusMap = {
  5170. available: "status-available",
  5171. unavailable: "status-unavailable",
  5172. disabled: "status-disabled",
  5173. };
  5174. return statusMap[item.status] || "";
  5175. });
  5176. const onItemClick = (e) => {
  5177. // 清除所有类型卡片的 active 状态(保持与一级对象一致) by xu 20260115
  5178. const allListCards = document.querySelectorAll(
  5179. ".knowledge-item-container"
  5180. );
  5181. const allFolderCards = document.querySelectorAll(".ss-folder-list");
  5182. allListCards.forEach((card) => card.classList.remove("active"));
  5183. allFolderCards.forEach((card) => card.classList.remove("active"));
  5184. e.currentTarget.classList.add("active");
  5185. // 二级对象卡片:点击仅查看(调用 item.onclick) by xu 20260115
  5186. props.item.onclick?.();
  5187. emit("click", props.item);
  5188. };
  5189. const onItemChange = (e) => {
  5190. e.stopPropagation();
  5191. props.item.buttons?.[0]?.onclick?.();
  5192. };
  5193. return { item, cardType, statusClass, onItemClick, onItemChange };
  5194. },
  5195. data() {
  5196. return {
  5197. showButtons: false,
  5198. showTextPopover: false, // 功能:右侧文字区 hover 展示全量 by xu 20260115
  5199. textPopoverType: "", // second-summary / second-tags / third / third-full by xu 20260115
  5200. textPopoverBottom: 0, // 功能:popover 从当前省略行位置向上展开 by xu 20260115
  5201. hideTextPopoverTimer: null, // 功能:鼠标从省略行移到浮层的缓冲 by xu 20260115
  5202. textPopoverPayload: null, // { kind, text?, lines? } by xu 20260115
  5203. ellipsisVisible: {
  5204. secondSummary: false,
  5205. secondTags: false,
  5206. third: false,
  5207. thirdFull: false,
  5208. }, // 功能:只在真实出现 ... 时才显示命中区/允许 goheight by xu 20260115
  5209. };
  5210. },
  5211. methods: {
  5212. getBizThumbIconClass() {
  5213. // 功能:无缩略图时,用业务对象 icon 做默认图(ss-icon + icon-obj-xx) by xu 20260115
  5214. const name = String(
  5215. this.ssObjName ||
  5216. this.item?.ssObjName ||
  5217. this.$root?.ssObjName ||
  5218. window?.ss?.dom?.ssObjName ||
  5219. ""
  5220. ).trim();
  5221. if (!name) return "";
  5222. return "icon-obj-" + name;
  5223. },
  5224. buildRightTextLines() {
  5225. // 功能:沿用一级对象卡片右侧文字区规则 by xu 20260115
  5226. const item = this.item || {};
  5227. const summary = String(item?.desc ?? "").trim();
  5228. const objNum = String(item?.objNum ?? "").trim();
  5229. const categoryArr = Array.isArray(item?.category) ? item.category : [];
  5230. const tagsArr = Array.isArray(item?.tags) ? item.tags : [];
  5231. const hasTags = tagsArr.length > 0;
  5232. const hasCategory = categoryArr.length > 0;
  5233. const secondKind = summary ? "summary" : hasTags ? "tags" : "";
  5234. const hasSecond = !!secondKind;
  5235. const thirdKind = objNum ? "objNum" : hasCategory ? "category" : "";
  5236. const hasThird = !!thirdKind;
  5237. const thirdFull = !hasSecond && hasThird;
  5238. const secondFull = hasSecond && !hasThird && secondKind === "tags";
  5239. const toValueText = (obj) => {
  5240. // 功能说明:类目/物品参数回显展示 key: value(否则只显示值看不懂) by xu 20260115
  5241. const [k, v] = Object.entries(obj || {})[0] || ["", ""];
  5242. const key = k !== undefined && k !== null ? String(k).trim() : "";
  5243. const val = v !== undefined && v !== null ? String(v).trim() : "";
  5244. if (key && val) return key + ":" + val;
  5245. if (val) return val;
  5246. if (key) return key;
  5247. return "";
  5248. };
  5249. const toLineList = (arr) =>
  5250. (arr || [])
  5251. .map(toValueText)
  5252. .map((s) => String(s ?? "").trim())
  5253. .filter(Boolean);
  5254. const secondTagsMaxLines = secondFull ? 4 : 3;
  5255. const secondTagsLinesFull = toLineList(tagsArr);
  5256. const secondTagsHead = secondTagsLinesFull.slice(
  5257. 0,
  5258. Math.max(0, secondTagsMaxLines - 1)
  5259. );
  5260. const secondTagsTail = secondTagsLinesFull.slice(
  5261. Math.max(0, secondTagsMaxLines - 1)
  5262. );
  5263. const secondTagsLast =
  5264. secondTagsTail.length <= 1
  5265. ? secondTagsTail[0] || ""
  5266. : secondTagsTail.join(" ");
  5267. const categoryLinesFull = toLineList(categoryArr);
  5268. const categoryLine = categoryLinesFull.join(" ");
  5269. const thirdText =
  5270. thirdKind === "objNum"
  5271. ? objNum
  5272. : thirdKind === "category"
  5273. ? categoryLine
  5274. : "";
  5275. return {
  5276. secondKind,
  5277. thirdKind,
  5278. hasSecond,
  5279. hasThird,
  5280. thirdFull,
  5281. secondFull,
  5282. summary,
  5283. secondTagsMaxLines,
  5284. secondTagsHead,
  5285. secondTagsLast,
  5286. secondTagsLinesFull,
  5287. objNum,
  5288. categoryLine,
  5289. categoryLinesFull,
  5290. thirdText,
  5291. };
  5292. },
  5293. measureTextOverflowByLines(text, maxLines, width) {
  5294. const w = Number(width) || 0;
  5295. if (!w || !text) return false;
  5296. const probe = document.createElement("div");
  5297. probe.style.position = "fixed";
  5298. probe.style.left = "-99999px";
  5299. probe.style.top = "0";
  5300. probe.style.width = w + "px";
  5301. probe.style.fontSize = "18px";
  5302. probe.style.lineHeight = "24px";
  5303. probe.style.whiteSpace = "normal";
  5304. probe.style.wordBreak = "break-word";
  5305. probe.style.visibility = "hidden";
  5306. probe.textContent = text;
  5307. document.body.appendChild(probe);
  5308. const h = probe.getBoundingClientRect().height || 0;
  5309. document.body.removeChild(probe);
  5310. return h > maxLines * 24 + 1;
  5311. },
  5312. measureSingleLineOverflow(text, width) {
  5313. const w = Number(width) || 0;
  5314. if (!w || !text) return false;
  5315. const probe = document.createElement("span");
  5316. probe.style.position = "fixed";
  5317. probe.style.left = "-99999px";
  5318. probe.style.top = "0";
  5319. probe.style.display = "inline-block";
  5320. probe.style.maxWidth = w + "px";
  5321. probe.style.fontSize = "18px";
  5322. probe.style.lineHeight = "24px";
  5323. probe.style.whiteSpace = "nowrap";
  5324. probe.style.visibility = "hidden";
  5325. probe.textContent = text;
  5326. document.body.appendChild(probe);
  5327. const overflow =
  5328. (probe.scrollWidth || 0) > (probe.clientWidth || w) + 1;
  5329. document.body.removeChild(probe);
  5330. return overflow;
  5331. },
  5332. refreshEllipsisVisible() {
  5333. // 功能:刷新「是否出现 ...」状态(用于控制命中区显示) by xu 20260115
  5334. try {
  5335. const right = this.$el?.querySelector?.(".right");
  5336. const rawWidth =
  5337. right?.getBoundingClientRect?.().width || right?.clientWidth || 0;
  5338. const width = Math.max(0, Math.round(rawWidth));
  5339. const model = this.buildRightTextLines();
  5340. const next = {
  5341. secondSummary: false,
  5342. secondTags: false,
  5343. third: false,
  5344. thirdFull: false,
  5345. };
  5346. if (model.secondKind === "summary" && model.summary) {
  5347. next.secondSummary = this.measureTextOverflowByLines(
  5348. model.summary,
  5349. 3,
  5350. width
  5351. );
  5352. }
  5353. if (model.secondKind === "tags") {
  5354. next.secondTags = this.measureSingleLineOverflow(
  5355. model.secondTagsLast,
  5356. width
  5357. ); // 功能说明:tags 仅看最后一行是否溢出 by xu 20260115
  5358. }
  5359. if (model.hasThird && !model.thirdFull) {
  5360. next.third = this.measureSingleLineOverflow(model.thirdText, width);
  5361. }
  5362. if (model.hasThird && model.thirdFull) {
  5363. next.thirdFull = this.measureTextOverflowByLines(
  5364. model.thirdText,
  5365. 4,
  5366. width
  5367. );
  5368. }
  5369. const prev = this.ellipsisVisible || {};
  5370. const changed =
  5371. prev.secondSummary !== next.secondSummary ||
  5372. prev.secondTags !== next.secondTags ||
  5373. prev.third !== next.third ||
  5374. prev.thirdFull !== next.thirdFull;
  5375. if (changed) this.ellipsisVisible = next;
  5376. } catch (e) {
  5377. // ignore by xu 20260115
  5378. }
  5379. },
  5380. clearHideTextPopoverTimer() {
  5381. if (this.hideTextPopoverTimer) {
  5382. clearTimeout(this.hideTextPopoverTimer);
  5383. this.hideTextPopoverTimer = null;
  5384. }
  5385. },
  5386. hideTextPopoverLater() {
  5387. this.clearHideTextPopoverTimer();
  5388. this.hideTextPopoverTimer = setTimeout(() => {
  5389. this.showTextPopover = false;
  5390. this.textPopoverType = "";
  5391. this.textPopoverPayload = null;
  5392. }, 120);
  5393. },
  5394. showTextPopoverFor(el, kind) {
  5395. const model = this.buildRightTextLines();
  5396. const lineEl = el?.closest?.(".ss-card-text__line") || el;
  5397. const right = lineEl?.closest?.(".right") || el?.closest?.(".right");
  5398. const rawWidth =
  5399. right?.getBoundingClientRect?.().width || right?.clientWidth || 0;
  5400. const width = Math.max(0, Math.round(rawWidth));
  5401. const overflowed =
  5402. kind === "second-summary"
  5403. ? this.measureTextOverflowByLines(model.summary, 3, width)
  5404. : kind === "second-tags"
  5405. ? this.measureSingleLineOverflow(model.secondTagsLast, width)
  5406. : kind === "third"
  5407. ? this.measureSingleLineOverflow(model.thirdText, width)
  5408. : this.measureTextOverflowByLines(model.thirdText, 4, width);
  5409. if (!overflowed) return;
  5410. let payload = null;
  5411. if (kind === "second-summary") {
  5412. if (model.summary) payload = { kind, text: model.summary };
  5413. } else if (kind === "second-tags") {
  5414. if (
  5415. Array.isArray(model.secondTagsLinesFull) &&
  5416. model.secondTagsLinesFull.length
  5417. ) {
  5418. payload = { kind, lines: model.secondTagsLinesFull };
  5419. }
  5420. } else if (kind === "third") {
  5421. if (
  5422. model.thirdKind === "category" &&
  5423. Array.isArray(model.categoryLinesFull) &&
  5424. model.categoryLinesFull.length
  5425. ) {
  5426. payload = { kind, lines: model.categoryLinesFull };
  5427. } else if (model.thirdText) {
  5428. payload = { kind, text: model.thirdText };
  5429. }
  5430. } else if (kind === "third-full") {
  5431. if (
  5432. model.thirdKind === "category" &&
  5433. Array.isArray(model.categoryLinesFull) &&
  5434. model.categoryLinesFull.length
  5435. ) {
  5436. payload = { kind, lines: model.categoryLinesFull };
  5437. } else if (model.thirdText) {
  5438. payload = { kind, text: model.thirdText };
  5439. }
  5440. }
  5441. if (!payload) return;
  5442. this.clearHideTextPopoverTimer();
  5443. const container = lineEl?.closest?.(".right");
  5444. const containerRect = container?.getBoundingClientRect?.();
  5445. const lineRect = lineEl?.getBoundingClientRect?.();
  5446. if (containerRect && lineRect) {
  5447. const bottom = Math.max(
  5448. 0,
  5449. Math.round(containerRect.bottom - lineRect.bottom)
  5450. );
  5451. this.textPopoverBottom = bottom;
  5452. } else {
  5453. this.textPopoverBottom = 0;
  5454. }
  5455. this.textPopoverPayload = payload;
  5456. this.textPopoverType = kind;
  5457. this.showTextPopover = true;
  5458. },
  5459. },
  5460. mounted() {
  5461. this.$nextTick?.(() => {
  5462. requestAnimationFrame(() => this.refreshEllipsisVisible?.());
  5463. });
  5464. this.__ssCObjCardResizeHandler = () => this.refreshEllipsisVisible?.(); // 功能说明:窗口变化时刷新 ... 显示 by xu 20260115
  5465. window.addEventListener?.("resize", this.__ssCObjCardResizeHandler);
  5466. },
  5467. updated() {
  5468. this.$nextTick?.(() => {
  5469. requestAnimationFrame(() => this.refreshEllipsisVisible?.());
  5470. });
  5471. },
  5472. beforeUnmount() {
  5473. this.clearHideTextPopoverTimer?.();
  5474. if (this.__ssCObjCardResizeHandler) {
  5475. window.removeEventListener?.("resize", this.__ssCObjCardResizeHandler);
  5476. this.__ssCObjCardResizeHandler = null;
  5477. }
  5478. },
  5479. render() {
  5480. const SsCartListIcon = Vue.resolveComponent("ss-cart-list-icon");
  5481. const SsIcon = Vue.resolveComponent("ss-icon");
  5482. const hasThumbArea = !!(this.item?.thumb || this.item?.thumbType);
  5483. const thumbSrc = (() => {
  5484. // 功能说明:兼容 thumb 为 raw path 或 {val}/{value},组件内统一转为 dlByHttp URL by xu 20260122
  5485. const t = this.item?.thumb;
  5486. if (!t) return "";
  5487. if (typeof t === "string") {
  5488. const s = t.trim();
  5489. if (!s) return "";
  5490. if (
  5491. /^https?:\/\//i.test(s) ||
  5492. s.startsWith("/service?") ||
  5493. s.startsWith("/")
  5494. )
  5495. return s;
  5496. return buildThumbUrl(s);
  5497. }
  5498. return buildThumbUrl(t);
  5499. })();
  5500. return Vue.h(
  5501. "div",
  5502. {
  5503. class: {
  5504. "knowledge-item-container": true,
  5505. active: this.item.active,
  5506. [this.cardType]: !!this.cardType,
  5507. [this.statusClass]: !!this.statusClass,
  5508. },
  5509. onClick: (e) => this.onItemClick?.(e), // 功能说明:二级对象卡片点击仅查看 by xu 20260115
  5510. },
  5511. [
  5512. this.item?.statusIcons?.length > 0 &&
  5513. Vue.h(
  5514. "div",
  5515. { class: "card-status-icons" },
  5516. this.item.statusIcons.map((icon) =>
  5517. Vue.h(SsIcon, {
  5518. class: "status-icon " + icon.class,
  5519. title: icon.title,
  5520. })
  5521. )
  5522. ),
  5523. this.item?.buttons?.length > 0 &&
  5524. Vue.h(
  5525. "div",
  5526. {
  5527. class: "header",
  5528. style:
  5529. this.item?.statusIcons?.length > 0
  5530. ? {
  5531. right: String(this.item.statusIcons.length * 48) + "px",
  5532. borderTopRightRadius: "0",
  5533. }
  5534. : {},
  5535. onMouseenter: () => (this.showButtons = true),
  5536. onMouseleave: () => (this.showButtons = false),
  5537. onClick: (e) => this.onItemChange(e),
  5538. },
  5539. [
  5540. Vue.h("div", {
  5541. class: "cart-list-setting cart-list-icon",
  5542. title: this.item?.buttons?.[0]?.title,
  5543. }),
  5544. this.showButtons &&
  5545. this.item?.buttons?.length > 1 &&
  5546. Vue.h(
  5547. "div",
  5548. { class: "cart-list-button-popup" },
  5549. this.item.buttons.map((btn) =>
  5550. Vue.h(
  5551. "div",
  5552. {
  5553. onClick: (e) => {
  5554. e.stopPropagation();
  5555. btn.onclick?.();
  5556. },
  5557. },
  5558. [
  5559. btn.class &&
  5560. Vue.h(SsCartListIcon, { class: [btn.class] }),
  5561. Vue.h("span", null, btn.title),
  5562. ]
  5563. )
  5564. )
  5565. ),
  5566. ]
  5567. ),
  5568. Vue.h("div", { class: "body" }, [
  5569. Vue.h("div", { class: "box-header" }, [
  5570. Vue.h("div", String(this.item.title || "")),
  5571. ]),
  5572. Vue.h(
  5573. "div",
  5574. { class: !hasThumbArea ? "no-thumb box-body" : "box-body" },
  5575. [
  5576. hasThumbArea
  5577. ? thumbSrc
  5578. ? Vue.h("div", { class: "left" }, [
  5579. Vue.h("img", {
  5580. src: thumbSrc,
  5581. alt: "Thumbnail",
  5582. class: "imgUnHandle",
  5583. style: {
  5584. "object-fit": "cover",
  5585. width: "100%",
  5586. height: "100%",
  5587. },
  5588. }),
  5589. ])
  5590. : Vue.h(
  5591. "div",
  5592. { class: "left ss-objlist-thumbPlaceholder" },
  5593. [
  5594. Vue.h(SsIcon, {
  5595. class:
  5596. this.getBizThumbIconClass() +
  5597. " ss-objlist-thumbIcon",
  5598. }),
  5599. ]
  5600. )
  5601. : null,
  5602. Vue.h(
  5603. "div",
  5604. { class: "right" },
  5605. (() => {
  5606. const model = this.buildRightTextLines();
  5607. const hasAny = !!(model?.hasSecond || model?.hasThird);
  5608. if (!hasAny) return [];
  5609. const children = [];
  5610. if (model.hasSecond && model.secondKind === "summary") {
  5611. children.push(
  5612. Vue.h(
  5613. "div",
  5614. {
  5615. class:
  5616. "ss-card-text__line ss-card-text__secondBlock",
  5617. },
  5618. [
  5619. Vue.h(
  5620. "div",
  5621. {
  5622. class: "ss-card-text__secondSummary",
  5623. title: model.summary,
  5624. },
  5625. model.summary
  5626. ),
  5627. Vue.h("span", {
  5628. class: [
  5629. "ss-card-text__ellipsisHit",
  5630. "ss-card-text__ellipsisHit--second",
  5631. this.ellipsisVisible?.secondSummary
  5632. ? "is-on"
  5633. : "",
  5634. ],
  5635. title: "查看完整信息",
  5636. onMouseenter: (e) =>
  5637. this.showTextPopoverFor(
  5638. e?.currentTarget,
  5639. "second-summary"
  5640. ),
  5641. onClick: (e) => {
  5642. e?.stopPropagation?.();
  5643. this.showTextPopoverFor(
  5644. e?.currentTarget,
  5645. "second-summary"
  5646. );
  5647. },
  5648. onMouseleave: () => this.hideTextPopoverLater(),
  5649. }),
  5650. ]
  5651. )
  5652. );
  5653. }
  5654. if (model.hasSecond && model.secondKind === "tags") {
  5655. children.push(
  5656. Vue.h(
  5657. "div",
  5658. {
  5659. class: [
  5660. "ss-card-text__line",
  5661. model.secondFull
  5662. ? "ss-card-text__secondFullBlock"
  5663. : "ss-card-text__secondBlock",
  5664. ],
  5665. },
  5666. [
  5667. Vue.h(
  5668. "div",
  5669. { class: "ss-card-text__secondTags" },
  5670. [
  5671. ...model.secondTagsHead.map((t) =>
  5672. Vue.h(
  5673. "div",
  5674. {
  5675. class: "ss-card-text__tagLine",
  5676. title: t,
  5677. },
  5678. t
  5679. )
  5680. ),
  5681. Vue.h(
  5682. "div",
  5683. {
  5684. class:
  5685. "ss-card-text__tagLine is-last ss-card-text__tagLineLast",
  5686. title: model.secondTagsLast,
  5687. },
  5688. model.secondTagsLast
  5689. ),
  5690. ]
  5691. ),
  5692. Vue.h("span", {
  5693. class: [
  5694. "ss-card-text__ellipsisHit",
  5695. "ss-card-text__ellipsisHit--second",
  5696. this.ellipsisVisible?.secondTags ? "is-on" : "",
  5697. ],
  5698. title: "查看完整信息",
  5699. onMouseenter: (e) =>
  5700. this.showTextPopoverFor(
  5701. e?.currentTarget,
  5702. "second-tags"
  5703. ),
  5704. onClick: (e) => {
  5705. e?.stopPropagation?.();
  5706. this.showTextPopoverFor(
  5707. e?.currentTarget,
  5708. "second-tags"
  5709. );
  5710. },
  5711. onMouseleave: () => this.hideTextPopoverLater(),
  5712. }),
  5713. ]
  5714. )
  5715. );
  5716. }
  5717. if (model.hasThird) {
  5718. if (model.thirdFull) {
  5719. children.push(
  5720. Vue.h(
  5721. "div",
  5722. {
  5723. class:
  5724. "ss-card-text__line ss-card-text__thirdFullBlock",
  5725. },
  5726. [
  5727. Vue.h(
  5728. "div",
  5729. {
  5730. class: "ss-card-text__thirdFull",
  5731. title: model.thirdText,
  5732. },
  5733. model.thirdText
  5734. ),
  5735. Vue.h("span", {
  5736. class: [
  5737. "ss-card-text__ellipsisHit",
  5738. "ss-card-text__ellipsisHit--third",
  5739. this.ellipsisVisible?.thirdFull
  5740. ? "is-on"
  5741. : "",
  5742. ],
  5743. title: "查看完整信息",
  5744. onMouseenter: (e) =>
  5745. this.showTextPopoverFor(
  5746. e?.currentTarget,
  5747. "third-full"
  5748. ),
  5749. onClick: (e) => {
  5750. e?.stopPropagation?.();
  5751. this.showTextPopoverFor(
  5752. e?.currentTarget,
  5753. "third-full"
  5754. );
  5755. },
  5756. onMouseleave: () => this.hideTextPopoverLater(),
  5757. }),
  5758. ]
  5759. )
  5760. );
  5761. } else {
  5762. children.push(
  5763. Vue.h(
  5764. "div",
  5765. {
  5766. class:
  5767. "ss-card-text__line ss-card-text__thirdLineWrap",
  5768. },
  5769. [
  5770. Vue.h(
  5771. "div",
  5772. {
  5773. class: "ss-card-text__thirdLine",
  5774. title: model.thirdText,
  5775. },
  5776. model.thirdText
  5777. ),
  5778. Vue.h("span", {
  5779. class: [
  5780. "ss-card-text__ellipsisHit",
  5781. "ss-card-text__ellipsisHit--third",
  5782. this.ellipsisVisible?.third ? "is-on" : "",
  5783. ],
  5784. title: "查看完整信息",
  5785. onMouseenter: (e) =>
  5786. this.showTextPopoverFor(
  5787. e?.currentTarget,
  5788. "third"
  5789. ),
  5790. onClick: (e) => {
  5791. e?.stopPropagation?.();
  5792. this.showTextPopoverFor(
  5793. e?.currentTarget,
  5794. "third"
  5795. );
  5796. },
  5797. onMouseleave: () => this.hideTextPopoverLater(),
  5798. }),
  5799. ]
  5800. )
  5801. );
  5802. }
  5803. }
  5804. const popover =
  5805. this.showTextPopover &&
  5806. Vue.h(
  5807. "div",
  5808. {
  5809. class: "ss-card-text-popover",
  5810. style: { bottom: this.textPopoverBottom + "px" },
  5811. onMouseenter: () => {
  5812. this.clearHideTextPopoverTimer();
  5813. this.showTextPopover = true;
  5814. },
  5815. onMouseleave: () => this.hideTextPopoverLater(),
  5816. },
  5817. (() => {
  5818. const p = this.textPopoverPayload || {};
  5819. if (p.kind === "second-summary" && p.text) {
  5820. return [
  5821. Vue.h(
  5822. "div",
  5823. { class: "ss-card-text-popover__summary" },
  5824. p.text
  5825. ),
  5826. ];
  5827. }
  5828. if (Array.isArray(p.lines)) {
  5829. return [
  5830. Vue.h(
  5831. "div",
  5832. { class: "ss-card-text-popover__kvlist" },
  5833. p.lines.map((t) =>
  5834. Vue.h(
  5835. "div",
  5836. { class: "ss-card-text-popover__kv" },
  5837. t
  5838. )
  5839. )
  5840. ),
  5841. ];
  5842. }
  5843. if (
  5844. (p.kind === "third" || p.kind === "third-full") &&
  5845. p.text
  5846. ) {
  5847. return [
  5848. Vue.h(
  5849. "div",
  5850. { class: "ss-card-text-popover__objno" },
  5851. p.text
  5852. ),
  5853. ];
  5854. }
  5855. return [];
  5856. })()
  5857. );
  5858. return [
  5859. Vue.h("div", { class: "ss-card-text" }, children),
  5860. popover,
  5861. ];
  5862. })()
  5863. ),
  5864. ]
  5865. ),
  5866. ]),
  5867. ]
  5868. );
  5869. },
  5870. };
  5871. // ss-sidebar 右侧边栏(容器 + 子组件),用于 objList 右侧区域 by xu 20260106
  5872. // 组件文档补全(JSDoc) by xu 20260108
  5873. /**
  5874. * SsSidebarButtons(右侧边栏顶部按钮栏)
  5875. *
  5876. * 用途:
  5877. * - 渲染 objList 右侧顶部快捷操作(预定/入住/退房/清洁...)
  5878. * - 内部复用 `ss-search-button`(项目现有按钮样式/交互)
  5879. *
  5880. * 调用示例:
  5881. * ```html
  5882. * <ss-sidebar-buttons :items="sidebarButtons" />
  5883. * ```
  5884. *
  5885. * Props:
  5886. * - `items`: 按钮配置数组
  5887. * - `{ id, text, icon?, onClick? }`
  5888. *
  5889. * Emits:
  5890. * - `click`:点击按钮时触发,参数为按钮对象
  5891. */
  5892. const SsSidebarButtons = {
  5893. name: "SsSidebarButtons",
  5894. props: {
  5895. items: { type: Array, default: () => [] },
  5896. },
  5897. emits: ["click"],
  5898. render() {
  5899. const SsSearchButton = Vue.resolveComponent("ss-search-button");
  5900. const items = this.items || [];
  5901. if (!items.length) return null;
  5902. return Vue.h(
  5903. "div",
  5904. { class: "ss-sidebar-actions" },
  5905. items.map((btn) =>
  5906. // 顶部操作按钮复用 ss-search-button(先 mock 固定按钮) by xu 20260106
  5907. Vue.h(SsSearchButton, {
  5908. text: btn?.text ?? "",
  5909. iconClass: btn?.iconClass ?? "",
  5910. opt: btn?.opt ?? [],
  5911. checkId: btn?.checkId ?? "0",
  5912. width: btn?.width,
  5913. id: btn?.id,
  5914. onClick: (e) => {
  5915. e?.stopPropagation?.();
  5916. btn?.onClick?.(btn);
  5917. this.$emit("click", btn);
  5918. },
  5919. })
  5920. )
  5921. );
  5922. },
  5923. };
  5924. // 组件文档补全(JSDoc) by xu 20260108
  5925. /**
  5926. * SsSidebarChart(ECharts 容器渲染)
  5927. *
  5928. * 用途:
  5929. * - 仅负责 echarts init / setOption / resize / dispose
  5930. * - 被 `ss-sidebar-chart-hover` 与图表面板复用
  5931. *
  5932. * 调用示例:
  5933. * ```html
  5934. * <ss-sidebar-chart :options="option" height="220px" />
  5935. * ```
  5936. *
  5937. * Props:
  5938. * - `options`:ECharts option(Object)
  5939. * - `height`:容器高度(String)
  5940. */
  5941. const SsSidebarChart = {
  5942. name: "SsSidebarChart",
  5943. props: {
  5944. options: { type: Object, default: () => ({}) },
  5945. height: { type: String, default: "200px" },
  5946. },
  5947. setup(props) {
  5948. const elRef = Vue.ref(null);
  5949. let chart = null;
  5950. const renderChart = () => {
  5951. if (!elRef.value || !window.echarts) return;
  5952. if (!chart) {
  5953. chart = window.echarts.init(elRef.value);
  5954. }
  5955. chart.setOption(props.options || {}, true);
  5956. };
  5957. const resizeChart = () => {
  5958. chart?.resize?.();
  5959. };
  5960. Vue.onMounted(() => {
  5961. renderChart();
  5962. window.addEventListener("resize", resizeChart);
  5963. });
  5964. Vue.onBeforeUnmount(() => {
  5965. window.removeEventListener("resize", resizeChart);
  5966. chart?.dispose?.();
  5967. chart = null;
  5968. });
  5969. Vue.watch(
  5970. () => props.options,
  5971. () => {
  5972. renderChart();
  5973. },
  5974. { deep: true }
  5975. );
  5976. return { elRef };
  5977. },
  5978. render() {
  5979. return Vue.h("div", {
  5980. ref: "elRef",
  5981. style: {
  5982. width: "100%",
  5983. height: this.height,
  5984. // 图表容器不加 padding/border,由外层布局控制 by xu 20260106
  5985. background: "transparent",
  5986. border: "none",
  5987. "border-radius": "0",
  5988. },
  5989. });
  5990. },
  5991. };
  5992. // ss-sidebar-chart-hover:hover 弹出左侧大图(支持图钉/全屏) by xu 20260106
  5993. // 组件文档补全(JSDoc) by xu 20260108
  5994. /**
  5995. * SsSidebarChartHover(小图 + hover 左侧大图预览 + 图钉固定 + 全屏)
  5996. *
  5997. * 用途:
  5998. * - 右侧统计图小卡片:hover 时在左侧弹出大图预览
  5999. * - 预览头部:左侧图标+标题;右侧固定/全屏按钮(icon-base)
  6000. * - 全屏:方案A(浏览器 Fullscreen API)
  6001. *
  6002. * 调用示例(由 ss-sidebar chart panel 内部调用):
  6003. * ```html
  6004. * <ss-sidebar-chart-hover
  6005. * title="校舍建筑面积和总体分布"
  6006. * icon-class="menu-icon icon-obj-jzw"
  6007. * :options="option"
  6008. * height="220px"
  6009. * />
  6010. * ```
  6011. *
  6012. * Props:
  6013. * - `title/iconClass/icon`:用于预览/全屏 header 显示(与面板 header 一致)
  6014. * - `options`:ECharts option
  6015. * - `height`:小图高度
  6016. * - `previewWidth/previewHeight`:预览建议尺寸(会按视口自适应)
  6017. */
  6018. const SsSidebarChartHover = {
  6019. name: "SsSidebarChartHover",
  6020. props: {
  6021. // hover 大图标题/图标(与小图面板 header 一致) by xu 20260108
  6022. title: { type: String, default: "" },
  6023. iconClass: { type: String, default: "" },
  6024. icon: { type: String, default: "" },
  6025. options: { type: Object, default: () => ({}) },
  6026. height: { type: String, default: "220px" },
  6027. // hover 弹窗建议尺寸:默认 1000x650(比例由逻辑统一管控),实际渲染会按比例自适应视口 by xu 20260115
  6028. previewWidth: { type: Number, default: 1000 },
  6029. previewHeight: { type: Number, default: 650 },
  6030. },
  6031. setup(props) {
  6032. const triggerRef = Vue.ref(null);
  6033. const fullscreenRef = Vue.ref(null);
  6034. const open = Vue.ref(false);
  6035. const pinned = Vue.ref(false);
  6036. const fullscreen = Vue.ref(false);
  6037. const hoveringTrigger = Vue.ref(false);
  6038. const hoveringPreview = Vue.ref(false);
  6039. const previewStyle = Vue.ref({});
  6040. let closeTimer = null;
  6041. const updatePreviewPosition = () => {
  6042. const el = triggerRef.value;
  6043. if (!el) return;
  6044. const rect = el.getBoundingClientRect();
  6045. // 功能说明:优先使用 visualViewport(避免浏览器 UI/缩放导致可视区与 innerHeight 偏差,出现预览被遮挡约 70px) by xu 20260116
  6046. const docEl = document.documentElement;
  6047. const vv = window.visualViewport;
  6048. let viewportLeft = Number(vv?.offsetLeft ?? 0) || 0;
  6049. let viewportTop = Number(vv?.offsetTop ?? 0) || 0;
  6050. let vw =
  6051. Number(vv?.width ?? 0) ||
  6052. Number(docEl?.clientWidth || 0) ||
  6053. window.innerWidth;
  6054. let vh =
  6055. Number(vv?.height ?? 0) ||
  6056. Number(docEl?.clientHeight || 0) ||
  6057. window.innerHeight;
  6058. // 功能说明:若页面在 iframe 内,父页面可能裁切 iframe 可见区域(overflow/弹窗容器),需要用“iframe可见区域”做二次约束 by xu 20260116
  6059. try {
  6060. const inIframe = window.top && window.top !== window;
  6061. const frameEl = window.frameElement;
  6062. if (inIframe && frameEl && window.top?.document) {
  6063. const topVv = window.top.visualViewport;
  6064. const topDocEl = window.top.document.documentElement;
  6065. const topVw =
  6066. Number(topVv?.width ?? 0) ||
  6067. Number(topDocEl?.clientWidth || 0) ||
  6068. window.top.innerWidth;
  6069. const topVh =
  6070. Number(topVv?.height ?? 0) ||
  6071. Number(topDocEl?.clientHeight || 0) ||
  6072. window.top.innerHeight;
  6073. const fr = frameEl.getBoundingClientRect?.();
  6074. if (fr && fr.width > 0 && fr.height > 0) {
  6075. const visibleW = Math.max(
  6076. 0,
  6077. Math.min(fr.right, topVw) - Math.max(fr.left, 0)
  6078. );
  6079. const visibleH = Math.max(
  6080. 0,
  6081. Math.min(fr.bottom, topVh) - Math.max(fr.top, 0)
  6082. );
  6083. if (visibleW > 0) {
  6084. vw = Math.min(vw, visibleW);
  6085. viewportLeft = Math.max(0, -fr.left); // iframe内坐标系偏移(左侧被裁切时) by xu 20260116
  6086. }
  6087. if (visibleH > 0) {
  6088. vh = Math.min(vh, visibleH);
  6089. viewportTop = Math.max(0, -fr.top); // iframe内坐标系偏移(顶部被裁切时) by xu 20260116
  6090. }
  6091. }
  6092. }
  6093. } catch (_) {}
  6094. // 预览窗尺寸:优先保证“完整可见”,其次再尽量对齐右侧 header 顶部 by xu 20260116
  6095. const viewportPaddingX = 40;
  6096. const viewportPaddingTop = 20;
  6097. const viewportPaddingBottom = 10;
  6098. // 功能说明:hover 预览框不要覆盖右侧栏,优先放在 ss-sidebar 左侧;必要时动态缩小宽度 by xu 20260115
  6099. const sidebarEl = el.closest ? el.closest(".ss-sidebar") : null;
  6100. const sidebarRect = sidebarEl?.getBoundingClientRect?.();
  6101. const maxWidthByViewport = Math.max(240, vw - viewportPaddingX);
  6102. const maxWidthByLeftSpace = sidebarRect
  6103. ? Math.max(
  6104. 240,
  6105. sidebarRect.left - viewportPaddingX - 14 /* gapFromSidebar */
  6106. )
  6107. : maxWidthByViewport;
  6108. // 功能说明:预览框尺寸按比例(默认 5:3)缩放,并提供最大/最小值约束 by xu 20260115
  6109. const ratio = 3 / 5;
  6110. const minWidth = 320;
  6111. const minHeight = 240;
  6112. const maxHeightByViewport = Math.max(
  6113. minHeight,
  6114. vh - viewportPaddingTop - viewportPaddingBottom
  6115. );
  6116. const maxWidthByProp = Number(props.previewWidth) || 1000;
  6117. const maxHeightByProp = Number(props.previewHeight) || 650;
  6118. // 优先按高度撑满可视区(保证预览完整可见),再根据左侧可用宽度回退 by xu 20260116
  6119. const maxHeight = Math.min(maxHeightByProp, maxHeightByViewport);
  6120. let height = Math.max(minHeight, maxHeight);
  6121. let width = Math.round(height / ratio);
  6122. width = Math.min(
  6123. width,
  6124. maxWidthByProp,
  6125. maxWidthByViewport,
  6126. maxWidthByLeftSpace
  6127. );
  6128. width = Math.max(minWidth, width);
  6129. height = Math.round(width * ratio);
  6130. if (height > maxHeight) {
  6131. height = maxHeight;
  6132. width = Math.round(height / ratio);
  6133. }
  6134. // 默认贴着小图左侧弹出,右边缘与小图左边缘轻微重叠,避免 1px 缝隙导致 hover 闪断 by xu 20260108
  6135. const overlap = 2;
  6136. const gapFromSidebar = 14; // 功能说明:弹窗与右侧边栏留出距离(更不贴近) by xu 20260108
  6137. // 优先:贴在 sidebar 左侧(不压住右侧栏内容) by xu 20260115
  6138. let left = sidebarRect
  6139. ? sidebarRect.left - gapFromSidebar - width + overlap
  6140. : rect.left - width - gapFromSidebar + overlap;
  6141. // 如果左侧空间不足,则贴右侧(兜底,同样重叠) by xu 20260108
  6142. if (left < 0) left = rect.right + gapFromSidebar - overlap;
  6143. left = Math.max(
  6144. viewportLeft,
  6145. Math.min(left, viewportLeft + vw - width)
  6146. );
  6147. // top:优先保证完整可见,然后才贴近 header 顶部 by xu 20260116
  6148. let headerEl = el;
  6149. while (headerEl && !headerEl.classList?.contains("ss-sidebar-panel")) {
  6150. headerEl = headerEl.parentElement;
  6151. }
  6152. let headerRect = headerEl
  6153. ?.querySelector(".ss-sidebar-panel__header")
  6154. ?.getBoundingClientRect();
  6155. let top = headerRect?.top ?? rect.top;
  6156. top = Math.max(
  6157. viewportTop + viewportPaddingTop,
  6158. Math.min(top, viewportTop + vh - height - viewportPaddingBottom)
  6159. );
  6160. previewStyle.value = {
  6161. position: "fixed",
  6162. left: `${Math.round(left)}px`,
  6163. top: `${Math.round(top)}px`,
  6164. width: `${width}px`,
  6165. height: `${height}px`,
  6166. zIndex: 2147483647, // 功能说明:提高层级到接近浏览器上限,避免仍被页面固定栏/弹窗遮挡 by xu 20260116
  6167. };
  6168. };
  6169. const clearCloseTimer = () => {
  6170. if (closeTimer) {
  6171. clearTimeout(closeTimer);
  6172. closeTimer = null;
  6173. }
  6174. };
  6175. const scheduleClose = () => {
  6176. clearCloseTimer();
  6177. if (pinned.value || fullscreen.value) return;
  6178. if (hoveringTrigger.value || hoveringPreview.value) return; // 功能:鼠标在小图/大图之间移动不关闭 by xu 20260108
  6179. closeTimer = setTimeout(() => {
  6180. open.value = false;
  6181. }, 100);
  6182. };
  6183. const openPreview = () => {
  6184. clearCloseTimer();
  6185. updatePreviewPosition();
  6186. open.value = true;
  6187. };
  6188. const togglePin = () => {
  6189. pinned.value = !pinned.value;
  6190. if (pinned.value) {
  6191. open.value = true;
  6192. updatePreviewPosition();
  6193. }
  6194. };
  6195. const toggleFullscreen = () => {
  6196. // 全屏:采用浏览器 Fullscreen API(方案A),不使用遮罩弹窗 by xu 20260108
  6197. if (!fullscreen.value) {
  6198. open.value = false; // 避免同时渲染预览与全屏 by xu 20260108
  6199. fullscreen.value = true;
  6200. Vue.nextTick(() => {
  6201. const el = fullscreenRef.value;
  6202. if (el?.requestFullscreen) {
  6203. el.requestFullscreen().catch(() => {
  6204. // requestFullscreen 失败则回退为非全屏状态 by xu 20260108
  6205. fullscreen.value = false;
  6206. });
  6207. } else {
  6208. fullscreen.value = false;
  6209. }
  6210. });
  6211. } else {
  6212. if (document?.exitFullscreen) {
  6213. document.exitFullscreen().catch(() => {});
  6214. }
  6215. }
  6216. };
  6217. const handleFullscreenChange = () => {
  6218. const isFs = !!document.fullscreenElement;
  6219. fullscreen.value = isFs; // 功能说明:同步 ESC/系统退出全屏状态 by xu 20260108
  6220. if (isFs) {
  6221. open.value = false;
  6222. clearCloseTimer();
  6223. return;
  6224. }
  6225. // 退出全屏后:若固定或仍 hover,则恢复预览,否则关闭 by xu 20260108
  6226. if (pinned.value || hoveringTrigger.value || hoveringPreview.value) {
  6227. open.value = true;
  6228. updatePreviewPosition();
  6229. } else {
  6230. open.value = false;
  6231. }
  6232. };
  6233. Vue.onMounted(() => {
  6234. window.addEventListener("resize", updatePreviewPosition);
  6235. window.addEventListener("scroll", updatePreviewPosition, true);
  6236. document.addEventListener("fullscreenchange", handleFullscreenChange); // 功能说明:监听全屏状态变化 by xu 20260108
  6237. });
  6238. Vue.onBeforeUnmount(() => {
  6239. clearCloseTimer();
  6240. window.removeEventListener("resize", updatePreviewPosition);
  6241. window.removeEventListener("scroll", updatePreviewPosition, true);
  6242. document.removeEventListener(
  6243. "fullscreenchange",
  6244. handleFullscreenChange
  6245. );
  6246. });
  6247. return {
  6248. triggerRef,
  6249. fullscreenRef,
  6250. open,
  6251. pinned,
  6252. fullscreen,
  6253. hoveringTrigger,
  6254. hoveringPreview,
  6255. previewStyle,
  6256. openPreview,
  6257. scheduleClose,
  6258. clearCloseTimer,
  6259. togglePin,
  6260. toggleFullscreen,
  6261. };
  6262. },
  6263. render() {
  6264. const SsIcon = Vue.resolveComponent("ss-icon");
  6265. const Chart = Vue.resolveComponent("ss-sidebar-chart");
  6266. const hasHeader = !!(this.title || this.iconClass || this.icon); // 功能:hover 大图显示左侧图标+标题 by xu 20260108
  6267. const previewContent = Vue.h(
  6268. "div",
  6269. {
  6270. class: { "ss-sidebar-chart-preview": true, "is-pinned": this.pinned },
  6271. style: this.previewStyle,
  6272. onMouseenter: () => {
  6273. this.hoveringPreview = true;
  6274. this.clearCloseTimer();
  6275. },
  6276. onMouseleave: () => {
  6277. this.hoveringPreview = false;
  6278. this.scheduleClose();
  6279. },
  6280. },
  6281. [
  6282. hasHeader
  6283. ? Vue.h(
  6284. "div",
  6285. {
  6286. class:
  6287. "ss-sidebar-panel__header ss-sidebar-chart-preview__header",
  6288. },
  6289. [
  6290. Vue.h("div", { class: "ss-sidebar-panel__title" }, [
  6291. this.iconClass
  6292. ? Vue.h(SsIcon, {
  6293. class: this.iconClass + " ss-sidebar-panel__icon",
  6294. })
  6295. : this.icon
  6296. ? Vue.h(SsIcon, {
  6297. name: this.icon,
  6298. size: "16px",
  6299. class: "ss-sidebar-panel__icon",
  6300. })
  6301. : null,
  6302. Vue.h("span", null, this.title || "统计图"),
  6303. ]),
  6304. Vue.h("div", { class: "ss-sidebar-panel__tools" }, [
  6305. Vue.h(
  6306. "button",
  6307. {
  6308. type: "button",
  6309. class: {
  6310. "ss-sidebar-chart-tool": true,
  6311. "is-active": this.pinned,
  6312. },
  6313. title: this.pinned ? "取消固定" : "固定",
  6314. onClick: (e) => {
  6315. e.stopPropagation();
  6316. this.togglePin();
  6317. },
  6318. },
  6319. // 功能说明:右侧栏 hover 工具图标使用 ss-sidebar-base-icon(不复用左侧 menu-base-icon) by xu 20260123
  6320. [
  6321. Vue.h(SsIcon, {
  6322. class: this.pinned
  6323. ? "ss-sidebar-base-icon icon-fix-bold"
  6324. : "ss-sidebar-base-icon icon-fix",
  6325. }),
  6326. ]
  6327. ),
  6328. Vue.h(
  6329. "button",
  6330. {
  6331. type: "button",
  6332. class: "ss-sidebar-chart-tool",
  6333. title: "全屏",
  6334. onClick: (e) => {
  6335. e.stopPropagation();
  6336. this.toggleFullscreen();
  6337. },
  6338. },
  6339. [
  6340. Vue.h(SsIcon, {
  6341. class: this.fullscreen
  6342. ? "ss-sidebar-base-icon icon-fs-exit"
  6343. : "ss-sidebar-base-icon icon-fs",
  6344. }),
  6345. ]
  6346. ),
  6347. ]),
  6348. ]
  6349. )
  6350. : Vue.h(
  6351. "div",
  6352. {
  6353. class:
  6354. "ss-sidebar-chart-preview__header ss-sidebar-chart-preview__header--simple",
  6355. },
  6356. [
  6357. Vue.h("div", { class: "ss-sidebar-panel__tools" }, [
  6358. Vue.h(
  6359. "button",
  6360. {
  6361. type: "button",
  6362. class: {
  6363. "ss-sidebar-chart-tool": true,
  6364. "is-active": this.pinned,
  6365. },
  6366. title: this.pinned ? "取消固定" : "固定",
  6367. onClick: (e) => {
  6368. e.stopPropagation();
  6369. this.togglePin();
  6370. },
  6371. },
  6372. [
  6373. Vue.h(SsIcon, {
  6374. class: this.pinned
  6375. ? "ss-sidebar-base-icon icon-fix-bold"
  6376. : "ss-sidebar-base-icon icon-fix",
  6377. }),
  6378. ]
  6379. ),
  6380. Vue.h(
  6381. "button",
  6382. {
  6383. type: "button",
  6384. class: "ss-sidebar-chart-tool",
  6385. title: "全屏",
  6386. onClick: (e) => {
  6387. e.stopPropagation();
  6388. this.toggleFullscreen();
  6389. },
  6390. },
  6391. [
  6392. Vue.h(SsIcon, {
  6393. class: this.fullscreen
  6394. ? "ss-sidebar-base-icon icon-chk-on"
  6395. : "ss-sidebar-base-icon icon-chk",
  6396. }),
  6397. ]
  6398. ),
  6399. ]),
  6400. ]
  6401. ),
  6402. Vue.h("div", { class: "ss-sidebar-chart-preview__body" }, [
  6403. Vue.h(Chart, { options: this.options, height: "100%" }),
  6404. ]),
  6405. ]
  6406. );
  6407. const fullscreenContent =
  6408. this.fullscreen &&
  6409. Vue.h(
  6410. "div",
  6411. {
  6412. ref: "fullscreenRef",
  6413. class: "ss-sidebar-chart-fullscreen",
  6414. },
  6415. [
  6416. Vue.h(
  6417. "div",
  6418. {
  6419. class:
  6420. "ss-sidebar-panel__header ss-sidebar-chart-fullscreen__header",
  6421. },
  6422. [
  6423. Vue.h("div", { class: "ss-sidebar-panel__title" }, [
  6424. this.iconClass
  6425. ? Vue.h(SsIcon, {
  6426. class: this.iconClass + " ss-sidebar-panel__icon",
  6427. })
  6428. : this.icon
  6429. ? Vue.h(SsIcon, {
  6430. name: this.icon,
  6431. size: "16px",
  6432. class: "ss-sidebar-panel__icon",
  6433. })
  6434. : null,
  6435. Vue.h("span", null, this.title || "统计图"),
  6436. ]),
  6437. Vue.h("div", { class: "ss-sidebar-panel__tools" }, [
  6438. Vue.h(
  6439. "button",
  6440. {
  6441. type: "button",
  6442. class: {
  6443. "ss-sidebar-chart-tool": true,
  6444. "is-active": this.pinned,
  6445. },
  6446. title: this.pinned ? "取消固定" : "固定",
  6447. onClick: (e) => {
  6448. e.stopPropagation();
  6449. this.togglePin();
  6450. },
  6451. },
  6452. [
  6453. Vue.h(SsIcon, {
  6454. class: this.pinned
  6455. ? "ss-sidebar-base-icon icon-fix-bold"
  6456. : "ss-sidebar-base-icon icon-fix",
  6457. }),
  6458. ]
  6459. ),
  6460. Vue.h(
  6461. "button",
  6462. {
  6463. type: "button",
  6464. class: "ss-sidebar-chart-tool",
  6465. title: "退出全屏",
  6466. onClick: (e) => {
  6467. e.stopPropagation();
  6468. this.toggleFullscreen();
  6469. },
  6470. },
  6471. [
  6472. Vue.h(SsIcon, {
  6473. class: "ss-sidebar-base-icon icon-fs-exit",
  6474. }),
  6475. ]
  6476. ),
  6477. ]),
  6478. ]
  6479. ),
  6480. Vue.h("div", { class: "ss-sidebar-chart-fullscreen__body" }, [
  6481. Vue.h(Chart, { options: this.options, height: "100%" }),
  6482. ]),
  6483. ]
  6484. );
  6485. return Vue.h("div", { class: "ss-sidebar-chart-hover" }, [
  6486. Vue.h(
  6487. "div",
  6488. {
  6489. ref: "triggerRef",
  6490. class: "ss-sidebar-chart-hover__trigger",
  6491. onMouseenter: () => {
  6492. this.hoveringTrigger = true;
  6493. this.openPreview();
  6494. },
  6495. onMouseleave: () => {
  6496. this.hoveringTrigger = false;
  6497. this.scheduleClose();
  6498. },
  6499. },
  6500. [Vue.h(Chart, { options: this.options, height: this.height })]
  6501. ),
  6502. (this.open || this.fullscreen) &&
  6503. Vue.h(Vue.Teleport, { to: "body" }, [
  6504. this.open ? previewContent : null,
  6505. fullscreenContent || null,
  6506. ]),
  6507. ]);
  6508. },
  6509. };
  6510. // 组件文档补全(JSDoc) by xu 20260108
  6511. /**
  6512. * SsSidebarList(右侧业务面板:人员/已选/服务/预定...)
  6513. *
  6514. * 用途:
  6515. * - 统一渲染面板 header(图标/标题/数量/右侧按钮)
  6516. * - 统一渲染 list(固定行高、hover、右侧移除按钮等)
  6517. *
  6518. * 调用示例(由 ss-sidebar 通过 panels 配置驱动):
  6519. * ```js
  6520. * { type:'list', title:'已选', iconClass:'menu-icon icon-obj-xcd', mode:'selected', items:selectedItems, closable:true }
  6521. * ```
  6522. *
  6523. * Props(核心):
  6524. * - `title`:header 标题
  6525. * - `iconClass/icon`:header 图标(优先 iconClass)
  6526. * - `count`:数量回显(图表面板可不传)
  6527. * - `closable`:是否显示“清空”按钮(触发 emit clear)
  6528. * - `headerFilters`:header 条件数组(组件名 + props),用于联调接口搜索
  6529. * - `headerSearchButton`:是否显示搜索按钮(触发 emit search)
  6530. * - `items`:列表数据
  6531. * - `mode`:`selected` 时右侧按钮语义为“移除”
  6532. * - `itemLayout`:`simple` / `person`(人员号槽位)
  6533. * - `itemAction`:是否显示 item 右侧操作按钮(hover 才出现)
  6534. *
  6535. * Emits:
  6536. * - `remove(item)`:点击 item 右侧移除
  6537. * - `clear()`:点击 header 清空
  6538. * - `search({keyword, filters})`:点击 header 搜索
  6539. */
  6540. const SsSidebarList = {
  6541. name: "SsSidebarList",
  6542. props: {
  6543. title: { type: String, default: "" },
  6544. // header 图标:优先使用 iconClass(走 ss-icon v3.0 class 分支) by xu 20260106
  6545. iconClass: { type: String, default: "" },
  6546. icon: { type: String, default: "" }, // 兼容旧写法(ss-icon name)
  6547. count: { type: [Number, String], default: "" },
  6548. // 选中类分区:右侧关闭按钮=清空分区数据 by xu 20260106
  6549. closable: { type: Boolean, default: false },
  6550. searchable: { type: Boolean, default: false },
  6551. // 搜索框是否放在 header 内(人员块需要该布局) by xu 20260106
  6552. searchInHeader: { type: Boolean, default: false },
  6553. // header 搜索:下拉条件 + 搜索按钮(适合“人员”块) by xu 20260106
  6554. headerFilters: { type: Array, default: () => [] },
  6555. headerSearchButton: { type: Boolean, default: false },
  6556. searchPlaceholder: { type: String, default: "搜索" },
  6557. // 列表项布局:simple(仅标题) / person(标题+人员号槽位) by xu 20260106
  6558. itemLayout: { type: String, default: "simple" },
  6559. itemAction: { type: Boolean, default: true },
  6560. collapsible: { type: Boolean, default: true }, // 功能说明:是否允许双击 header 折叠/展开 by xu 20260116
  6561. collapsed: { type: Boolean, default: false }, // 功能说明:折叠态仅展示 header by xu 20260116
  6562. items: { type: Array, default: () => [] },
  6563. mode: { type: String, default: "search" }, // search / selected
  6564. },
  6565. emits: ["select", "remove", "clear", "search", "toggle-collapse"],
  6566. data() {
  6567. return {
  6568. keyword: "",
  6569. filterValues: {},
  6570. }; // 功能说明:折叠状态完全由 props.collapsed 驱动,避免多面板复用导致状态不同步 by xu 20260116
  6571. },
  6572. created() {
  6573. // header 下拉条件默认值初始化 by xu 20260106
  6574. (this.headerFilters || []).forEach((f) => {
  6575. if (!f || !f.key) return;
  6576. if (this.filterValues[f.key] !== undefined) return;
  6577. const first = f?.options?.[0]?.value ?? "";
  6578. this.filterValues[f.key] = f.value !== undefined ? f.value : first;
  6579. });
  6580. },
  6581. methods: {
  6582. __shouldIgnoreHeaderToggle(e) {
  6583. // 功能说明:忽略工具区/输入区触发折叠,避免误触 by xu 20260116
  6584. const t = e?.target;
  6585. if (!t || !t.closest) return false;
  6586. if (t.closest(".ss-sidebar-panel__tools")) return true;
  6587. if (t.closest(".ss-sidebar-panel__filters")) return true;
  6588. if (t.closest("input,textarea,select,button")) return true;
  6589. return false;
  6590. },
  6591. __toggleCollapseInternal(e, source) {
  6592. if (!this.collapsible) return;
  6593. if (this.__shouldIgnoreHeaderToggle(e)) return;
  6594. const nextCollapsed = !this.collapsed;
  6595. console.log("[SsSidebarList] toggle emit", {
  6596. title: this.title,
  6597. source,
  6598. to: nextCollapsed,
  6599. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  6600. // 功能说明:由父组件(SsSidebar.toggleSectionCollapse)统一控制 section 高度与折叠数组 by xu 20260116
  6601. this.$emit("toggle-collapse");
  6602. },
  6603. },
  6604. render() {
  6605. const items = this.items || [];
  6606. const SsIcon = Vue.resolveComponent("ss-icon");
  6607. const isSelectedMode = this.mode === "selected";
  6608. const activeKeyword = this.filterValues?.keyword ?? this.keyword; // 功能:keyword 优先取 headerFilters.keyword by xu 20260106
  6609. const hasHeaderKeyword = (this.headerFilters || []).some(
  6610. (f) => f?.key === "keyword"
  6611. ); // 功能:header 内 keyword 过滤 by xu 20260106
  6612. const renderHeaderFilter = (f) => {
  6613. if (!f) return null;
  6614. const key = f.key;
  6615. const componentName = f.component;
  6616. if (!key || !componentName) return null;
  6617. const Comp = Vue.resolveComponent(componentName);
  6618. if (!Comp) return null;
  6619. const modelValue = this.filterValues[key];
  6620. const props = f.props || {};
  6621. return Vue.h(Comp, {
  6622. ...props,
  6623. modelValue,
  6624. "onUpdate:modelValue": (v) => {
  6625. this.filterValues[key] = v;
  6626. },
  6627. });
  6628. };
  6629. const filteredItems =
  6630. this.searchable && activeKeyword
  6631. ? items.filter((it) =>
  6632. String(it?.title ?? "")
  6633. .toLowerCase()
  6634. .includes(String(activeKeyword).toLowerCase())
  6635. )
  6636. : items;
  6637. if (!filteredItems.length && !this.title) return null;
  6638. if (this.collapsed) {
  6639. return Vue.h("div", { class: "ss-sidebar-panel" }, [
  6640. this.title
  6641. ? Vue.h(
  6642. "div",
  6643. {
  6644. class: "ss-sidebar-panel__header",
  6645. // 功能说明:折叠触发绑定到整个 header(dblclick + click.detail==2 兜底) by xu 20260116
  6646. onDblclick: (e) => {
  6647. e?.preventDefault?.();
  6648. e?.stopPropagation?.();
  6649. console.log("[SsSidebarList] header dblclick", {
  6650. title: this.title,
  6651. collapsed: true,
  6652. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  6653. this.__toggleCollapseInternal(e, "dblclick");
  6654. },
  6655. // 功能说明:移除 click.detail==2 兜底,避免双击同时触发 click+dblclick 导致“折叠又立刻展开” by xu 20260116
  6656. },
  6657. [
  6658. Vue.h("div", { class: "ss-sidebar-panel__title" }, [
  6659. this.iconClass
  6660. ? Vue.h(SsIcon, {
  6661. class: this.iconClass + " ss-sidebar-panel__icon",
  6662. })
  6663. : this.icon
  6664. ? Vue.h(SsIcon, {
  6665. name: this.icon,
  6666. size: "16px",
  6667. class: "ss-sidebar-panel__icon",
  6668. })
  6669. : null,
  6670. Vue.h("span", null, this.title),
  6671. this.count !== ""
  6672. ? Vue.h(
  6673. "span",
  6674. { class: "ss-sidebar-panel__count" },
  6675. `(${this.count})`
  6676. )
  6677. : null,
  6678. ]),
  6679. Vue.h("div", { class: "ss-sidebar-panel__tools" }, [
  6680. this.closable
  6681. ? Vue.h(
  6682. "button",
  6683. {
  6684. type: "button",
  6685. class: "ss-sidebar-icon-btn ss-sidebar-header-btn",
  6686. title: "清空",
  6687. onClick: (e) => {
  6688. e.stopPropagation();
  6689. this.$emit("clear");
  6690. },
  6691. },
  6692. [
  6693. Vue.h(SsIcon, {
  6694. class: "ss-sidebar-base-icon icon-cl",
  6695. }),
  6696. ] // 功能说明:右侧栏“已选”清空按钮图标使用 ss-sidebar-base-icon by xu 20260123
  6697. )
  6698. : null,
  6699. ]),
  6700. ]
  6701. )
  6702. : null,
  6703. ]);
  6704. }
  6705. return Vue.h("div", { class: "ss-sidebar-panel" }, [
  6706. this.title
  6707. ? Vue.h(
  6708. "div",
  6709. {
  6710. class: "ss-sidebar-panel__header",
  6711. // 功能说明:折叠触发绑定到整个 header(dblclick + click.detail==2 兜底) by xu 20260116
  6712. onDblclick: (e) => {
  6713. e?.preventDefault?.();
  6714. e?.stopPropagation?.();
  6715. console.log("[SsSidebarList] header dblclick", {
  6716. title: this.title,
  6717. collapsed: false,
  6718. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  6719. this.__toggleCollapseInternal(e, "dblclick");
  6720. },
  6721. // 功能说明:移除 click.detail==2 兜底,避免双击同时触发 click+dblclick 导致“折叠又立刻展开” by xu 20260116
  6722. },
  6723. [
  6724. Vue.h("div", { class: "ss-sidebar-panel__title" }, [
  6725. // 图标 + 标题(每个分区都有) by xu 20260106
  6726. this.iconClass
  6727. ? Vue.h(SsIcon, {
  6728. class: this.iconClass + " ss-sidebar-panel__icon",
  6729. })
  6730. : this.icon
  6731. ? Vue.h(SsIcon, {
  6732. name: this.icon,
  6733. size: "16px",
  6734. class: "ss-sidebar-panel__icon",
  6735. })
  6736. : null,
  6737. Vue.h("span", null, this.title),
  6738. // 数量回显:图表分区可不传 count by xu 20260106
  6739. this.count !== ""
  6740. ? Vue.h(
  6741. "span",
  6742. { class: "ss-sidebar-panel__count" },
  6743. `(${this.count})`
  6744. )
  6745. : null,
  6746. ]),
  6747. Vue.h("div", { class: "ss-sidebar-panel__tools" }, [
  6748. // header 条件(例如下拉框)+ 右侧搜索按钮 by xu 20260106
  6749. this.headerFilters?.length
  6750. ? Vue.h(
  6751. "div",
  6752. { class: "ss-sidebar-panel__filters" },
  6753. this.headerFilters
  6754. .map(renderHeaderFilter)
  6755. .filter(Boolean)
  6756. )
  6757. : null,
  6758. this.headerSearchButton
  6759. ? Vue.h(
  6760. "button",
  6761. {
  6762. type: "button",
  6763. class: "ss-sidebar-icon-btn",
  6764. title: "搜索",
  6765. onClick: (e) => {
  6766. e.stopPropagation();
  6767. this.$emit("search", {
  6768. // headerFilters 内也可能包含 keyword by xu 20260106
  6769. keyword: activeKeyword,
  6770. filters: { ...(this.filterValues || {}) },
  6771. });
  6772. },
  6773. },
  6774. [Vue.h(SsIcon, { name: "search", size: "14px" })]
  6775. )
  6776. : null,
  6777. // 人员块:搜索框在 header 内 by xu 20260106
  6778. this.searchable &&
  6779. this.searchInHeader &&
  6780. !this.headerSearchButton
  6781. ? Vue.h(
  6782. "div",
  6783. { class: "ss-sidebar-panel__searchInline" },
  6784. [
  6785. Vue.h(
  6786. "div",
  6787. { class: "ss-sidebar-search is-inline" },
  6788. [
  6789. Vue.h(SsIcon, {
  6790. name: "search",
  6791. size: "14px",
  6792. class: "ss-sidebar-search__prefix",
  6793. }),
  6794. Vue.h("input", {
  6795. class: "ss-sidebar-search__input",
  6796. value: this.keyword,
  6797. placeholder: this.searchPlaceholder,
  6798. onInput: (e) => {
  6799. this.keyword = e?.target?.value ?? "";
  6800. },
  6801. }),
  6802. ]
  6803. ),
  6804. ]
  6805. )
  6806. : null,
  6807. this.closable
  6808. ? Vue.h(
  6809. "button",
  6810. {
  6811. type: "button",
  6812. class: "ss-sidebar-icon-btn ss-sidebar-header-btn",
  6813. title: "清空",
  6814. onClick: (e) => {
  6815. e.stopPropagation();
  6816. this.$emit("clear");
  6817. },
  6818. },
  6819. // 清空按钮使用 icon-base 的 icon-cl by xu 20260106
  6820. [
  6821. Vue.h(SsIcon, {
  6822. class: "ss-sidebar-base-icon icon-cl",
  6823. }),
  6824. ] // 功能说明:右侧栏清空按钮图标使用 ss-sidebar-base-icon by xu 20260123
  6825. )
  6826. : null,
  6827. ]),
  6828. ]
  6829. )
  6830. : null,
  6831. // 非 header 内搜索:独立一行 by xu 20260106
  6832. // headerSearchButton/headerFilters 已覆盖搜索能力时,不再额外渲染独立搜索行 by xu 20260106
  6833. this.searchable &&
  6834. !this.searchInHeader &&
  6835. !this.headerSearchButton &&
  6836. !hasHeaderKeyword
  6837. ? Vue.h("div", { class: "ss-sidebar-panel__search" }, [
  6838. Vue.h("div", { class: "ss-sidebar-search" }, [
  6839. Vue.h(SsIcon, {
  6840. name: "search",
  6841. size: "14px",
  6842. class: "ss-sidebar-search__prefix",
  6843. }),
  6844. Vue.h("input", {
  6845. class: "ss-sidebar-search__input",
  6846. value: this.keyword,
  6847. placeholder: this.searchPlaceholder,
  6848. onInput: (e) => {
  6849. this.keyword = e?.target?.value ?? "";
  6850. },
  6851. }),
  6852. ]),
  6853. ])
  6854. : null,
  6855. Vue.h(
  6856. "div",
  6857. { class: "ss-sidebar-list" },
  6858. filteredItems.map((item, idx) => {
  6859. const title = item?.title ?? "";
  6860. const tags = item?.tags || [];
  6861. const isPersonLayout = this.itemLayout === "person";
  6862. const hasTags = !isPersonLayout && tags?.length > 0; // 列表项垂直对齐:有 tags 顶对齐 by xu 20260106
  6863. return Vue.h(
  6864. "div",
  6865. {
  6866. class: {
  6867. "ss-sidebar-list-item": true,
  6868. "is-first": idx === 0,
  6869. "is-person": isPersonLayout,
  6870. "has-tags": hasTags,
  6871. },
  6872. },
  6873. [
  6874. Vue.h("div", { class: "ss-sidebar-list-item__main" }, [
  6875. Vue.h(
  6876. "div",
  6877. { class: "ss-sidebar-list-item__title" },
  6878. Vue.h(
  6879. "span",
  6880. {
  6881. style: {
  6882. "white-space": "nowrap",
  6883. overflow: "hidden",
  6884. "text-overflow": "ellipsis",
  6885. },
  6886. },
  6887. title
  6888. )
  6889. ),
  6890. // 非人员布局才显示 tags by xu 20260106
  6891. !isPersonLayout && tags?.length
  6892. ? Vue.h(
  6893. "div",
  6894. { class: "ss-sidebar-list-item__tags" },
  6895. tags.map((tag) => {
  6896. const [k, v] = Object.entries(tag)[0] || ["", ""];
  6897. return Vue.h(
  6898. "span",
  6899. { class: "ss-sidebar-tag", title: `${k}: ${v}` },
  6900. `${k}: ${v}`
  6901. );
  6902. })
  6903. )
  6904. : null,
  6905. ]),
  6906. // 人员布局:中间保留“人员号”槽位 by xu 20260106
  6907. isPersonLayout
  6908. ? Vue.h(
  6909. "div",
  6910. {
  6911. class: "ss-sidebar-list-item__meta",
  6912. title: String(item?.meta ?? ""),
  6913. },
  6914. item?.meta ?? ""
  6915. )
  6916. : null,
  6917. this.itemAction
  6918. ? Vue.h(
  6919. "button",
  6920. {
  6921. type: "button",
  6922. class: {
  6923. // item 操作按钮:默认无背景/无边框,hover 才高亮 by xu 20260106
  6924. "ss-sidebar-item-btn": true,
  6925. },
  6926. title: isSelectedMode ? "移除" : "选择",
  6927. onClick: (e) => {
  6928. e.stopPropagation();
  6929. if (isSelectedMode) this.$emit("remove", item);
  6930. else this.$emit("select", item);
  6931. },
  6932. },
  6933. [
  6934. // item 移除图标使用 icon-base 的 icon-cl by xu 20260106
  6935. isSelectedMode
  6936. ? Vue.h(SsIcon, {
  6937. class: "ss-sidebar-base-icon icon-cl",
  6938. }) // 功能说明:右侧栏 item 移除图标使用 ss-sidebar-base-icon by xu 20260123
  6939. : Vue.h(SsIcon, { name: "check", size: "14px" }),
  6940. ]
  6941. )
  6942. : null,
  6943. ]
  6944. );
  6945. })
  6946. ),
  6947. ]);
  6948. },
  6949. };
  6950. // ss-sidebar-report-table:右侧“统计表/报表”面板(pstatList grtjlbm=51 聚拢渲染) by xu 20260115
  6951. const SsSidebarReportTable = {
  6952. name: "SsSidebarReportTable",
  6953. props: {
  6954. title: { type: String, default: "" },
  6955. iconClass: { type: String, default: "" },
  6956. icon: { type: String, default: "" },
  6957. items: { type: Array, default: () => [] }, // pstatList(grtjlbm=51) 数组
  6958. onOpen: { type: Function, default: null }, // (srv, ctx) => void
  6959. collapsible: { type: Boolean, default: true }, // 功能说明:是否允许双击 header 折叠/展开 by xu 20260116
  6960. collapsed: { type: Boolean, default: false }, // 功能说明:折叠态仅展示 header by xu 20260116
  6961. },
  6962. emits: ["open", "toggle-collapse"],
  6963. data() {
  6964. return {}; // 功能说明:折叠状态完全由 props.collapsed 驱动 by xu 20260116
  6965. },
  6966. methods: {
  6967. __toggleCollapseInternal(e, source) {
  6968. if (!this.collapsible) return;
  6969. const next = !this.collapsed;
  6970. console.log("[SsSidebarReportTable] toggle emit", {
  6971. title: this.title,
  6972. source,
  6973. to: next,
  6974. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  6975. this.$emit("toggle-collapse");
  6976. },
  6977. },
  6978. render() {
  6979. const SsIcon = Vue.resolveComponent("ss-icon");
  6980. const list = this.items || [];
  6981. if (!this.title && !list.length) return null;
  6982. const header = this.title
  6983. ? Vue.h(
  6984. "div",
  6985. {
  6986. class: "ss-sidebar-panel__header",
  6987. // 功能说明:折叠触发绑定到整个 header(dblclick + click.detail==2 兜底) by xu 20260116
  6988. onDblclick: (e) => {
  6989. e?.preventDefault?.();
  6990. e?.stopPropagation?.();
  6991. console.log("[SsSidebarReportTable] header dblclick", {
  6992. title: this.title,
  6993. collapsed: this.collapsed,
  6994. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  6995. this.__toggleCollapseInternal(e, "dblclick");
  6996. },
  6997. // 功能说明:移除 click.detail==2 兜底,避免双击同时触发 click+dblclick 导致“折叠又立刻展开” by xu 20260116
  6998. },
  6999. [
  7000. Vue.h("div", { class: "ss-sidebar-panel__title" }, [
  7001. this.iconClass
  7002. ? Vue.h(SsIcon, {
  7003. class: this.iconClass + " ss-sidebar-panel__icon",
  7004. })
  7005. : this.icon
  7006. ? Vue.h(SsIcon, {
  7007. name: this.icon,
  7008. size: "16px",
  7009. class: "ss-sidebar-panel__icon",
  7010. })
  7011. : null,
  7012. Vue.h("span", null, this.title),
  7013. ]),
  7014. Vue.h("div", { class: "ss-sidebar-panel__tools" }),
  7015. ]
  7016. )
  7017. : null;
  7018. const renderReport = (report) => {
  7019. const title = String(report?.mc ?? "");
  7020. const mx = Array.isArray(report?.grtjmxList) ? report.grtjmxList : [];
  7021. if (!title && !mx.length) return null;
  7022. // 功能说明:每个报表对象渲染为一个 table(有边框、无圆角、表间距10px;样式由 base.css 统一控制) by xu 20260115
  7023. const cols = Math.max(1, mx.length);
  7024. // 功能说明:table 外层包一层 wrap,子项过多时支持横向滚动 by xu 20260115
  7025. return Vue.h("div", { class: "ss-sidebar-report-table-wrap" }, [
  7026. Vue.h("table", { class: "ss-sidebar-report-table" }, [
  7027. Vue.h("thead", null, [
  7028. Vue.h("tr", null, [
  7029. Vue.h(
  7030. "th",
  7031. { class: "ss-sidebar-report-table__title", colspan: cols },
  7032. Vue.h(
  7033. "div",
  7034. { class: "ss-sidebar-report-table__title-content" },
  7035. [
  7036. Vue.h("span", { class: "ss-sidebar-report-table__dot" }),
  7037. Vue.h(
  7038. "span",
  7039. { class: "ss-sidebar-report-table__title-text", title },
  7040. title
  7041. ),
  7042. ]
  7043. )
  7044. ),
  7045. ]),
  7046. ]),
  7047. Vue.h("tbody", null, [
  7048. Vue.h(
  7049. "tr",
  7050. null,
  7051. mx.map((cell) => {
  7052. const text = String(cell?.mc ?? "");
  7053. const srv = {
  7054. servName: cell?.fwm ?? "",
  7055. dest: cell?.bjm ?? "",
  7056. title: text,
  7057. width: cell?.width,
  7058. height: cell?.height,
  7059. minHeight: cell?.height,
  7060. maxHeight: cell?.height,
  7061. showTitle: text,
  7062. };
  7063. return Vue.h(
  7064. "td",
  7065. {
  7066. class: "ss-sidebar-report-table__cell",
  7067. title: text,
  7068. onClick: (e) => {
  7069. e?.stopPropagation?.();
  7070. try {
  7071. this.onOpen?.(srv, { report, cell });
  7072. } catch (_) {}
  7073. this.$emit("open", { report, cell, srv });
  7074. },
  7075. },
  7076. text
  7077. );
  7078. })
  7079. ),
  7080. ]),
  7081. ]),
  7082. ]);
  7083. };
  7084. // 功能说明:报表面板增加独立 class,便于 base.css 统一控制 padding/间距 by xu 20260115
  7085. return Vue.h(
  7086. "div",
  7087. { class: "ss-sidebar-panel ss-sidebar-report-panel" },
  7088. [
  7089. header,
  7090. this.collapsed
  7091. ? null
  7092. : Vue.h(
  7093. "div",
  7094. // 功能说明:报表列表滚动/高度样式下沉到 base.css,避免写在 DOM 上 by xu 20260115
  7095. { class: "ss-sidebar-report__list" },
  7096. list.map(renderReport).filter(Boolean)
  7097. ),
  7098. ]
  7099. );
  7100. },
  7101. };
  7102. // 组件文档补全(JSDoc) by xu 20260108
  7103. /**
  7104. * SsSidebar(objList 右侧边栏容器)
  7105. *
  7106. * 用途:
  7107. * - 统一渲染顶部按钮栏(buttons)
  7108. * - 统一渲染中间业务面板(list panels,可拖拽调高度)
  7109. * - 统一渲染底部图表(chart panels,内部用 ss-sidebar-chart-hover)
  7110. *
  7111. * 调用示例:
  7112. * ```html
  7113. * <ss-sidebar :buttons="sidebarButtons" :panels="sidebarPanels" @remove="handleSidebarRemove" />
  7114. * ```
  7115. *
  7116. * Props:
  7117. * - `buttons`:顶部按钮配置数组
  7118. * - `panels`:分区配置数组(`type: 'list' | 'chart'`)
  7119. *
  7120. * Events(向外透传):
  7121. * - `remove(item)`:来自 list 面板移除
  7122. * - `select(item)`:来自 list 面板选择(如后续需要)
  7123. */
  7124. const SsSidebar = {
  7125. name: "SsSidebar",
  7126. props: {
  7127. buttons: { type: Array, default: () => [] },
  7128. charts: { type: Array, default: () => [] },
  7129. list: { type: Array, default: () => [] }, // legacy
  7130. listMode: { type: String, default: "search" }, // legacy
  7131. panels: { type: Array, default: () => [] },
  7132. },
  7133. emits: ["select", "remove"],
  7134. data() {
  7135. return {
  7136. // 业务面板高度(索引 -> px) by xu 20260106
  7137. sectionHeights: [],
  7138. sectionCollapsed: [], // 功能说明:面板折叠状态(sectionPanels 索引) by xu 20260116
  7139. sectionHeightsExpanded: [], // 功能说明:面板展开高度缓存(用于折叠后恢复) by xu 20260116
  7140. sectionLastItemCounts: [], // 功能说明:记录栏目数据量,供“无数据默认关闭/已选自动展开”规则复用 by xu 20260313
  7141. chartCollapsed: [], // 功能说明:图表面板折叠状态(chartPanels 索引) by xu 20260116
  7142. chartHeaderTitleDownAt: [], // 功能说明:双击检测绑定到 chart 标题区 by xu 20260116
  7143. reportCollapsed: [], // 功能说明:报表面板折叠状态(reportPanels 索引) by xu 20260116
  7144. resizeTimer: null,
  7145. resizing: false,
  7146. resizeIndex: -1,
  7147. resizeStartY: 0,
  7148. resizeStartPrev: 0,
  7149. resizeStartNext: 0,
  7150. __resizeMoveHandler: null, // 功能说明:显式绑定 this 的 pointermove handler,避免 addEventListener 场景 this 丢失导致拖拽无效 by xu 20260122
  7151. __resizeEndHandler: null, // 功能说明:显式绑定 this 的 pointerup handler,确保能正确结束拖拽 by xu 20260122
  7152. __resizeCancelHandler: null, // 功能说明:显式绑定 this 的 pointercancel handler,触控取消也能收尾 by xu 20260122
  7153. __resizePrevSectionEl: null, // 功能说明:拖拽时直接写 DOM 高度(修复响应式更新不生效) by xu 20260122
  7154. __resizeNextSectionEl: null, // 功能说明:拖拽时直接写 DOM 高度(修复响应式更新不生效) by xu 20260122
  7155. };
  7156. },
  7157. methods: {
  7158. // 初始化默认高度(只在第一次/面板数量变化时补齐) by xu 20260106
  7159. ensureSectionHeights(sectionCount) {
  7160. if (!Array.isArray(this.sectionHeights)) this.sectionHeights = [];
  7161. if (this.sectionHeights.length === sectionCount) return;
  7162. const next = [];
  7163. for (let i = 0; i < sectionCount; i++) {
  7164. next[i] = this.sectionHeights[i] ?? 190; // 默认高度 by xu 20260106
  7165. }
  7166. this.sectionHeights = next;
  7167. // 功能说明:面板数量变化时补齐折叠/缓存数组长度 by xu 20260116
  7168. this.sectionCollapsed = Array.from(
  7169. { length: sectionCount },
  7170. (_, i) => !!this.sectionCollapsed?.[i]
  7171. );
  7172. this.sectionHeightsExpanded = Array.from(
  7173. { length: sectionCount },
  7174. (_, i) => this.sectionHeightsExpanded?.[i] ?? null
  7175. );
  7176. this.sectionLastItemCounts = Array.from(
  7177. { length: sectionCount },
  7178. (_, i) => Number(this.sectionLastItemCounts?.[i] ?? 0) || 0
  7179. );
  7180. },
  7181. __getPanelsForSectionState(panelsInput) {
  7182. return (panelsInput || []).length
  7183. ? panelsInput
  7184. : this.list?.length
  7185. ? [
  7186. {
  7187. type: "list",
  7188. title: "已选",
  7189. icon: "",
  7190. mode: this.listMode,
  7191. items: this.list,
  7192. },
  7193. ]
  7194. : [];
  7195. },
  7196. __getSectionPanelsForState(panelsInput) {
  7197. return this.__getPanelsForSectionState(panelsInput)
  7198. .filter((p) => {
  7199. const k = String(p?._tabKey ?? "")
  7200. .trim()
  7201. .toLowerCase();
  7202. const t = String(p?.title ?? "").trim();
  7203. if (k === "rbarobj") return false;
  7204. if (t === "对象") return false;
  7205. return true;
  7206. })
  7207. .filter((p) => p?.type !== "chart" && p?.type !== "report-table");
  7208. },
  7209. __getSectionPanelCount(panel) {
  7210. const explicitCount = Number(panel?.count);
  7211. if (Number.isFinite(explicitCount)) return explicitCount;
  7212. return Array.isArray(panel?.items) ? panel.items.length : 0;
  7213. },
  7214. __setSectionCollapsedState(index, collapsed) {
  7215. const i = Number(index);
  7216. if (isNaN(i) || i < 0) return;
  7217. const nextCollapsed = !!collapsed;
  7218. const collapsedHeight = 37;
  7219. const cur = !!this.sectionCollapsed?.[i];
  7220. const currentHeight = Number(this.sectionHeights?.[i] ?? 190) || 190;
  7221. if (nextCollapsed === cur) {
  7222. if (nextCollapsed && currentHeight !== collapsedHeight) {
  7223. this.sectionHeights.splice(i, 1, collapsedHeight);
  7224. }
  7225. return;
  7226. }
  7227. if (nextCollapsed) {
  7228. this.sectionHeightsExpanded[i] =
  7229. currentHeight > collapsedHeight
  7230. ? currentHeight
  7231. : Number(this.sectionHeightsExpanded?.[i] ?? 190) || 190;
  7232. this.sectionHeights.splice(i, 1, collapsedHeight);
  7233. } else {
  7234. const restore =
  7235. Number(this.sectionHeightsExpanded?.[i] ?? 190) || 190;
  7236. this.sectionHeights.splice(i, 1, restore);
  7237. }
  7238. this.sectionCollapsed.splice(i, 1, nextCollapsed);
  7239. },
  7240. __syncSectionAutoCollapse(panelsInput) {
  7241. const sectionPanels = this.__getSectionPanelsForState(panelsInput);
  7242. const sectionCount = sectionPanels.length;
  7243. this.ensureSectionHeights(sectionCount);
  7244. const prevCounts = Array.isArray(this.sectionLastItemCounts)
  7245. ? this.sectionLastItemCounts.slice()
  7246. : [];
  7247. const nextCounts = Array.from({ length: sectionCount }, (_, i) =>
  7248. this.__getSectionPanelCount(sectionPanels[i])
  7249. );
  7250. this.sectionLastItemCounts = nextCounts;
  7251. sectionPanels.forEach((panel, index) => {
  7252. const count = Number(nextCounts[index] ?? 0) || 0;
  7253. const prevCount = Number(prevCounts[index] ?? 0) || 0;
  7254. const title = String(panel?.title ?? "").trim();
  7255. if (count <= 0) {
  7256. this.__setSectionCollapsedState(index, true);
  7257. return;
  7258. }
  7259. if (title === "已选" && count > prevCount) {
  7260. this.__setSectionCollapsedState(index, false);
  7261. }
  7262. });
  7263. },
  7264. toggleSectionCollapse(index) {
  7265. // 功能说明:双击 header 折叠/展开 section 面板(仅控制高度与内容渲染) by xu 20260116
  7266. const i = Number(index);
  7267. if (isNaN(i) || i < 0) return;
  7268. // 功能说明:加更细粒度日志,定位“多面板折叠无视觉效果”的根因(高度是否真的变、DOM 是否更新) by xu 20260116
  7269. const collapsedHeight = 37; // 功能说明:header(35) + panel 边框(2),避免 flex shrink 导致 header 变 25px by xu 20260116
  7270. const cur = !!this.sectionCollapsed?.[i];
  7271. console.log("[SsSidebar] toggleSectionCollapse", {
  7272. index: i,
  7273. to: !cur,
  7274. prevHeight: this.sectionHeights?.[i],
  7275. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7276. if (!cur) {
  7277. this.sectionHeightsExpanded[i] = this.sectionHeights[i] ?? 190;
  7278. this.sectionHeights.splice(i, 1, collapsedHeight);
  7279. } else {
  7280. const restore =
  7281. Number(this.sectionHeightsExpanded?.[i] ?? 190) || 190;
  7282. this.sectionHeights.splice(i, 1, restore);
  7283. }
  7284. this.sectionCollapsed.splice(i, 1, !cur);
  7285. console.log("[SsSidebar] section state(after)", {
  7286. index: i,
  7287. height: this.sectionHeights?.[i],
  7288. collapsed: this.sectionCollapsed?.[i],
  7289. allHeights: Array.from(this.sectionHeights || []),
  7290. allCollapsed: Array.from(this.sectionCollapsed || []),
  7291. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7292. Vue.nextTick(() => {
  7293. try {
  7294. const root = this.$el;
  7295. const sections = root?.querySelectorAll?.(".ss-sidebar-section");
  7296. const el = sections?.[i];
  7297. const rectH = el?.getBoundingClientRect?.().height;
  7298. console.log("[SsSidebar] section dom(beforeFix)", {
  7299. index: i,
  7300. styleHeight: el?.style?.height,
  7301. rectH,
  7302. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7303. // 功能说明:若渲染未把 height patch 到 DOM,则在 nextTick 强制同步一次(并打印) by xu 20260116
  7304. const targetH =
  7305. (Number(this.sectionHeights?.[i] ?? 190) || 190) + "px";
  7306. if (el && el.style && el.style.height !== targetH) {
  7307. el.style.height = targetH;
  7308. }
  7309. if (el?.classList) {
  7310. el.classList.toggle("is-collapsed", !!this.sectionCollapsed?.[i]);
  7311. }
  7312. const rectAfter = el?.getBoundingClientRect?.().height;
  7313. console.log("[SsSidebar] section dom(afterFix)", {
  7314. index: i,
  7315. styleHeight: el?.style?.height,
  7316. rectH: rectAfter,
  7317. targetH,
  7318. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7319. } catch (err) {
  7320. console.log("[SsSidebar] section dom(afterTick) error", err); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7321. }
  7322. });
  7323. },
  7324. toggleChartCollapse(index) {
  7325. // 功能说明:双击 header 折叠/展开底部 chart 面板(隐藏/显示 chart-hover) by xu 20260116
  7326. const i = Number(index);
  7327. if (isNaN(i) || i < 0) return;
  7328. const cur = !!this.chartCollapsed?.[i];
  7329. console.log("[SsSidebar] toggleChartCollapse", { index: i, to: !cur }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7330. this.chartCollapsed.splice(i, 1, !cur);
  7331. console.log("[SsSidebar] chart state(after)", {
  7332. index: i,
  7333. collapsed: this.chartCollapsed?.[i],
  7334. allCollapsed: Array.from(this.chartCollapsed || []),
  7335. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7336. Vue.nextTick(() => {
  7337. try {
  7338. const root = this.$el;
  7339. const el = root?.querySelector?.(
  7340. `.ss-sidebar-chart-panel[data-chart-idx="${i}"]`
  7341. );
  7342. console.log("[SsSidebar] chart dom(beforeFix)", {
  7343. index: i,
  7344. found: !!el,
  7345. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7346. if (el?.classList)
  7347. el.classList.toggle("is-collapsed", !!this.chartCollapsed?.[i]);
  7348. console.log("[SsSidebar] chart dom(afterFix)", {
  7349. index: i,
  7350. collapsed: !!this.chartCollapsed?.[i],
  7351. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7352. } catch (err) {
  7353. console.log("[SsSidebar] chart dom(afterTick) error", err); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7354. }
  7355. });
  7356. },
  7357. toggleReportCollapse(index) {
  7358. // 功能说明:双击 header 折叠/展开底部 report-table 面板(隐藏/显示表格) by xu 20260116
  7359. const i = Number(index);
  7360. if (isNaN(i) || i < 0) return;
  7361. const cur = !!this.reportCollapsed?.[i];
  7362. console.log("[SsSidebar] toggleReportCollapse", { index: i, to: !cur }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7363. this.reportCollapsed.splice(i, 1, !cur);
  7364. console.log("[SsSidebar] report state(after)", {
  7365. index: i,
  7366. collapsed: this.reportCollapsed?.[i],
  7367. allCollapsed: Array.from(this.reportCollapsed || []),
  7368. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7369. Vue.nextTick(() => {
  7370. try {
  7371. const root = this.$el;
  7372. const el = root?.querySelector?.(
  7373. `.ss-sidebar-report-panel[data-report-idx="${i}"]`
  7374. );
  7375. console.log("[SsSidebar] report dom(beforeFix)", {
  7376. index: i,
  7377. found: !!el,
  7378. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7379. if (el?.classList)
  7380. el.classList.toggle("is-collapsed", !!this.reportCollapsed?.[i]);
  7381. console.log("[SsSidebar] report dom(afterFix)", {
  7382. index: i,
  7383. collapsed: !!this.reportCollapsed?.[i],
  7384. }); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7385. } catch (err) {
  7386. console.log("[SsSidebar] report dom(afterTick) error", err); // 功能说明:直接打印用于排查多面板不生效 by xu 20260116
  7387. }
  7388. });
  7389. },
  7390. startResize(index, e) {
  7391. if (e?.preventDefault) e.preventDefault();
  7392. if (e?.stopPropagation) e.stopPropagation();
  7393. if (this.resizing) return;
  7394. // 长按 0.5s 后才进入拖拽调高度 by xu 20260106
  7395. clearTimeout(this.resizeTimer);
  7396. // 功能说明:修复“长按拖拽偶发失效/卡死”——pointerup 早于 500ms 时必须取消 timer,否则 timer 触发后进入 resizing=true 且再也收不到 pointerup,导致后续都无法拖拽 by xu 20260122
  7397. const pointerId = e?.pointerId;
  7398. const cancelPendingResize = (ev) => {
  7399. try {
  7400. if (
  7401. pointerId != null &&
  7402. ev?.pointerId != null &&
  7403. ev.pointerId !== pointerId
  7404. )
  7405. return;
  7406. } catch (_) {}
  7407. clearTimeout(this.resizeTimer);
  7408. this.resizeTimer = null;
  7409. document.removeEventListener("pointerup", cancelPendingResize, true);
  7410. document.removeEventListener(
  7411. "pointercancel",
  7412. cancelPendingResize,
  7413. true
  7414. );
  7415. };
  7416. // 功能说明:用 document+capture 监听,避免 window 监听在部分容器/iframe 场景丢事件导致拖拽无响应 by xu 20260122
  7417. document.addEventListener("pointerup", cancelPendingResize, {
  7418. passive: false,
  7419. once: true,
  7420. capture: true,
  7421. });
  7422. document.addEventListener("pointercancel", cancelPendingResize, {
  7423. passive: false,
  7424. once: true,
  7425. capture: true,
  7426. });
  7427. const startY = e?.clientY ?? 0;
  7428. const gapEl = e?.currentTarget;
  7429. // 功能说明:记录 gap 前后 section 节点,拖拽过程中直接写入 style.height,避免某些环境下 Vue render 不刷新导致“拖拽无视觉变化” by xu 20260122
  7430. this.__resizePrevSectionEl = gapEl?.previousElementSibling || null;
  7431. this.__resizeNextSectionEl = gapEl?.nextElementSibling || null;
  7432. this.resizeTimer = setTimeout(() => {
  7433. // 定时器触发后进入拖拽态,移除“取消等待”监听,避免误取消 by xu 20260122
  7434. document.removeEventListener("pointerup", cancelPendingResize, true);
  7435. document.removeEventListener(
  7436. "pointercancel",
  7437. cancelPendingResize,
  7438. true
  7439. );
  7440. this.resizing = true;
  7441. this.resizeIndex = index;
  7442. this.resizeStartY = startY;
  7443. this.resizeStartPrev = this.sectionHeights[index] ?? 190;
  7444. this.resizeStartNext = this.sectionHeights[index + 1] ?? 190;
  7445. gapEl?.classList?.add("is-active");
  7446. try {
  7447. if (gapEl?.setPointerCapture && pointerId != null)
  7448. gapEl.setPointerCapture(pointerId);
  7449. } catch (_) {} // 功能说明:捕获 pointer,避免拖拽过程中移出窗口导致 pointermove 丢失 by xu 20260122
  7450. // 功能说明:用 document+capture,确保拖拽时 pointermove/up 不会被中途 stopPropagation 影响 by xu 20260122
  7451. document.addEventListener(
  7452. "pointermove",
  7453. this.__resizeMoveHandler || this.onResizeMove,
  7454. { passive: false, capture: true }
  7455. );
  7456. document.addEventListener(
  7457. "pointerup",
  7458. this.__resizeEndHandler || this.endResize,
  7459. { passive: false, once: true, capture: true }
  7460. );
  7461. document.addEventListener(
  7462. "pointercancel",
  7463. this.__resizeCancelHandler || this.endResize,
  7464. { passive: false, once: true, capture: true }
  7465. ); // 功能说明:触控/手势取消时也要结束拖拽 by xu 20260122
  7466. }, 500);
  7467. },
  7468. onResizeMove(e) {
  7469. if (!this.resizing) return;
  7470. if (e?.preventDefault) e.preventDefault();
  7471. const dy = (e?.clientY ?? 0) - this.resizeStartY;
  7472. const minPanelHeight = 83; // header(35) + listMin(48) by xu 20260106
  7473. const prev = Math.max(minPanelHeight, this.resizeStartPrev + dy);
  7474. const next = Math.max(minPanelHeight, this.resizeStartNext - dy);
  7475. // 若其中一个达到最小值,则停止继续挤压 by xu 20260106
  7476. const adjustedDy = prev - this.resizeStartPrev;
  7477. const nextAdjusted = this.resizeStartNext - adjustedDy;
  7478. const appliedPrev = prev;
  7479. const appliedNext = Math.max(minPanelHeight, nextAdjusted);
  7480. this.sectionHeights.splice(this.resizeIndex, 1, appliedPrev);
  7481. this.sectionHeights.splice(this.resizeIndex + 1, 1, appliedNext);
  7482. // 功能说明:强制触发一次 sectionHeights 引用变化,避免某些运行时环境下 splice 未触发视图更新导致“拖拽没反应” by xu 20260122
  7483. this.sectionHeights = (this.sectionHeights || []).slice();
  7484. // 功能说明:兜底——直接写 DOM 的 height,确保视觉立即响应(用于排查/修复某些环境下 render 不更新) by xu 20260122
  7485. try {
  7486. if (this.__resizePrevSectionEl?.style)
  7487. this.__resizePrevSectionEl.style.height = appliedPrev + "px";
  7488. if (this.__resizeNextSectionEl?.style)
  7489. this.__resizeNextSectionEl.style.height = appliedNext + "px";
  7490. } catch (_) {}
  7491. },
  7492. endResize(e) {
  7493. clearTimeout(this.resizeTimer);
  7494. this.resizeTimer = null;
  7495. if (!this.resizing) return;
  7496. if (e?.preventDefault) e.preventDefault();
  7497. const activeGaps = document.querySelectorAll(
  7498. ".ss-sidebar-gap.is-active"
  7499. );
  7500. activeGaps.forEach((g) => g.classList.remove("is-active"));
  7501. this.resizing = false;
  7502. this.resizeIndex = -1;
  7503. this.__resizePrevSectionEl = null; // 功能说明:释放 DOM 引用,避免内存泄漏 by xu 20260122
  7504. this.__resizeNextSectionEl = null; // 功能说明:释放 DOM 引用,避免内存泄漏 by xu 20260122
  7505. document.removeEventListener(
  7506. "pointermove",
  7507. this.__resizeMoveHandler || this.onResizeMove,
  7508. true
  7509. );
  7510. document.removeEventListener(
  7511. "pointercancel",
  7512. this.__resizeCancelHandler || this.endResize,
  7513. true
  7514. ); // 功能说明:清理 cancel 监听,避免残留 by xu 20260122
  7515. },
  7516. },
  7517. watch: {
  7518. panels: {
  7519. handler(nextPanels) {
  7520. this.__syncSectionAutoCollapse(nextPanels);
  7521. },
  7522. deep: true,
  7523. immediate: true,
  7524. },
  7525. list: {
  7526. handler(nextList) {
  7527. if ((this.panels || []).length) return;
  7528. this.__syncSectionAutoCollapse(nextList);
  7529. },
  7530. deep: true,
  7531. immediate: true,
  7532. },
  7533. },
  7534. mounted() {
  7535. clearTimeout(this.resizeTimer);
  7536. this.resizeTimer = null;
  7537. // 功能说明:绑定拖拽事件 handler(用于 add/removeEventListener) by xu 20260122
  7538. if (!this.__resizeMoveHandler)
  7539. this.__resizeMoveHandler = (e) => this.onResizeMove?.(e);
  7540. if (!this.__resizeEndHandler)
  7541. this.__resizeEndHandler = (e) => this.endResize?.(e);
  7542. if (!this.__resizeCancelHandler)
  7543. this.__resizeCancelHandler = (e) => this.endResize?.(e);
  7544. // 功能说明:暂时回退为固定底部留白方案(CSS 控制),后续再定位遮挡根因 by xu 20260115
  7545. },
  7546. beforeUnmount() {
  7547. clearTimeout(this.resizeTimer);
  7548. this.resizeTimer = null;
  7549. document.removeEventListener(
  7550. "pointermove",
  7551. this.__resizeMoveHandler || this.onResizeMove,
  7552. true
  7553. );
  7554. document.removeEventListener(
  7555. "pointerup",
  7556. this.__resizeEndHandler || this.endResize,
  7557. true
  7558. );
  7559. document.removeEventListener(
  7560. "pointercancel",
  7561. this.__resizeCancelHandler || this.endResize,
  7562. true
  7563. );
  7564. },
  7565. render() {
  7566. const SsSidebarButtonsComp = Vue.resolveComponent("ss-sidebar-buttons");
  7567. const SsSidebarChartComp = Vue.resolveComponent("ss-sidebar-chart");
  7568. const SsSidebarListComp = Vue.resolveComponent("ss-sidebar-list");
  7569. const SsSidebarReportTableComp = Vue.resolveComponent(
  7570. "ss-sidebar-report-table"
  7571. );
  7572. const SsIcon = Vue.resolveComponent("ss-icon");
  7573. // 支持 panels(多分区),list/listMode 作为 legacy 兜底 by xu 20260106
  7574. const panels = (this.panels || []).length
  7575. ? this.panels
  7576. : this.list?.length
  7577. ? [
  7578. {
  7579. type: "list",
  7580. title: "已选",
  7581. icon: "",
  7582. mode: this.listMode,
  7583. items: this.list,
  7584. },
  7585. ]
  7586. : [];
  7587. // 功能说明:右侧栏强制移除“对象”tab(兼容后端返回 rbarObj/rbarobj 或直接返回中文“对象”标题) by xu 20260116
  7588. const panelsNoObj = (panels || []).filter((p) => {
  7589. const k = String(p?._tabKey ?? "")
  7590. .trim()
  7591. .toLowerCase();
  7592. const t = String(p?.title ?? "").trim();
  7593. if (k === "rbarobj") return false;
  7594. if (t === "对象") return false;
  7595. return true;
  7596. });
  7597. // 功能说明:report-table 作为底部报表区(放在统计图下面),不参与可拖拽 section 面板 by xu 20260115
  7598. const sectionPanels = panelsNoObj.filter(
  7599. (p) => p?.type !== "chart" && p?.type !== "report-table"
  7600. );
  7601. const chartPanels = panelsNoObj.filter((p) => p?.type === "chart");
  7602. const reportPanels = panelsNoObj.filter(
  7603. (p) => p?.type === "report-table"
  7604. );
  7605. this.ensureSectionHeights(sectionPanels.length);
  7606. // 功能说明:补齐 chart/report 折叠数组长度 by xu 20260116
  7607. this.chartCollapsed = Array.from(
  7608. { length: chartPanels.length },
  7609. (_, i) => !!this.chartCollapsed?.[i]
  7610. );
  7611. this.chartHeaderTitleDownAt = Array.from(
  7612. { length: chartPanels.length },
  7613. (_, i) => this.chartHeaderTitleDownAt?.[i] ?? 0
  7614. );
  7615. this.reportCollapsed = Array.from(
  7616. { length: reportPanels.length },
  7617. (_, i) => !!this.reportCollapsed?.[i]
  7618. );
  7619. return Vue.h("div", { class: "ss-sidebar" }, [
  7620. this.buttons?.length
  7621. ? Vue.h(SsSidebarButtonsComp, { items: this.buttons })
  7622. : null,
  7623. Vue.h(
  7624. "div",
  7625. { class: "ss-sidebar__inner" },
  7626. [
  7627. ...(this.charts || []).map((c) =>
  7628. Vue.h(SsSidebarChartComp, {
  7629. options: c?.options || {},
  7630. height: c?.height || "200px",
  7631. })
  7632. ),
  7633. // 可拖拽的业务面板容器 by xu 20260106
  7634. Vue.h(
  7635. "div",
  7636. { class: "ss-sidebar-sections", style: { flex: "0 0 auto" } },
  7637. sectionPanels.flatMap((p, idx) => {
  7638. const panelContent =
  7639. p?.type === "report-table"
  7640. ? Vue.h(SsSidebarReportTableComp, {
  7641. key: `ss-sidebar-report-in-section-${idx}-${
  7642. p?.title ?? ""
  7643. }`,
  7644. title: p?.title ?? "",
  7645. icon: p?.icon ?? "",
  7646. iconClass: p?.iconClass ?? "",
  7647. items: p?.items || [],
  7648. onOpen: (srv, ctx) => p?.onOpen?.(srv, ctx),
  7649. })
  7650. : Vue.h(SsSidebarListComp, {
  7651. key: `ss-sidebar-list-${idx}-${p?.title ?? ""}`,
  7652. title: p?.title ?? "",
  7653. icon: p?.icon ?? "",
  7654. count: p?.count ?? p?.items?.length ?? "",
  7655. closable: !!p?.closable,
  7656. searchable: !!p?.searchable,
  7657. searchInHeader: !!p?.searchInHeader,
  7658. headerFilters: p?.headerFilters || [],
  7659. headerSearchButton: !!p?.headerSearchButton,
  7660. searchPlaceholder: p?.searchPlaceholder ?? "搜索",
  7661. itemLayout: p?.itemLayout ?? "simple",
  7662. itemAction: p?.itemAction ?? true,
  7663. collapsible: true,
  7664. collapsed: !!this.sectionCollapsed?.[idx],
  7665. onToggleCollapse: () =>
  7666. this.toggleSectionCollapse?.(idx),
  7667. iconClass: p?.iconClass ?? "",
  7668. items: p?.items || [],
  7669. mode: p?.mode || "search",
  7670. onSelect: (item) => this.$emit("select", item),
  7671. onRemove: (item) => this.$emit("remove", item),
  7672. onClear: () => p?.onClear?.(),
  7673. onSearch: (payload) => p?.onSearch?.(payload),
  7674. });
  7675. const section = Vue.h(
  7676. "div",
  7677. {
  7678. class: {
  7679. "ss-sidebar-section": true,
  7680. "is-collapsed": !!this.sectionCollapsed?.[idx],
  7681. },
  7682. key: `ss-sidebar-section-${idx}-${p?.type ?? "list"}-${
  7683. p?.title ?? ""
  7684. }`,
  7685. style: {
  7686. height: (this.sectionHeights[idx] ?? 190) + "px",
  7687. flex: "0 0 auto",
  7688. },
  7689. },
  7690. [
  7691. Vue.h("div", { class: "ss-sidebar-section__content" }, [
  7692. panelContent,
  7693. ]),
  7694. ]
  7695. );
  7696. const gap =
  7697. idx < sectionPanels.length - 1
  7698. ? Vue.h("div", {
  7699. class: "ss-sidebar-gap",
  7700. onPointerdown: (e) => this.startResize(idx, e),
  7701. })
  7702. : null;
  7703. return gap ? [section, gap] : [section];
  7704. })
  7705. ),
  7706. // 图表区固定在底部(hover 弹出大图) by xu 20260106
  7707. ...chartPanels.map((p, chartIdx) =>
  7708. Vue.h(
  7709. "div",
  7710. {
  7711. class: {
  7712. "ss-sidebar-panel": true,
  7713. "ss-sidebar-chart-panel": true,
  7714. "is-collapsed": !!this.chartCollapsed?.[chartIdx],
  7715. },
  7716. style: { flex: "0 0 auto", minHeight: "37px" },
  7717. "data-chart-idx": chartIdx,
  7718. key: `ss-sidebar-chart-${chartIdx}-${p?.title ?? ""}`,
  7719. },
  7720. [
  7721. p?.title
  7722. ? Vue.h(
  7723. "div",
  7724. {
  7725. class: "ss-sidebar-panel__header",
  7726. onDblclick: (e) => {
  7727. e?.preventDefault?.();
  7728. e?.stopPropagation?.();
  7729. console.log("[SsSidebar] chart header dblclick", {
  7730. idx: chartIdx,
  7731. title: p?.title,
  7732. });
  7733. this.toggleChartCollapse?.(chartIdx);
  7734. },
  7735. },
  7736. [
  7737. Vue.h("div", { class: "ss-sidebar-panel__title" }, [
  7738. p?.iconClass
  7739. ? Vue.h(SsIcon, {
  7740. class:
  7741. p.iconClass + " ss-sidebar-panel__icon",
  7742. })
  7743. : p?.icon
  7744. ? Vue.h(SsIcon, {
  7745. name: p.icon,
  7746. size: "16px",
  7747. class: "ss-sidebar-panel__icon",
  7748. })
  7749. : null,
  7750. Vue.h("span", null, p.title),
  7751. ]),
  7752. Vue.h("div", { class: "ss-sidebar-panel__tools" }),
  7753. ]
  7754. )
  7755. : null,
  7756. this.chartCollapsed?.[chartIdx]
  7757. ? null
  7758. : Vue.h(Vue.resolveComponent("ss-sidebar-chart-hover"), {
  7759. title: p?.title ?? "",
  7760. iconClass: p?.iconClass ?? "",
  7761. icon: p?.icon ?? "",
  7762. options: p?.options || {},
  7763. height: p?.height || "240px",
  7764. }),
  7765. ]
  7766. )
  7767. ),
  7768. ...reportPanels.map((p, reportIdx) =>
  7769. Vue.h(
  7770. "div",
  7771. {
  7772. class: {
  7773. "ss-sidebar-report-panel-wrap": true,
  7774. "ss-sidebar-report-panel": true,
  7775. "is-collapsed": !!this.reportCollapsed?.[reportIdx],
  7776. },
  7777. style: { flex: "0 0 auto", minHeight: "37px" },
  7778. "data-report-idx": reportIdx,
  7779. key: `ss-sidebar-report-wrap-${reportIdx}-${p?.title ?? ""}`,
  7780. },
  7781. [
  7782. Vue.h(SsSidebarReportTableComp, {
  7783. key: `ss-sidebar-report-${reportIdx}-${p?.title ?? ""}`,
  7784. title: p?.title ?? "",
  7785. icon: p?.icon ?? "",
  7786. iconClass: p?.iconClass ?? "",
  7787. items: p?.items || [],
  7788. collapsible: true,
  7789. collapsed: !!this.reportCollapsed?.[reportIdx],
  7790. onToggleCollapse: () =>
  7791. this.toggleReportCollapse?.(reportIdx),
  7792. onOpen: (srv, ctx) => p?.onOpen?.(srv, ctx),
  7793. }),
  7794. ]
  7795. )
  7796. ),
  7797. ].filter(Boolean)
  7798. ),
  7799. ]);
  7800. },
  7801. };
  7802. // ss-folder-card 文件夹卡片
  7803. const SsFolderCard = {
  7804. name: "SsFolderCard",
  7805. props: {
  7806. item: {
  7807. type: Object,
  7808. required: true,
  7809. },
  7810. },
  7811. data() {
  7812. return {
  7813. showButtons: false,
  7814. };
  7815. },
  7816. emits: ["click", "change"],
  7817. setup(props, { emit }) {
  7818. const item = props.item;
  7819. const showChildren = ref(false);
  7820. const eventBus = window.parent.sharedEventBus;
  7821. const itemWidth = Vue.computed(() => {
  7822. // 功能说明:页面改为 grid 等分布局后,卡片宽度交给容器控制,这里固定 100% by xu 20260116
  7823. return "100%";
  7824. });
  7825. onMounted(() => {
  7826. eventBus.subscribe("folderPath", (path) => {
  7827. const currentPath = path || [];
  7828. // 如果当前文件夹不在路径中,则销毁视图
  7829. if (
  7830. !currentPath.some((item) => item.folder.title === props.item.title)
  7831. ) {
  7832. showChildren.value = false;
  7833. }
  7834. });
  7835. });
  7836. const onItemClick = (e) => {
  7837. if (e && e.stopPropagation) {
  7838. e.stopPropagation();
  7839. }
  7840. // 单击只处理 active 状态
  7841. if (e && e.currentTarget) {
  7842. const allListCards = document.querySelectorAll(
  7843. ".knowledge-item-container"
  7844. );
  7845. const allFolderCards = document.querySelectorAll(".ss-folder-list");
  7846. allListCards.forEach((card) => card.classList.remove("active"));
  7847. allFolderCards.forEach((card) => card.classList.remove("active"));
  7848. e.currentTarget.classList.add("active");
  7849. } else {
  7850. // 如果是数据对象,需要找到对应的 DOM 元素
  7851. const allListCards = document.querySelectorAll(
  7852. ".knowledge-item-container"
  7853. );
  7854. const allFolderCards = document.querySelectorAll(".ss-folder-list");
  7855. allListCards.forEach((card) => card.classList.remove("active"));
  7856. allFolderCards.forEach((card) => card.classList.remove("active"));
  7857. // 找到标题匹配的文件夹元素
  7858. const targetFolder = Array.from(allFolderCards).find((card) =>
  7859. card.textContent.includes(e.title)
  7860. );
  7861. if (targetFolder) {
  7862. targetFolder.classList.add("active");
  7863. }
  7864. }
  7865. emit("click", item);
  7866. };
  7867. // 修改双击处理函数
  7868. const handleFolderDblClick = (folder, e) => {
  7869. if (e) e.stopPropagation();
  7870. if (folder.children?.length) {
  7871. showChildren.value = true;
  7872. const pathInfo = {
  7873. title: folder.title,
  7874. folder: folder,
  7875. };
  7876. const currentPath = eventBus.getState("folderPath") || [];
  7877. if (!currentPath.some((item) => item.title === folder.title)) {
  7878. eventBus.publish("folderPath", [...currentPath, pathInfo]);
  7879. }
  7880. }
  7881. };
  7882. const onItemChange = (e, icon, index) => {
  7883. e.stopPropagation();
  7884. props.item.buttons[0].onclick();
  7885. // emit("change", { item: props.item, icon, index });
  7886. };
  7887. return {
  7888. item,
  7889. itemWidth,
  7890. showChildren,
  7891. onItemClick,
  7892. onItemChange,
  7893. handleFolderDblClick,
  7894. };
  7895. },
  7896. render() {
  7897. const SsCartListIcon = Vue.resolveComponent("ss-cart-list-icon");
  7898. if (this.showChildren) {
  7899. return h(SsFolderCartView, {
  7900. folder: this.item,
  7901. });
  7902. }
  7903. return Vue.h(
  7904. "div",
  7905. {
  7906. class: { "ss-folder-list": true, active: this.item.active },
  7907. onClick: (e) => {
  7908. e.stopPropagation();
  7909. this.onItemClick(e);
  7910. },
  7911. onDblclick: (e) => this.handleFolderDblClick(this.item, e),
  7912. style: { width: this.itemWidth },
  7913. },
  7914. [
  7915. // 文件夹特有的装饰元素
  7916. Vue.h("div", { class: "ss-folder-list-trapezoid" }),
  7917. Vue.h("div", { class: "ss-folder-list-top-transparent" }),
  7918. Vue.h("div", { class: "ss-folder-list-top" }),
  7919. Vue.h("div", { class: "ss-folder-list-right" }),
  7920. // header 部分(按钮)
  7921. this.item?.buttons?.length > 0 &&
  7922. Vue.h(
  7923. "div",
  7924. {
  7925. class: "header",
  7926. onMouseenter: () => (this.showButtons = true),
  7927. onMouseleave: () => (this.showButtons = false),
  7928. onClick: (e) => this.onItemChange(e, this.item.buttons[0], 0),
  7929. },
  7930. [
  7931. // this.item?.buttons?.length > 0 &&
  7932. Vue.h("div", {
  7933. class: "cart-list-setting cart-list-icon",
  7934. title: this.item?.buttons?.[0]?.title,
  7935. }),
  7936. // this.item?.buttons?.length > 0 &&
  7937. this.showButtons &&
  7938. this.item?.buttons?.length > 1 &&
  7939. Vue.h(
  7940. "div",
  7941. {
  7942. class: "cart-list-button-popup",
  7943. },
  7944. this.item.buttons.map((btn) =>
  7945. Vue.h(
  7946. "div",
  7947. {
  7948. onClick: (e) => {
  7949. e.stopPropagation();
  7950. btn.onclick?.();
  7951. },
  7952. },
  7953. [
  7954. btn.class &&
  7955. Vue.h(SsCartListIcon, {
  7956. class: [btn.class],
  7957. }),
  7958. Vue.h("span", null, btn.title),
  7959. ]
  7960. )
  7961. )
  7962. ),
  7963. ]
  7964. ),
  7965. // body 部分
  7966. Vue.h("div", { class: "body" }, [
  7967. Vue.h("div", { class: "box-header" }, [
  7968. Vue.h("div", null, this.item.title),
  7969. ]),
  7970. Vue.h(
  7971. "div",
  7972. {
  7973. class: !this.item.thumb ? "no-thumb box-body" : "box-body",
  7974. },
  7975. [
  7976. this.item.thumb
  7977. ? Vue.h("div", { class: "left" }, [
  7978. Vue.h("img", {
  7979. src: this.item.thumb,
  7980. alt: "Thumbnail",
  7981. class: "imgUnHandle",
  7982. style: {
  7983. "object-fit": "cover",
  7984. width: "100%",
  7985. height: "100%",
  7986. },
  7987. }),
  7988. ])
  7989. : null,
  7990. Vue.h("div", { class: "right" }, [
  7991. ...this.item.tags.map((tag) => {
  7992. const [key, value] = Object.entries(tag)[0];
  7993. return Vue.h(
  7994. "div",
  7995. {
  7996. class: "title",
  7997. title: `${key}: ${value}`,
  7998. },
  7999. `${key}: ${value}`
  8000. );
  8001. }),
  8002. ]),
  8003. ]
  8004. ),
  8005. ]),
  8006. ]
  8007. );
  8008. },
  8009. };
  8010. // SsFolderCartView 组件 - 用于显示文件夹内容
  8011. const SsFolderCartView = {
  8012. name: "SsFolderCartView",
  8013. props: {
  8014. folder: {
  8015. type: Object,
  8016. required: true,
  8017. },
  8018. },
  8019. emits: ["click"],
  8020. setup(props, { emit }) {
  8021. const eventBus = window.parent.sharedEventBus;
  8022. const currentFolder = ref(props.folder);
  8023. const showChildren = ref(false);
  8024. const onItemClick = (e) => {
  8025. if (e && e.stopPropagation) {
  8026. e.stopPropagation();
  8027. }
  8028. // 单击只处理 active 状态
  8029. if (e && e.currentTarget) {
  8030. const allListCards = document.querySelectorAll(
  8031. ".knowledge-item-container"
  8032. );
  8033. const allFolderCards = document.querySelectorAll(".ss-folder-list");
  8034. allListCards.forEach((card) => card.classList.remove("active"));
  8035. allFolderCards.forEach((card) => card.classList.remove("active"));
  8036. e.currentTarget.classList.add("active");
  8037. } else {
  8038. // 如果是数据对象,需要找到对应的 DOM 元素
  8039. const allListCards = document.querySelectorAll(
  8040. ".knowledge-item-container"
  8041. );
  8042. const allFolderCards = document.querySelectorAll(".ss-folder-list");
  8043. allListCards.forEach((card) => card.classList.remove("active"));
  8044. allFolderCards.forEach((card) => card.classList.remove("active"));
  8045. // 找到标题匹配的文件夹元素
  8046. const targetFolder = Array.from(allFolderCards).find((card) =>
  8047. card.textContent.includes(e.title)
  8048. );
  8049. if (targetFolder) {
  8050. targetFolder.classList.add("active");
  8051. }
  8052. }
  8053. emit("click", props.folder);
  8054. };
  8055. const handleFolderDblClick = (folder, e) => {
  8056. if (e) e.stopPropagation();
  8057. if (folder.children?.length) {
  8058. showChildren.value = true;
  8059. const pathInfo = {
  8060. title: folder.title,
  8061. folder: folder,
  8062. };
  8063. const currentPath = eventBus.getState("folderPath") || [];
  8064. if (!currentPath.some((item) => item.title === folder.title)) {
  8065. eventBus.publish("folderPath", [...currentPath, pathInfo]);
  8066. currentFolder.value = folder;
  8067. }
  8068. }
  8069. };
  8070. const goBack = (targetFolder) => {
  8071. if (targetFolder === null) {
  8072. // 返回根目录
  8073. eventBus.publish("folderPath", []);
  8074. } else {
  8075. currentFolder.value = targetFolder;
  8076. }
  8077. };
  8078. return {
  8079. currentFolder,
  8080. showChildren,
  8081. onItemClick,
  8082. handleFolderDblClick,
  8083. goBack,
  8084. };
  8085. },
  8086. render() {
  8087. return h(
  8088. "div",
  8089. {
  8090. class: "page-container",
  8091. style: {
  8092. position: "fixed",
  8093. top: 0,
  8094. left: 0,
  8095. width: "100%",
  8096. height: "100%",
  8097. background: "var(--lightgray)",
  8098. padding: "20px 0",
  8099. zIndex: 1000,
  8100. },
  8101. },
  8102. [
  8103. // 搜索栏
  8104. h("div", { class: "search-bar" }, [
  8105. h("div", { class: "search-bar-contaienr" }, [
  8106. h(SsBreadcrumb, {
  8107. level: {
  8108. onBack: this.goBack,
  8109. },
  8110. }),
  8111. ]),
  8112. ]),
  8113. // 内容区域
  8114. h(
  8115. "div",
  8116. {
  8117. class: "content-area item-content-area",
  8118. style: { gap: "20px" },
  8119. },
  8120. [
  8121. ...(this.currentFolder.children || []).map((child, index) =>
  8122. h(child.children ? SsFolderCard : SsListCard, {
  8123. key: index,
  8124. item: child,
  8125. onClick: (e) => this.onItemClick(e),
  8126. onDblclick: (e) => this.handleFolderDblClick(child, e),
  8127. })
  8128. ),
  8129. ]
  8130. ),
  8131. ]
  8132. );
  8133. },
  8134. };
  8135. // ss-page分页
  8136. const SsPage = {
  8137. name: "SsPage",
  8138. props: {
  8139. total: {
  8140. type: Number,
  8141. required: true,
  8142. },
  8143. size: {
  8144. type: Number,
  8145. default: 10,
  8146. },
  8147. page: {
  8148. type: Number,
  8149. default: 1,
  8150. },
  8151. onChange: {
  8152. type: Function,
  8153. default: () => {},
  8154. },
  8155. },
  8156. setup(props) {
  8157. const totalItems = ref(props.total); // 总条目数
  8158. const totalPages = ref(Math.ceil(props.total / props.size));
  8159. const currentPage = ref(props.page); // 当前页码
  8160. // 计算显示的信息
  8161. const pageInfo = ref(
  8162. `共${totalItems.value}条,第 ${currentPage.value}/${totalPages.value} 页`
  8163. );
  8164. // 上一页的逻辑
  8165. const goToPreviousPage = (e) => {
  8166. e.preventDefault(); // 阻止默认行为
  8167. if (currentPage.value > 1) {
  8168. currentPage.value -= 1;
  8169. updatePageInfo();
  8170. props.onChange?.({
  8171. pageNo: currentPage.value, // 当前页码
  8172. rowNumPer: props.size, // 每页条数
  8173. rowNum: props.total, // 总记录数
  8174. });
  8175. }
  8176. };
  8177. // 下一页的逻辑
  8178. const goToNextPage = (e) => {
  8179. e.preventDefault(); // 阻止默认行为
  8180. if (currentPage.value < totalPages.value) {
  8181. currentPage.value += 1;
  8182. updatePageInfo();
  8183. props.onChange?.({
  8184. pageNo: currentPage.value, // 当前页码
  8185. rowNumPer: props.size, // 每页条数
  8186. rowNum: props.total, // 总记录数
  8187. });
  8188. }
  8189. };
  8190. // 更新页码信息的函数
  8191. const updatePageInfo = () => {
  8192. pageInfo.value = `共${totalItems.value}条,第 ${currentPage.value}/${totalPages.value} 页`;
  8193. };
  8194. return {
  8195. pageInfo,
  8196. totalPages,
  8197. goToPreviousPage,
  8198. goToNextPage,
  8199. };
  8200. },
  8201. render(props, { slots, emit }) {
  8202. return Vue.h("div", { class: "pager-container" }, [
  8203. Vue.h("input", { type: "hidden", name: "rowNum", value: props.total }),
  8204. Vue.h("input", {
  8205. type: "hidden",
  8206. name: "rowNumPer",
  8207. value: props.size,
  8208. }),
  8209. Vue.h("input", {
  8210. type: "hidden",
  8211. name: "pageCount",
  8212. value: this.totalPages,
  8213. }),
  8214. Vue.h("input", { type: "hidden", name: "pageNo", value: props.page }),
  8215. Vue.h("div", { class: "pager-content" }, [
  8216. Vue.h("div", { class: "info" }, this.pageInfo),
  8217. Vue.h(
  8218. "div",
  8219. { class: "btn" },
  8220. Vue.h(
  8221. "button",
  8222. { onClick: (e) => this.goToPreviousPage(e) },
  8223. "上一页"
  8224. )
  8225. ),
  8226. Vue.h(
  8227. "div",
  8228. { class: "btn" },
  8229. Vue.h("button", { onClick: (e) => this.goToNextPage(e) }, "下一页")
  8230. ),
  8231. ]),
  8232. ]);
  8233. },
  8234. };
  8235. // ss-right-info 一级页面右边栏
  8236. const SSRightInfo = {
  8237. name: "SSRightInfo",
  8238. setup() {
  8239. // 初始化响应式数据
  8240. const item = ref({
  8241. thumb: "images/example/project-img.png", // 更换为适合你项目的实际路径
  8242. title: "工业和信息化产业高质量发展资金",
  8243. });
  8244. return {
  8245. item,
  8246. };
  8247. },
  8248. render() {
  8249. return Vue.h("div", { class: "info-container" }, [
  8250. Vue.h("div", { class: "header" }, [
  8251. Vue.h("div", [
  8252. Vue.h("img", {
  8253. src: this.item.thumb,
  8254. class: "imgUnHandle",
  8255. style: { "object-fit": "cover", width: "100%", height: "100%" },
  8256. }), // 将 ImageViewer 替换为 img 标签
  8257. ]),
  8258. Vue.h("div", [Vue.h("div", this.item.title)]),
  8259. ]),
  8260. Vue.h("div", { class: "section-container" }, [
  8261. Vue.h("div", { class: "section" }, [
  8262. Vue.h("div", { class: "title" }, "合同"),
  8263. Vue.h("div", { class: "text" }, "合同总金额:42,399,320"),
  8264. Vue.h(
  8265. "div",
  8266. { class: "a" },
  8267. "《工业和信息化产业高质量发展资金补助合同》"
  8268. ),
  8269. ]),
  8270. Vue.h("div", { class: "section" }, [
  8271. Vue.h("div", { class: "title" }, "发票"),
  8272. Vue.h("div", { class: "text" }, "应开发票总额:42,399,320"),
  8273. Vue.h("div", { class: "text" }, "已开发票金额:17,235,345"),
  8274. Vue.h("div", { class: "text" }, "未开发票金额:25,163,975"),
  8275. ]),
  8276. Vue.h("div", { class: "section" }, [
  8277. Vue.h("div", { class: "title" }, "项目组成员"),
  8278. Vue.h("div", { class: "text" }, "我司:3人"),
  8279. Vue.h("div", { class: "text" }, "对方:2人"),
  8280. Vue.h("div", { class: "text" }, "项目负责人:张三"),
  8281. ]),
  8282. Vue.h("div", { class: "section" }, [
  8283. Vue.h("div", { class: "title" }, "采购"),
  8284. Vue.h("div", { class: "text" }, "总额:999,320"),
  8285. Vue.h("div", { class: "text" }, "已付金额:335,345"),
  8286. Vue.h("div", { class: "text" }, "未付金额:663,975"),
  8287. ]),
  8288. ]),
  8289. ]);
  8290. },
  8291. };
  8292. //
  8293. const SsSuccessPopup = {
  8294. name: "SsSuccessPopup",
  8295. props: {
  8296. right: {
  8297. type: String,
  8298. default: "20px",
  8299. },
  8300. bottom: {
  8301. type: String,
  8302. default: "calc(100% + 5px)",
  8303. },
  8304. },
  8305. setup(props, { expose }) {
  8306. // 响应式状态:是否可见
  8307. const visible = ref(false);
  8308. // 计算样式
  8309. const style = computed(() => {
  8310. return {
  8311. "--message-dialog-right": props.right,
  8312. "--message-dialog-bottom": props.bottom,
  8313. };
  8314. });
  8315. // 显示对话框的方法
  8316. const show = () => {
  8317. visible.value = true;
  8318. };
  8319. // 隐藏对话框的方法
  8320. const hide = () => {
  8321. visible.value = false;
  8322. };
  8323. // 将方法暴露给外部使用
  8324. expose({ show, hide });
  8325. // 返回渲染函数
  8326. return () => {
  8327. if (!visible.value) return null;
  8328. const SsIcon = resolveComponent("ss-icon");
  8329. return h(
  8330. "div",
  8331. {
  8332. class: "success-popup",
  8333. style: style.value,
  8334. onClick: (e) => e.stopPropagation(),
  8335. },
  8336. [
  8337. h("div", { class: "left" }, [
  8338. h("div", { class: "icon" }, [
  8339. h(SsIcon, { name: "check", size: "36px" }),
  8340. ]),
  8341. ]),
  8342. h("div", { class: "right" }, [
  8343. h("div", { class: "title" }, "提交成功"),
  8344. h("div", { class: "desc" }, "您的信息已成功提交。"),
  8345. ]),
  8346. ]
  8347. );
  8348. };
  8349. },
  8350. };
  8351. const SsErrorDialog = {
  8352. name: "SsErrorDialog",
  8353. setup(props, { emit }) {
  8354. const visible = ref(false);
  8355. const style = computed(() => {
  8356. return {};
  8357. });
  8358. const show = () => {
  8359. visible.value = true;
  8360. };
  8361. const hide = () => {
  8362. visible.value = false;
  8363. };
  8364. const onBack = () => {
  8365. emit("back");
  8366. hide();
  8367. };
  8368. return {
  8369. visible,
  8370. style,
  8371. show,
  8372. hide,
  8373. onBack,
  8374. };
  8375. },
  8376. render() {
  8377. const SsIcon = resolveComponent("ss-icon");
  8378. return this.visible
  8379. ? h(
  8380. "div",
  8381. {
  8382. class: "errorDialog",
  8383. style: this.style,
  8384. onClick: (event) => event.stopPropagation(),
  8385. },
  8386. [
  8387. h("div", { class: "body" }, [
  8388. h("div", { class: "left" }, [
  8389. h("div", { class: "icon" }, [
  8390. h(SsIcon, { name: "close", size: "36px" }),
  8391. ]),
  8392. ]),
  8393. h("div", { class: "right" }, [
  8394. h("div", { class: "title" }, "操作失败"),
  8395. h("div", { class: "desc" }, "请点击返回以继续。"),
  8396. ]),
  8397. ]),
  8398. h("div", { class: "footer" }, [
  8399. h("div", { class: "left" }),
  8400. h("div", { class: "right" }, [
  8401. h(
  8402. "div",
  8403. {
  8404. class: "btn",
  8405. onClick: this.onBack,
  8406. },
  8407. [h(SsIcon, { name: "arrow-left-line" }), h("div", "返回")]
  8408. ),
  8409. ]),
  8410. ]),
  8411. ]
  8412. )
  8413. : null;
  8414. },
  8415. };
  8416. /**
  8417. * 审核链条
  8418. * @name ss-verify
  8419. * @param { Array } verify-list 审核节点列表
  8420. * @property { Array } verify-list 审核节点列表
  8421. * @example <ss-verify :verify-list="verifyList"></ss-verify>
  8422. * verify-list [
  8423. * {
  8424. * groupName: "", // 群组名称
  8425. * open: true, // 默认是否展开
  8426. * children:[ //群组里的人员
  8427. * {
  8428. * thumb: "images/example/user-4.png", // 头像
  8429. * name: "李丽思 ", // 姓名
  8430. * role: "人事处处长", // 角色
  8431. * description: "同意。", // 审核意见
  8432. * time: "09:38 08/11", // 审核时间
  8433. * video: false, // false不显示/true显示 视频icon
  8434. * link: false, // false不显示/true显示 链接icon 后续应该是附件
  8435. * }
  8436. * ]
  8437. * }
  8438. * ]
  8439. */
  8440. const SsVerify = {
  8441. name: "SsVerify",
  8442. props: {
  8443. verifyList: {
  8444. type: Array,
  8445. required: true,
  8446. },
  8447. },
  8448. setup(props) {
  8449. const toggleOpen = (item) => {
  8450. item.open = !item.open;
  8451. };
  8452. onMounted(() => {
  8453. setTimeout(() => {
  8454. const lastOpenGroup = document.querySelector(".group-item-last-open");
  8455. console.log("lastOpenGroup", lastOpenGroup);
  8456. if (lastOpenGroup) {
  8457. const nodes = $(lastOpenGroup).find(".verify-node-container");
  8458. if (nodes.length) {
  8459. let totalHeight = 0;
  8460. const gudingHeight = 100;
  8461. if (nodes.length === 1) {
  8462. totalHeight = gudingHeight;
  8463. } else {
  8464. // 累加除最后一个节点外的所有节点高度
  8465. for (let i = 0; i < nodes.length - 1; i++) {
  8466. totalHeight += $(nodes[i]).outerHeight();
  8467. }
  8468. totalHeight += gudingHeight;
  8469. }
  8470. console.log("节点信息:", {
  8471. 节点总数: nodes.length,
  8472. 计算后的高度: totalHeight,
  8473. });
  8474. lastOpenGroup.style.setProperty(
  8475. "--group-line-height",
  8476. `${totalHeight}px`
  8477. );
  8478. }
  8479. }
  8480. }, 0);
  8481. });
  8482. return {
  8483. toggleOpen,
  8484. };
  8485. },
  8486. render() {
  8487. const SsIcon = resolveComponent("ss-icon");
  8488. const SsCommonIcon = resolveComponent("ss-common-icon");
  8489. const SsVerifyNode = resolveComponent("ss-verify-node");
  8490. return h(
  8491. "div",
  8492. { class: "verify-nodes" },
  8493. this.verifyList.map((item, i) =>
  8494. h(
  8495. "div",
  8496. {
  8497. key: i,
  8498. class: {
  8499. "group-item": true,
  8500. "group-item-last-open":
  8501. i === this.verifyList.length - 1 && item.open,
  8502. },
  8503. },
  8504. [
  8505. h(
  8506. "div",
  8507. {
  8508. class: "group-item-title",
  8509. onClick: () => this.toggleOpen(item),
  8510. },
  8511. [
  8512. h("div", { class: "icon" }, [
  8513. item.open
  8514. ? h(SsCommonIcon, { class: "common-icon-folder-open" })
  8515. : h(SsCommonIcon, { class: "common-icon-folder-close" }),
  8516. h(
  8517. "div",
  8518. {
  8519. class: "num",
  8520. style: { top: item.open ? "60%" : "55%" },
  8521. },
  8522. item.children?.length || 0
  8523. ),
  8524. ]),
  8525. h("div", { class: "name" }, item.groupName),
  8526. ]
  8527. ),
  8528. item.open && item.children?.length > 0
  8529. ? h(
  8530. "div",
  8531. { class: "group-item-children" },
  8532. item.children.map((citem, j) =>
  8533. h(SsVerifyNode, {
  8534. key: j,
  8535. item: citem,
  8536. // isGroup: i + 1 !== this.verifyList.length,
  8537. isGroup: true,
  8538. })
  8539. )
  8540. )
  8541. : null,
  8542. ]
  8543. )
  8544. )
  8545. );
  8546. },
  8547. };
  8548. /**
  8549. * 审核页面的审核节点
  8550. * @name ss-verify-node
  8551. * @param {Object} item 审核节点信息
  8552. * @param {Boolean} isGroup 是否为分组节点
  8553. */
  8554. const SsVerifyNode = {
  8555. name: "SsVerifyNode",
  8556. props: {
  8557. item: {
  8558. type: Object,
  8559. required: true,
  8560. },
  8561. isGroup: {
  8562. type: Boolean,
  8563. default: false,
  8564. },
  8565. },
  8566. render() {
  8567. const SsIcon = resolveComponent("ss-icon");
  8568. const SsCommonIcon = resolveComponent("ss-common-icon");
  8569. return Vue.h("div", { class: "verify-node-container" }, [
  8570. Vue.h("div", { class: "info" }, [
  8571. Vue.h("div", { class: "avatar" }, [
  8572. Vue.h("img", {
  8573. src: this.item.thumb,
  8574. style: {
  8575. width: "50px",
  8576. height: "50px",
  8577. borderRadius: "50%",
  8578. },
  8579. }),
  8580. ]),
  8581. Vue.h("div", { class: "desc" }, [
  8582. Vue.h("div", this.item.name),
  8583. Vue.h("div", this.item.role),
  8584. ]),
  8585. Vue.h("div", { class: "link" }, [
  8586. Vue.h("div", [
  8587. this.item.video
  8588. ? Vue.h(SsCommonIcon, { class: "common-icon-video" })
  8589. : null,
  8590. this.item.link
  8591. ? Vue.h(SsCommonIcon, {
  8592. class: "common-icon-paper-clip",
  8593. })
  8594. : null,
  8595. ]),
  8596. ]),
  8597. ]),
  8598. Vue.h(
  8599. "div",
  8600. {
  8601. class: {
  8602. description: true,
  8603. link: this.isGroup,
  8604. },
  8605. attrs: { "data-num": "3" },
  8606. },
  8607. [Vue.h("div", this.item.description)]
  8608. ),
  8609. Vue.h("div", { class: "time" }, this.item.time),
  8610. ]);
  8611. },
  8612. };
  8613. /**
  8614. * 智能识别图片的左侧图片播放 可以放大缩小旋转图片
  8615. * @name ss-orc-img-box
  8616. * @param { Object } image-obj 包含图片的url, 和图片的名称
  8617. *
  8618. */
  8619. const SsOrcImgBox = {
  8620. name: "SsOrcImgBox",
  8621. props: {
  8622. imageObj: {
  8623. type: Object,
  8624. required: true,
  8625. },
  8626. },
  8627. setup(props) {
  8628. const zoom = ref(1);
  8629. const rotation = ref(0);
  8630. const containerWidth = ref(0);
  8631. const containerHeight = ref(0);
  8632. const container = ref(null);
  8633. const imgPosition = ref({ x: 0, y: 0 });
  8634. const isDragging = ref(false);
  8635. const lastMousePosition = ref({ x: 0, y: 0 });
  8636. const imgStyle = computed(() => ({
  8637. width: `${zoom.value * 100}%`,
  8638. height: `${zoom.value * 100}%`,
  8639. transform: `rotate(${rotation.value}deg) translate(${imgPosition.value.x}px, ${imgPosition.value.y}px)`,
  8640. transformOrigin: "center center",
  8641. cursor: isDragging.value ? "grabbing" : "grab",
  8642. }));
  8643. const resetZoom = () => {
  8644. zoom.value = 1;
  8645. rotation.value = rotation.value + 90;
  8646. imgPosition.value = { x: 0, y: 0 };
  8647. };
  8648. const handleRangeChange = (event) => {
  8649. const value = event.target.value / 50; // 0 到 100 映射到 0 到 2 的缩放
  8650. zoom.value = Math.max(value, 0.1); // 设置最小缩放值为 0.1
  8651. };
  8652. const updateImgBoxDimensions = () => {
  8653. if (container.value) {
  8654. containerWidth.value = container.value.clientWidth;
  8655. containerHeight.value = container.value.clientHeight;
  8656. }
  8657. };
  8658. const onMouseDown = (event) => {
  8659. isDragging.value = true;
  8660. lastMousePosition.value = { x: event.clientX, y: event.clientY };
  8661. };
  8662. const onMouseMove = (event) => {
  8663. if (isDragging.value) {
  8664. const dx = event.clientX - lastMousePosition.value.x;
  8665. const dy = event.clientY - lastMousePosition.value.y;
  8666. // 防止旋转后拖动的x,y反转
  8667. // 首先将当前旋转角度从度数转换为弧度,因为 JavaScript 的 Math 库使用弧度
  8668. const angle = rotation.value * (Math.PI / 180);
  8669. // 使用基本的二维旋转矩阵将原始位移 dx 和 dy 转换为旋转后的位移 rotatedDx 和 rotatedDy。
  8670. const rotatedDx = dx * Math.cos(angle) + dy * Math.sin(angle);
  8671. const rotatedDy = dy * Math.cos(angle) - dx * Math.sin(angle);
  8672. imgPosition.value = {
  8673. x: imgPosition.value.x + rotatedDx,
  8674. y: imgPosition.value.y + rotatedDy,
  8675. };
  8676. lastMousePosition.value = { x: event.clientX, y: event.clientY };
  8677. }
  8678. };
  8679. const onMouseUp = () => {
  8680. isDragging.value = false;
  8681. };
  8682. onMounted(() => {
  8683. nextTick(() => {
  8684. updateImgBoxDimensions();
  8685. window.addEventListener("resize", updateImgBoxDimensions);
  8686. window.addEventListener("mousemove", onMouseMove);
  8687. window.addEventListener("mouseup", onMouseUp);
  8688. });
  8689. });
  8690. return {
  8691. zoom,
  8692. rotation,
  8693. container,
  8694. imgStyle,
  8695. resetZoom,
  8696. handleRangeChange,
  8697. containerWidth,
  8698. containerHeight,
  8699. onMouseDown,
  8700. imgPosition,
  8701. };
  8702. },
  8703. render() {
  8704. const SsIcon = resolveComponent("ss-icon");
  8705. return h("div", { class: "ocr-img-box" }, [
  8706. h("div", { class: "img-bar" }, [
  8707. h("div", this.imageObj.name),
  8708. h("div", { class: "action-bar" }, [
  8709. h("div", { class: "ocr-img-range-box" }, [
  8710. h("input", {
  8711. type: "range",
  8712. min: 0,
  8713. max: 100,
  8714. value: this.zoom * 50, // 初始位置为50
  8715. onInput: this.handleRangeChange,
  8716. }),
  8717. h("span", { class: "line" }),
  8718. ]),
  8719. h(SsIcon, {
  8720. name: "reset",
  8721. size: "26px",
  8722. onClick: this.resetZoom,
  8723. }),
  8724. ]),
  8725. ]),
  8726. h("div", { class: "img-viewer", ref: "container" }, [
  8727. h(
  8728. "div",
  8729. {
  8730. class: "img-box",
  8731. style: {
  8732. width: `${this.containerWidth}px`,
  8733. height: `${this.containerHeight}px`,
  8734. overflow: "hidden",
  8735. position: "relative",
  8736. },
  8737. },
  8738. [
  8739. h("img", {
  8740. src: this.imageObj.thumb,
  8741. style: this.imgStyle,
  8742. class: "zoomable-img",
  8743. onMousedown: this.onMouseDown,
  8744. }),
  8745. ]
  8746. ),
  8747. ]),
  8748. ]);
  8749. },
  8750. };
  8751. // 搜索输入框组件
  8752. const SsSearchInput = {
  8753. name: "SsSearchInput",
  8754. props: {
  8755. name: String,
  8756. placeholder: String,
  8757. width: {
  8758. type: String,
  8759. default: "100px",
  8760. },
  8761. modelValue: String,
  8762. },
  8763. emits: ["update:modelValue", "search"],
  8764. setup(props, { emit }) {
  8765. const handleInput = (e) => {
  8766. emit("update:modelValue", e.target.value);
  8767. };
  8768. const handleKeyup = (e) => {
  8769. if (e.key === "Enter") {
  8770. emit("search");
  8771. }
  8772. };
  8773. return { handleInput, handleKeyup };
  8774. },
  8775. render() {
  8776. return h(
  8777. "div",
  8778. {
  8779. class: "input",
  8780. style: this.width ? { width: this.width } : undefined,
  8781. },
  8782. [
  8783. h("input", {
  8784. name: this.name,
  8785. placeholder: this.placeholder,
  8786. value: this.modelValue,
  8787. onInput: this.handleInput,
  8788. onKeyup: this.handleKeyup,
  8789. }),
  8790. ]
  8791. );
  8792. },
  8793. };
  8794. // ss-search-date-picker 日期时间选择器组件
  8795. const SsSearchDatePicker = {
  8796. name: "SsSearchDatePicker",
  8797. props: {
  8798. modelValue: {
  8799. type: [String, Number, Date],
  8800. default: "",
  8801. },
  8802. name: {
  8803. type: String,
  8804. required: true,
  8805. },
  8806. type: {
  8807. type: String,
  8808. default: "date",
  8809. validator: (value) => ["date", "datetime", "time"].includes(value),
  8810. },
  8811. fmt: {
  8812. type: String,
  8813. default: null,
  8814. },
  8815. placeholder: {
  8816. type: String,
  8817. default: "",
  8818. },
  8819. width: {
  8820. type: String,
  8821. default: "100%",
  8822. },
  8823. },
  8824. emits: ["update:modelValue"],
  8825. setup(props, { emit }) {
  8826. const errMsg = ref("");
  8827. const validate = () => {
  8828. if (window.ssVm) {
  8829. const result = window.ssVm.validateField(props.name);
  8830. console.log("validate", window.ssVm.validateField(props.name));
  8831. errMsg.value = result.valid ? "" : result.message;
  8832. }
  8833. };
  8834. // 根据type确定默认格式
  8835. const defaultFormat = computed(() => {
  8836. switch (props.type) {
  8837. case "datetime":
  8838. return "YYYY-MM-DD HH:mm:ss";
  8839. case "date":
  8840. return "YYYY-MM-DD";
  8841. case "time":
  8842. return "HH:mm:ss";
  8843. }
  8844. });
  8845. const convertJavaFormatToElement = (javaFormat) => {
  8846. if (!javaFormat) return null;
  8847. return javaFormat
  8848. .replace("yyyy", "YYYY")
  8849. .replace("MM", "MM")
  8850. .replace("dd", "DD")
  8851. .replace("HH", "HH")
  8852. .replace("mm", "mm")
  8853. .replace("ss", "ss");
  8854. };
  8855. const finalFormat = computed(() => {
  8856. if (props.fmt) {
  8857. return convertJavaFormatToElement(props.fmt);
  8858. }
  8859. return defaultFormat.value;
  8860. });
  8861. // 使用 resolveComponent 获取组件
  8862. const ElDatePicker = resolveComponent("ElDatePicker");
  8863. const ElTimePicker = resolveComponent("ElTimePicker");
  8864. const SsFormIcon = resolveComponent("SsFormIcon");
  8865. const ElIcon = resolveComponent("ElIcon");
  8866. let useTimePicker = true;
  8867. //"yyyy-MM-dd HH:mm:ss"; "日期字符串格式在java的写法",传到本组件fmt属性也是按这个格式
  8868. if (props.fmt) {
  8869. //有fmt属性,则以fmt属性优先判断类型
  8870. if (/[dMy]/.test(props.fmt)) {
  8871. //如果有传入日期格式,且含年月日
  8872. useTimePicker = false;
  8873. } else {
  8874. useTimePicker = true;
  8875. }
  8876. } else if (props.type !== "time") {
  8877. useTimePicker = false;
  8878. }
  8879. const dateType = computed(() => {
  8880. const fmt = props.fmt || "";
  8881. if (fmt.includes("HH:mm:ss")) {
  8882. return "datetime";
  8883. } else if (fmt.includes("HH:mm")) {
  8884. return "datetime";
  8885. } else if (fmt.includes("mm:ss")) {
  8886. return "time";
  8887. }
  8888. return "date";
  8889. });
  8890. const handleValueUpdate = (val) => {
  8891. emit("update:modelValue", val);
  8892. emit("change", val); // 同时触发 change 事件
  8893. setTimeout(() => {
  8894. validate();
  8895. }, 50);
  8896. };
  8897. return () =>
  8898. h(
  8899. "div",
  8900. { class: "ss-search-date-picker", style: { width: props.width } },
  8901. [
  8902. h("input", {
  8903. type: "hidden",
  8904. name: props.name,
  8905. value: props.modelValue,
  8906. }),
  8907. h(useTimePicker ? ElTimePicker : ElDatePicker, {
  8908. modelValue: props.modelValue,
  8909. "onUpdate:modelValue": handleValueUpdate,
  8910. type: dateType.value,
  8911. format: finalFormat.value,
  8912. "value-format": finalFormat.value,
  8913. clearable: true,
  8914. placeholder: props.placeholder,
  8915. class: "custom-date-picker", // 用于自定义样式
  8916. "time-arrow-control": props.type === "datetime", // 修改这里
  8917. size: "large", // 添加这一行,改为 large 尺寸
  8918. style: { width: "100%" },
  8919. "prefix-icon": h(SsFormIcon, { class: "form-icon-time" }),
  8920. }),
  8921. ]
  8922. );
  8923. },
  8924. };
  8925. // 搜索按钮组件(包含下拉按钮)
  8926. const SsSearchButton = {
  8927. name: "SsSearchButton",
  8928. props: {
  8929. text: {
  8930. type: String,
  8931. required: true,
  8932. },
  8933. iconClass: {
  8934. type: String,
  8935. required: false,
  8936. },
  8937. opt: {
  8938. type: Array,
  8939. default: () => [],
  8940. },
  8941. checkId: {
  8942. type: String,
  8943. default: "0",
  8944. },
  8945. width: {
  8946. //add by Ben(20251225)
  8947. type: String,
  8948. required: false,
  8949. },
  8950. id: {
  8951. //add by Ben(20251225)
  8952. type: String,
  8953. required: false,
  8954. },
  8955. },
  8956. emits: ["click"],
  8957. setup(props, { emit }) {
  8958. const currentId = ref(props.checkId || "0");
  8959. const showPopup = ref(false);
  8960. const handleMouseEnter = () => {
  8961. showPopup.value = true;
  8962. };
  8963. const handleMouseLeave = () => {
  8964. showPopup.value = false;
  8965. };
  8966. // 添加点击事件处理,阻止默认行为
  8967. const handleClick = (e) => {
  8968. e.preventDefault();
  8969. if (props.opt?.length > 0) {
  8970. const selectedOption =
  8971. currentId.value === "0"
  8972. ? props.opt[0]
  8973. : props.opt.find((opt) => opt.id === currentId.value);
  8974. if (selectedOption) {
  8975. selectedOption.callback?.();
  8976. }
  8977. } else {
  8978. emit("click", e);
  8979. }
  8980. };
  8981. // 获取显示文本
  8982. const getDisplayText = () => {
  8983. if (!props.opt?.length) return props.text;
  8984. const selectedOption =
  8985. currentId.value === "0"
  8986. ? props.opt[0]
  8987. : props.opt.find((opt) => opt.id === currentId.value);
  8988. return selectedOption ? selectedOption.desc : props.opt[0].desc;
  8989. };
  8990. return () =>
  8991. h(
  8992. "button",
  8993. {
  8994. class:
  8995. props.opt?.length > 0
  8996. ? "ss-drop-button ss-drop-button-more"
  8997. : "ss-drop-button",
  8998. type: "button", // 明确指定按钮类型为 button
  8999. onMouseenter: handleMouseEnter,
  9000. onMouseleave: handleMouseLeave,
  9001. onClick: handleClick, // 添加点击事件处理
  9002. style: { width: props.width }, //add by Ben(20251225)
  9003. id: props.id, //add by Ben(20251225)
  9004. },
  9005. [
  9006. props.iconClass
  9007. ? h("span", {
  9008. class: props.iconClass,
  9009. style: { fontFamily: "iconfont", marginRight: "5px" },
  9010. })
  9011. : null,
  9012. h("span", getDisplayText()),
  9013. props.opt.length > 0 &&
  9014. showPopup.value &&
  9015. h(
  9016. "div",
  9017. {
  9018. class: "popup",
  9019. },
  9020. props.opt.map((item) =>
  9021. h(
  9022. "div",
  9023. {
  9024. onClick: (e) => {
  9025. e.preventDefault(); // 选项点击也阻止默认行为
  9026. e.stopPropagation(); // 阻止事件冒泡
  9027. currentId.value = item.id; // 更新当前选中的ID
  9028. item.callback();
  9029. showPopup.value = false; // 选择后关闭弹窗
  9030. },
  9031. },
  9032. item.desc
  9033. )
  9034. )
  9035. ),
  9036. ]
  9037. );
  9038. },
  9039. };
  9040. // 下拉按钮组件
  9041. const SsDropButton = {
  9042. name: "SsDropButton",
  9043. props: {
  9044. text: {
  9045. type: String,
  9046. required: true,
  9047. },
  9048. iconClass: {
  9049. type: String,
  9050. required: true,
  9051. },
  9052. opt: {
  9053. type: Array,
  9054. default: () => [],
  9055. },
  9056. checkId: {
  9057. type: String,
  9058. default: "0",
  9059. },
  9060. onclick: {
  9061. type: Function,
  9062. default: null,
  9063. },
  9064. },
  9065. setup(props) {
  9066. const currentId = ref(props.checkId || "0");
  9067. const showPopup = ref(false);
  9068. const handleMouseEnter = () => {
  9069. showPopup.value = true;
  9070. };
  9071. const handleMouseLeave = () => {
  9072. showPopup.value = false;
  9073. };
  9074. // 添加点击事件处理,阻止默认行为
  9075. const handleClick = (e) => {
  9076. e.preventDefault();
  9077. if (props.opt?.length > 0) {
  9078. const selectedOption =
  9079. currentId.value === "0"
  9080. ? props.opt[0]
  9081. : props.opt.find((opt) => opt.id === currentId.value);
  9082. if (selectedOption) {
  9083. selectedOption.callback?.();
  9084. }
  9085. } else if (props.onclick) {
  9086. props.onclick();
  9087. }
  9088. };
  9089. // 获取显示文本
  9090. const getDisplayText = () => {
  9091. if (!props.opt?.length) return props.text;
  9092. const selectedOption =
  9093. currentId.value === "0"
  9094. ? props.opt[0]
  9095. : props.opt.find((opt) => opt.id === currentId.value);
  9096. return selectedOption ? selectedOption.desc : props.opt[0].desc;
  9097. };
  9098. return () =>
  9099. h(
  9100. "button",
  9101. {
  9102. class:
  9103. props.opt?.length > 0
  9104. ? "ss-drop-button ss-drop-button-more"
  9105. : "ss-drop-button",
  9106. type: "button", // 明确指定按钮类型为 button
  9107. onMouseenter: handleMouseEnter,
  9108. onMouseleave: handleMouseLeave,
  9109. onClick: handleClick, // 添加点击事件处理
  9110. },
  9111. [
  9112. h("span", {
  9113. class: props.iconClass,
  9114. style: { fontFamily: "iconfont" },
  9115. }),
  9116. h("span", getDisplayText()),
  9117. props.opt.length > 0 &&
  9118. showPopup.value &&
  9119. h(
  9120. "div",
  9121. {
  9122. class: "popup",
  9123. },
  9124. props.opt.map((item) =>
  9125. h(
  9126. "div",
  9127. {
  9128. onClick: (e) => {
  9129. e.preventDefault(); // 选项点击也阻止默认行为
  9130. e.stopPropagation(); // 阻止事件冒泡
  9131. currentId.value = item.id; // 更新当前选中的ID
  9132. item.callback();
  9133. showPopup.value = false; // 选择后关闭弹窗
  9134. },
  9135. },
  9136. item.desc
  9137. )
  9138. )
  9139. ),
  9140. ]
  9141. );
  9142. },
  9143. };
  9144. /**
  9145. * 二级页面标签组件
  9146. * @name ss-sub-tab
  9147. * @description 用于展示二级页面的布局组件,包含左侧垂直标签导航(支持分组)和右侧iframe内容区
  9148. * @property {String} headerImage - 左侧顶部图片地址
  9149. * @property {Array} menuList - 菜单配置列表
  9150. * @property {Object} [activeMenu] - 当前选中的菜单项,不传则自动选择第一个可选菜单
  9151. * @property {Array} footerButtons - 底部按钮配置列表
  9152. */
  9153. /**
  9154. * SsSubTab 左侧菜单+iframe内容组件
  9155. * v3.0 改造:去掉顶部图片,改为图标+悬浮模式,iframe懒加载 by xu 20251216
  9156. */
  9157. const SsSubTab = {
  9158. name: "SsSubTab",
  9159. props: {
  9160. menuList: {
  9161. type: Array,
  9162. required: true,
  9163. },
  9164. activeMenu: {
  9165. type: String,
  9166. default: "",
  9167. },
  9168. footerButtons: {
  9169. type: Array,
  9170. default: () => [],
  9171. },
  9172. leftDisplay: {
  9173. type: Boolean,
  9174. default: true,
  9175. },
  9176. // v3.0 新增:菜单模式 collapse(悬浮展开) / fixed(始终收起) by xu 20251216
  9177. initialMode: {
  9178. type: String,
  9179. default: "collapse",
  9180. },
  9181. },
  9182. emits: ["menu-change", "footer-click"],
  9183. setup(props, { emit }) {
  9184. // v3.0 新增:默认图标映射,使用icon-biz图标 by xu 20251216
  9185. const defaultIcons = [
  9186. "icon-obj-ry", // 人员
  9187. "icon-obj-dw", // 单位
  9188. "icon-obj-gw", // 岗位
  9189. "icon-biz-rc", // 人才
  9190. "icon-biz-xc", // 巡查
  9191. "icon-biz-cl", // 材料
  9192. "icon-biz-men", // 门
  9193. "icon-obj-xy", // 协议
  9194. ];
  9195. //功能: SsSubTab 支持后端下发 iconName + pobj/cobj 两级菜单 by xu 20251222
  9196. const isTrue = (v) => v === true || v === "true" || v === 1 || v === "1"; //功能 by xu 20251222
  9197. const resolveIconClass = (iconNameOrClass, fallbackIndex) => {
  9198. //功能 by xu 20251222
  9199. const fallback = `menu-icon ${
  9200. defaultIcons[fallbackIndex % defaultIcons.length]
  9201. }`;
  9202. if (!iconNameOrClass) {
  9203. return fallback;
  9204. }
  9205. // 已经是完整 class(可能包含 menu-icon / menu-base-icon / 多个 class)
  9206. if (
  9207. typeof iconNameOrClass === "string" &&
  9208. iconNameOrClass.indexOf(" ") > -1
  9209. ) {
  9210. return iconNameOrClass;
  9211. }
  9212. const iconName = iconNameOrClass;
  9213. if (iconName === "menu-icon" || iconName === "menu-base-icon") {
  9214. return fallback;
  9215. }
  9216. // 业务图标库:icon-biz / icon-obj -> menu-icon
  9217. if (
  9218. typeof iconName === "string" &&
  9219. (iconName.indexOf("icon-obj-") === 0 ||
  9220. iconName.indexOf("icon-biz-") === 0)
  9221. ) {
  9222. return `menu-icon ${iconName}`;
  9223. }
  9224. // 默认认为是 icon-base 图标 -> menu-base-icon
  9225. return `menu-base-icon ${iconName}`;
  9226. };
  9227. const getMenuIcon = (item, index) => {
  9228. //功能 by xu 20251222
  9229. if (!item) {
  9230. return resolveIconClass(null, index);
  9231. }
  9232. //功能: 变动图标后端暂不正确,前端先写死为 icon-chg by xu 20251223
  9233. if (item.title === "变动" || item.name === "sys_bd") {
  9234. return resolveIconClass("icon-chg", index);
  9235. }
  9236. // 兼容旧字段 icon(优先使用)
  9237. if (item.icon) return resolveIconClass(item.icon, index);
  9238. // v3.0 使用后端下发 iconName
  9239. if (item.iconName) return resolveIconClass(item.iconName, index);
  9240. return resolveIconClass(null, index);
  9241. };
  9242. //功能: SsSubTab 底部按钮支持 icon+文字(icon-base)by xu 20251224
  9243. const getFooterIcon = (button) => {
  9244. //功能 by xu 20251224
  9245. if (!button) return "menu-base-icon icon-subm";
  9246. const iconNameOrClass =
  9247. button.iconClass || button.iconName || button.icon;
  9248. if (!iconNameOrClass) return "menu-base-icon icon-subm";
  9249. if (
  9250. typeof iconNameOrClass === "string" &&
  9251. iconNameOrClass.indexOf(" ") > -1
  9252. ) {
  9253. return iconNameOrClass;
  9254. }
  9255. return `menu-base-icon ${iconNameOrClass}`;
  9256. };
  9257. //功能: pobj/cobj 扁平结构转换为 children 树结构,兼容原 children 结构 by xu 20251222
  9258. const normalizeMenuList = (rawList) => {
  9259. if (!Array.isArray(rawList) || rawList.length === 0) {
  9260. return [];
  9261. }
  9262. const hasTree = rawList.some(
  9263. (it) => Array.isArray(it?.children) && it.children.length > 0
  9264. );
  9265. if (hasTree) {
  9266. return rawList.map((it) => ({
  9267. ...it,
  9268. __level: 1,
  9269. children: Array.isArray(it.children)
  9270. ? it.children.map((c) => ({ ...c, __level: 2 }))
  9271. : it.children,
  9272. }));
  9273. }
  9274. const hasMarker = rawList.some(
  9275. (it) => it && ("pobj" in it || "cobj" in it)
  9276. );
  9277. if (!hasMarker) {
  9278. return rawList.map((it) => ({ ...it, __level: 1 }));
  9279. }
  9280. const result = [];
  9281. let currentGroup = null;
  9282. for (const item of rawList) {
  9283. //功能: “变动”始终按一级处理(即使后端误传 pobj/cobj)by xu 20251223
  9284. const isChgItem =
  9285. item && (item.title === "变动" || item.name === "sys_bd");
  9286. if (isChgItem) {
  9287. result.push({ ...item, __level: 1 });
  9288. continue;
  9289. }
  9290. const isParent = isTrue(item?.pobj);
  9291. const isChild = isTrue(item?.cobj);
  9292. if (isParent) {
  9293. currentGroup = {
  9294. ...item,
  9295. __level: 1,
  9296. children: [],
  9297. };
  9298. result.push(currentGroup);
  9299. continue;
  9300. }
  9301. if (isChild && currentGroup) {
  9302. currentGroup.children.push({ ...item, __level: 2 });
  9303. continue;
  9304. }
  9305. //功能: 变动等无 pobj/cobj 的选项按一级展示(不挂到 children,也不打断当前分组)by xu 20251223
  9306. result.push({ ...item, __level: 1 });
  9307. }
  9308. return result;
  9309. };
  9310. const menuListComputed = computed(() =>
  9311. normalizeMenuList(props.menuList)
  9312. ); //功能 by xu 20251222
  9313. //功能: 分组展开状态(默认展开),避免 computed 生成对象导致 open 状态丢失 by xu 20251222
  9314. const groupOpenState = reactive({}); // { [key]: boolean }
  9315. const getGroupKey = (item) => item?.name || item?.title || ""; //功能 by xu 20251222
  9316. const isGroupOpen = (item) => {
  9317. //功能 by xu 20251222
  9318. const key = getGroupKey(item);
  9319. if (!key) return true;
  9320. return groupOpenState[key] !== false;
  9321. };
  9322. const toggleGroupOpen = (item) => {
  9323. //功能 by xu 20251222
  9324. const key = getGroupKey(item);
  9325. if (!key) return;
  9326. groupOpenState[key] = !isGroupOpen(item);
  9327. };
  9328. const getLevelClass = (item, fallbackLevel) => {
  9329. //功能 by xu 20251222
  9330. //功能: “变动”始终按一级样式处理 by xu 20251223
  9331. if (item && (item.title === "变动" || item.name === "sys_bd")) {
  9332. return "level-1";
  9333. }
  9334. const level = item?.__level || fallbackLevel || 1;
  9335. return level === 2 ? "level-2" : "level-1";
  9336. };
  9337. // v3.0 新增:菜单模式管理 by xu 20251216
  9338. const menuMode = ref(props.initialMode);
  9339. const isHovering = ref(false);
  9340. const toggleMenuMode = () => {
  9341. menuMode.value = menuMode.value === "collapse" ? "fixed" : "collapse";
  9342. };
  9343. //功能: 提供给旧UI弹窗顶部按钮调用的 API(替代点击 .menu-mode-toggle DOM)by xu 20251224
  9344. const registerSsSubTabApi = () => {
  9345. //功能 by xu 20251224
  9346. try {
  9347. window.SS = window.SS || {};
  9348. window.SS.dom = window.SS.dom || {};
  9349. //功能: 兼容小写 ss 命名空间(部分页面只引用 window.ss)by xu 20251224
  9350. window.ss = window.ss || window.SS;
  9351. window.ss.dom = window.ss.dom || window.SS.dom;
  9352. const api = {
  9353. toggleMenuMode,
  9354. getMenuMode: () => menuMode.value,
  9355. };
  9356. window.SS.dom.ssSubTabApi = api;
  9357. window.ss.dom.ssSubTabApi = api;
  9358. //功能: 多层弹窗(如 objPlay -> objInfo) 场景,将 API 注册到 topWindow 供按钮跨层调用 by xu 20251224
  9359. try {
  9360. const wdDialogId =
  9361. window.wd &&
  9362. wd.display &&
  9363. wd.display.getwdDialogId &&
  9364. wd.display.getwdDialogId();
  9365. if (wdDialogId && window.top) {
  9366. window.top.__ssSubTabApiMap = window.top.__ssSubTabApiMap || {};
  9367. window.top.__ssSubTabApiMap[wdDialogId] = api;
  9368. }
  9369. } catch (e) {}
  9370. try {
  9371. console.log(
  9372. "[SsSubTabApi] registered",
  9373. window.location && window.location.pathname
  9374. );
  9375. } catch (e) {}
  9376. } catch (e) {}
  9377. };
  9378. const unregisterSsSubTabApi = () => {
  9379. //功能 by xu 20251224
  9380. try {
  9381. //功能: 从 topWindow 解绑(避免弹窗关闭后残留)by xu 20251224
  9382. try {
  9383. const wdDialogId =
  9384. window.wd &&
  9385. wd.display &&
  9386. wd.display.getwdDialogId &&
  9387. wd.display.getwdDialogId();
  9388. if (
  9389. wdDialogId &&
  9390. window.top &&
  9391. window.top.__ssSubTabApiMap &&
  9392. window.top.__ssSubTabApiMap[wdDialogId]
  9393. ) {
  9394. delete window.top.__ssSubTabApiMap[wdDialogId];
  9395. }
  9396. } catch (e) {}
  9397. if (window.SS?.dom?.ssSubTabApi?.toggleMenuMode === toggleMenuMode) {
  9398. delete window.SS.dom.ssSubTabApi;
  9399. }
  9400. if (window.ss?.dom?.ssSubTabApi?.toggleMenuMode === toggleMenuMode) {
  9401. delete window.ss.dom.ssSubTabApi;
  9402. }
  9403. } catch (e) {}
  9404. };
  9405. //功能: 立即注册,避免 enable 早于 onMounted 导致“api not ready”by xu 20251224
  9406. registerSsSubTabApi(); //功能 by xu 20251224
  9407. onBeforeUnmount(unregisterSsSubTabApi); //功能 by xu 20251224
  9408. const onMouseEnter = () => {
  9409. if (menuMode.value === "collapse") {
  9410. isHovering.value = true;
  9411. }
  9412. };
  9413. const onMouseLeave = () => {
  9414. isHovering.value = false;
  9415. };
  9416. const isExpanded = computed(() => {
  9417. return menuMode.value === "collapse" && isHovering.value;
  9418. });
  9419. // v3.0 新增:iframe 懒加载,点击才加载 by xu 20251216
  9420. const loadedMenus = ref(new Set());
  9421. const isMenuLoaded = (menuName) => {
  9422. return loadedMenus.value.has(menuName);
  9423. };
  9424. // 根据标题找到对应的菜单项
  9425. const findMenuByTitle = (title) => {
  9426. for (const item of menuListComputed.value) {
  9427. //功能 by xu 20251222
  9428. if (item.children?.length > 0) {
  9429. const child = item.children.find((c) => c.title === title);
  9430. if (child) return child;
  9431. } else if (item.title === title) {
  9432. return item;
  9433. }
  9434. }
  9435. return null;
  9436. };
  9437. // 计算默认选中的菜单项
  9438. const defaultActiveMenu = computed(() => {
  9439. if (props.activeMenu) {
  9440. const menu = findMenuByTitle(props.activeMenu);
  9441. if (menu) return menu;
  9442. }
  9443. const firstItem = menuListComputed.value[0]; //功能 by xu 20251222
  9444. if (!firstItem) return null;
  9445. //功能: 默认选中第一个一级菜单(不默认跳到第一个二级)by xu 20251224
  9446. return firstItem;
  9447. });
  9448. const currentMenu = ref(defaultActiveMenu.value);
  9449. // 监听外部activeMenu变化
  9450. watch(
  9451. () => props.activeMenu,
  9452. (newTitle) => {
  9453. if (newTitle) {
  9454. const menu = findMenuByTitle(newTitle);
  9455. if (menu) {
  9456. currentMenu.value = menu;
  9457. }
  9458. }
  9459. }
  9460. );
  9461. // 初始化:默认选中项加入已加载集合
  9462. watch(
  9463. currentMenu,
  9464. (menu) => {
  9465. if (menu?.name) {
  9466. loadedMenus.value.add(menu.name);
  9467. }
  9468. },
  9469. { immediate: true }
  9470. );
  9471. // 选择菜单项时触发 menu-change 钩子
  9472. const selectItem = (item) => {
  9473. currentMenu.value = item;
  9474. // 标记为已加载
  9475. if (item.name) {
  9476. loadedMenus.value.add(item.name);
  9477. }
  9478. emit("menu-change", item);
  9479. };
  9480. // 处理底部按钮点击
  9481. const handleFooterClick = (button, index) => {
  9482. emit("footer-click", { button, index });
  9483. };
  9484. return {
  9485. menuListComputed, //功能 by xu 20251222
  9486. currentMenu,
  9487. selectItem,
  9488. handleFooterClick,
  9489. getFooterIcon, //功能: SsSubTab 底部按钮支持 icon+文字(icon-base)by xu 20251224
  9490. menuMode,
  9491. isHovering,
  9492. isExpanded,
  9493. toggleMenuMode,
  9494. onMouseEnter,
  9495. onMouseLeave,
  9496. isMenuLoaded,
  9497. getMenuIcon,
  9498. isGroupOpen, //功能 by xu 20251222
  9499. toggleGroupOpen, //功能 by xu 20251222
  9500. getLevelClass, //功能 by xu 20251222
  9501. };
  9502. },
  9503. template: `
  9504. <div class="project-edit-container">
  9505. <div class="left-side"
  9506. v-if="leftDisplay"
  9507. :data-mode="menuMode"
  9508. :class="{ 'is-expanded': isExpanded }"
  9509. @mouseenter="onMouseEnter"
  9510. @mouseleave="onMouseLeave">
  9511. <!-- 菜单内容 -->
  9512. <div class="menu-content">
  9513. <div class="scroll-view">
  9514. <template v-for="(menuItem, i) in menuListComputed" :key="i">
  9515. <!-- 分组菜单 -->
  9516. <div v-if="menuItem.children?.length > 0" class="group">
  9517. <!-- 功能: 一级(pobj)可点击进入,箭头仅控制展开/收起;二级点击不影响一级选中状态 by xu 20251223 -->
  9518. <div class="menu-item"
  9519. :class="[getLevelClass(menuItem, 1), { active: menuItem.name === currentMenu?.name }]"
  9520. @click="selectItem(menuItem)">
  9521. <ss-icon :class="getMenuIcon(menuItem, i)" />
  9522. <span class="menu-label">{{ menuItem.title }}</span>
  9523. <!-- 功能: 一级菜单有子项时显示 dot(参考全局左侧菜单)by xu 20251224 -->
  9524. <div class="has-children-dot"></div>
  9525. <div class="menu-tooltip">{{ menuItem.title }}</div>
  9526. </div>
  9527. <!-- 功能: 二级菜单始终展示,不做收缩展开 by xu 20251223 -->
  9528. <div class="group-detail">
  9529. <div v-for="(item, j) in menuItem.children"
  9530. :key="j"
  9531. class="menu-item"
  9532. :class="[getLevelClass(item, 2), { active: item.name === currentMenu?.name }]"
  9533. @click.stop="selectItem(item)">
  9534. <ss-icon :class="getMenuIcon(item, j)" />
  9535. <span class="menu-label">{{ item.title }}</span>
  9536. </div>
  9537. </div>
  9538. </div>
  9539. <!-- 普通菜单项 -->
  9540. <div v-else
  9541. class="menu-item"
  9542. :class="[getLevelClass(menuItem, 1), { active: menuItem.name === currentMenu?.name }]"
  9543. @click="selectItem(menuItem)">
  9544. <ss-icon :class="getMenuIcon(menuItem, i)" />
  9545. <span class="menu-label">{{ menuItem.title }}</span>
  9546. <div class="menu-tooltip">{{ menuItem.title }}</div>
  9547. </div>
  9548. </template>
  9549. </div>
  9550. </div>
  9551. <!-- 底部按钮 -->
  9552. <div v-if="footerButtons.length > 0"
  9553. class="sub-tab-menu-footer"
  9554. :class="{ 'has-text': !!footerButtons[0].text }"
  9555. @click="footerButtons[0].onclick">
  9556. <ss-icon :class="getFooterIcon(footerButtons[0])" />
  9557. <div class="footer-label" v-if="footerButtons[0].text">{{ footerButtons[0].text }}</div>
  9558. <ss-icon v-if="footerButtons.length > 1" class="footer-arrow" name="arrow-up" size="24px" />
  9559. <div v-if="footerButtons.length > 1" class="sub-tab-menu-popup">
  9560. <div v-for="(button, index) in footerButtons.slice(1)"
  9561. :key="index"
  9562. @click.stop="button.onclick">
  9563. {{ button.text }}
  9564. </div>
  9565. </div>
  9566. </div>
  9567. </div>
  9568. <!-- 右侧内容区域 - 懒加载 iframe -->
  9569. <div class="content-area fit-height-content" style="overflow: hidden;" :style="!leftDisplay ? { width: '100%' } : {}">
  9570. <template v-for="(menuItem, i) in menuList" :key="i">
  9571. <iframe
  9572. v-if="isMenuLoaded(menuItem.name)"
  9573. :src="menuItem.url"
  9574. style="height: 100%;width: 100%;"
  9575. frameborder="0"
  9576. class="sub-tab-iframe"
  9577. :id="i === 0 ? 'sub-tab-iframe' : ''"
  9578. v-show="currentMenu?.name === menuItem.name"
  9579. />
  9580. </template>
  9581. </div>
  9582. </div>
  9583. `,
  9584. };
  9585. // <iframe
  9586. // v-if="currentMenu?.url"
  9587. // :src="currentMenu.url"
  9588. // style="height: 100%;width: 100%;"
  9589. // frameborder="0"
  9590. // id="sub-tab-iframe"
  9591. // />
  9592. // ss-photo-upload 通用图片上传组件
  9593. const SsImgUpload = {
  9594. name: "SsImgUpload",
  9595. props: {
  9596. name: {
  9597. type: String,
  9598. required: true,
  9599. },
  9600. // 图片URL,用于回显
  9601. // url: {
  9602. // type: String,
  9603. // default: "",
  9604. // },
  9605. // 样式类名
  9606. class: {
  9607. type: String,
  9608. required: true,
  9609. },
  9610. // 裁剪配置
  9611. cropperOpt: {
  9612. type: Object,
  9613. default: () => ({
  9614. width: 360,
  9615. height: 360,
  9616. aspectRatio: 1,
  9617. }),
  9618. },
  9619. //上传图片url(未加图片名参数之前的部分)
  9620. ulUrl: {
  9621. type: String,
  9622. required: true,
  9623. },
  9624. //下载图片url(未加图片名参数之前的部分)
  9625. dlUrl: {
  9626. type: String,
  9627. required: true,
  9628. },
  9629. modelValue: [String, Number],
  9630. },
  9631. emits: ["update:modelValue"],
  9632. setup(props, { emit }) {
  9633. const inputId = Vue.computed(
  9634. () => `file_${Vue.getCurrentInstance().uid}`
  9635. );
  9636. //修改图片初始显示路径
  9637. let pathVal = ref(props.modelValue);
  9638. let picUrl = ref("");
  9639. if (props.modelValue) {
  9640. picUrl.value = props.dlUrl + "&path=" + props.modelValue;
  9641. }
  9642. Vue.onMounted(() => {
  9643. window.SS.cropper.init({
  9644. el: $(`#${inputId.value}`),
  9645. photoSize: {
  9646. width: props.cropperOpt.width,
  9647. height: props.cropperOpt.height,
  9648. },
  9649. aspectRatio: props.cropperOpt.aspectRatio,
  9650. uploadUrl: props.ulUrl,
  9651. success: (path) => {
  9652. pathVal.value = path;
  9653. picUrl.value = props.dlUrl + "&path=" + path;
  9654. emit("update:modelValue", path);
  9655. },
  9656. });
  9657. });
  9658. return () =>
  9659. h("div", { class: [props.class] }, [
  9660. h("input", {
  9661. type: "file",
  9662. accept: "image/*",
  9663. id: inputId.value,
  9664. style: { display: "none" },
  9665. }),
  9666. h("input", {
  9667. type: "hidden",
  9668. name: props.name,
  9669. value: pathVal.value,
  9670. }),
  9671. h(
  9672. "div",
  9673. {
  9674. style: {
  9675. width: "100%",
  9676. height: "100%",
  9677. },
  9678. onClick: () => $(`#${inputId.value}`).click(),
  9679. },
  9680. [
  9681. picUrl.value &&
  9682. h("img", {
  9683. src: picUrl.value,
  9684. style:
  9685. "width: 100%; height: 100%;object-fit: inherit;position: relative;z-index: 11;",
  9686. }),
  9687. ]
  9688. ),
  9689. ]);
  9690. },
  9691. };
  9692. // 初始化函数,负责创建和挂载 Vue 应用
  9693. // window.SS = { dom: {} };
  9694. /**
  9695. * 获取当前窗口的父窗口
  9696. * @returns {Window} 父窗口对象
  9697. */
  9698. window.SS.topWin = (function (p, c) {
  9699. while (p != c) {
  9700. c = p;
  9701. p = p.parent;
  9702. }
  9703. return c;
  9704. })(window.parent, window);
  9705. window.SS.createSsDialogInstance = createSsDialogInstance;
  9706. /**
  9707. * 创建弹窗
  9708. * @param {Object} setting
  9709. * @param {Function} callbackEvent
  9710. */
  9711. window.SS.openDialog = function (setting, callbackEvent) {
  9712. if (setting.params) {
  9713. const encodedParams = encodeURIComponent(JSON.stringify(setting.params));
  9714. setting.src +=
  9715. (setting.src.includes("?") ? "&" : "?") + "params=" + encodedParams;
  9716. }
  9717. if (window.parent && window.parent !== window) {
  9718. window.parent.SS.createSsDialogInstance(setting, callbackEvent);
  9719. } else {
  9720. createSsDialogInstance(setting, callbackEvent);
  9721. }
  9722. };
  9723. //关闭弹窗
  9724. window.SS.closeDialog = function () {
  9725. console.log("关闭弹窗");
  9726. if (topWindow.dialogInstances.length > 0) {
  9727. const instance = topWindow.dialogInstances.pop();
  9728. console.log("instance", instance);
  9729. console.log("instance.callbackEvent", instance.callbackEvent);
  9730. console.log(
  9731. "instance.callbackEvent.end",
  9732. typeof instance.callbackEvent === "function"
  9733. );
  9734. if (instance.callbackEvent) {
  9735. // 判断是否有end回调并执行
  9736. if (typeof instance.callbackEvent === "function") {
  9737. instance.callbackEvent();
  9738. }
  9739. if (typeof instance.callbackEvent.end === "function") {
  9740. instance.callbackEvent.end();
  9741. }
  9742. }
  9743. instance.app.unmount(); // 卸载最后一个实例
  9744. if (instance.container && instance.container.parentNode) {
  9745. instance.container.parentNode.removeChild(instance.container); // 移除容器
  9746. }
  9747. }
  9748. };
  9749. /**
  9750. * 裁剪插件
  9751. */
  9752. window.SS.cropper = {
  9753. init: function (setting) {
  9754. if (!window.top.SS) window.top.SS = {};
  9755. // 重要:确保 cropper 对象的完整初始化
  9756. if (!window.top.SS.cropper) {
  9757. window.top.SS.cropper = {
  9758. settings: new Map(),
  9759. _backupSettings: {},
  9760. getSetting: this.getSetting,
  9761. clearSetting: this.clearSetting,
  9762. debug: this.debug,
  9763. };
  9764. } else if (!window.top.SS.cropper.settings) {
  9765. // 如果 cropper 存在但 settings 不存在,重新初始化 settings
  9766. window.top.SS.cropper.settings = new Map();
  9767. window.top.SS.cropper._backupSettings = {};
  9768. }
  9769. const uploaderId = `uploader_${Date.now()}_${Math.random()
  9770. .toString(36)
  9771. .substr(2, 9)}`;
  9772. window.top.SS.cropper.settings.set(uploaderId, setting);
  9773. window.top.SS.cropper._backupSettings[uploaderId] = setting;
  9774. setting.box = setting.box || "1";
  9775. var winSetting = {
  9776. headerTitle: "图片裁剪",
  9777. src: "/js/cropper/cropper.jsp", //原来在"/newUI/page/cropper.jsp" Ben(20251205)
  9778. width: "900",
  9779. height: "500",
  9780. };
  9781. $(setting.el).change(function () {
  9782. if (!window.SS.cropper.verify(setting)) {
  9783. $(setting.el).val(""); // 清空文件选择
  9784. return false;
  9785. }
  9786. var files = this.files;
  9787. if (files && files.length) {
  9788. if (!window.SS.cropper.verifySize($(setting.el)[0], 5)) {
  9789. $(setting.el).val(""); // 清空文件选择
  9790. alert("文件大小不能超过5M,请重新选择");
  9791. return false;
  9792. }
  9793. var URL = window.URL || window.webkitURL;
  9794. var file = files[0];
  9795. setting.file = file;
  9796. if (
  9797. /^image\/\w+$/.test(file.type) &&
  9798. /\.(jpg|jpeg|png|)$/i.test(file.name)
  9799. ) {
  9800. var uploadedImageURL = URL.createObjectURL(file);
  9801. setting.data = uploadedImageURL;
  9802. setting.fileName = file.name;
  9803. // console.log()
  9804. winSetting.params = {
  9805. ...setting,
  9806. uploaderId,
  9807. };
  9808. console.log("ss-componets中change之后的winSetting", winSetting);
  9809. SS.openDialog(winSetting, {
  9810. success: function (win) {
  9811. console.log("裁剪插件成功");
  9812. // win.cropperSetting = setting;
  9813. },
  9814. end: function () {
  9815. console.log("裁剪插件结束");
  9816. $(setting.el).val(""); // 清空文件选择
  9817. },
  9818. });
  9819. } else {
  9820. alert("请选择图片文件,支持jpg、jpeg、png格式");
  9821. }
  9822. }
  9823. });
  9824. return uploaderId;
  9825. },
  9826. verify: function (setting) {
  9827. if (!setting) {
  9828. console.error(" cropper setting is not undefined! ");
  9829. return false;
  9830. }
  9831. if (!setting.el) {
  9832. console.error(" cropper setting.el is not undefined! ");
  9833. return false;
  9834. }
  9835. if (setting.photoSize) {
  9836. if (
  9837. (!setting.photoSize.width && setting.photoSize.height) ||
  9838. (!setting.photoSize.height && setting.photoSize.width)
  9839. ) {
  9840. console.error(
  9841. " cropper setting.photoSize { width, height } is not undefined! "
  9842. );
  9843. return false;
  9844. }
  9845. }
  9846. if (!setting.box) {
  9847. setting.box = "1";
  9848. }
  9849. if (!setting.aspectRatio) {
  9850. setting.aspectRatio = 1 / 1;
  9851. }
  9852. return true;
  9853. },
  9854. verifySize: function (fileEl, maxSize) {
  9855. // 判断是否为IE浏览器: /msie/i.test(navigator.userAgent) 为一个简单正则
  9856. var isIE = /msie/i.test(navigator.userAgent) && !window.opera;
  9857. var fileSize = 0;
  9858. if (isIE && !fileEl.files) {
  9859. // IE浏览器
  9860. var filePath = fileEl.value; // 获得上传文件的绝对路径
  9861. var fileSystem = new ActiveXObject("Scripting.FileSystemObject");
  9862. var file = fileSystem.GetFile(filePath);
  9863. fileSize = file.Size; // 文件大小,单位:b
  9864. } else {
  9865. // 非IE浏览器
  9866. fileSize = fileEl.files[0].size;
  9867. }
  9868. var size = fileSize / 1024 / 1024;
  9869. return !(size > maxSize);
  9870. },
  9871. // 获取特定上传组件的setting
  9872. getSetting: function (uploaderId) {
  9873. if (!window.top.SS?.cropper) {
  9874. console.warn("顶层窗口中未找到 SS.cropper");
  9875. return null;
  9876. }
  9877. // 优先从 Map 中获取
  9878. let setting = window.top.SS.cropper.settings.get(uploaderId);
  9879. // 如果 Map 中没有,尝试从备份中获取
  9880. if (!setting && window.top.SS.cropper._backupSettings[uploaderId]) {
  9881. console.log("从备份中恢复 setting");
  9882. setting = window.top.SS.cropper._backupSettings[uploaderId];
  9883. // 恢复到 Map 中
  9884. window.top.SS.cropper.settings.set(uploaderId, setting);
  9885. }
  9886. return setting;
  9887. },
  9888. // 清理特定上传组件的setting
  9889. clearSetting: function (uploaderId) {
  9890. if (!window.top.SS?.cropper) return;
  9891. window.top.SS.cropper.settings.delete(uploaderId);
  9892. delete window.top.SS.cropper._backupSettings[uploaderId];
  9893. console.log(
  9894. "清理设置后的 Map size:",
  9895. window.top.SS.cropper.settings.size
  9896. );
  9897. },
  9898. };
  9899. /**
  9900. * 获取url中的参数
  9901. * @returns {Object}
  9902. */
  9903. window.SS.getQueryParams = function () {
  9904. const params = {};
  9905. const queryString = window.location.search.substring(1);
  9906. const pairs = queryString.split("&");
  9907. for (let i = 0; i < pairs.length; i++) {
  9908. const pair = pairs[i].split("=");
  9909. params[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
  9910. }
  9911. if (params.params) {
  9912. try {
  9913. params.params = JSON.parse(params.params);
  9914. } catch (e) {
  9915. console.error("Failed to parse params:", e);
  9916. }
  9917. }
  9918. return params;
  9919. };
  9920. /**
  9921. * 创建vue应用
  9922. * @param {Object} config 配置项
  9923. * @values {String} config.el 挂载的元素
  9924. * @values {Boolean} config.isDialogPage 是否是弹窗页面 决定了是否可以使用顶天立地
  9925. * @values {Object} config.vueOptions vue配置项
  9926. * @returns {Object} vue实例
  9927. */
  9928. window.SS.dom.initializeFormApp = function (config) {
  9929. const { el, isDialogPage = false, ...vueOptions } = config;
  9930. const app = createApp({
  9931. ...vueOptions,
  9932. });
  9933. // 如果是弹窗iframe里面的html的话 给当前的页面挂上事件 实现顶天立地的效果
  9934. if (isDialogPage) {
  9935. function checkScroll() {
  9936. // 选出所有fit-height-content的元素 如果有滚动条
  9937. const elements = document.querySelectorAll(".fit-height-content");
  9938. let hasScrollBar = false;
  9939. // 检查元素是否有滚动条 检查当前窗口的所有元素
  9940. // 如果有滚动条,则将结果设置为true
  9941. elements.forEach((el) => {
  9942. if (el.scrollHeight > el.clientHeight) {
  9943. hasScrollBar = true;
  9944. }
  9945. });
  9946. // 将结果发送给父窗口
  9947. window.parent.postMessage({ hasScrollBar }, "*");
  9948. }
  9949. function addScrollListeners() {
  9950. const elements = document.querySelectorAll("div");
  9951. elements.forEach((el) => {
  9952. el.addEventListener("scroll", checkScroll);
  9953. });
  9954. }
  9955. const observer = new MutationObserver((mutations) => {
  9956. addScrollListeners();
  9957. checkScroll();
  9958. });
  9959. observer.observe(document.body, {
  9960. childList: true,
  9961. subtree: true,
  9962. });
  9963. window.addEventListener("resize", checkScroll);
  9964. }
  9965. // 注册组件
  9966. app.component("SsLoginIcon", SsLoginIcon);
  9967. app.component("SsMark", SsMark);
  9968. app.component("SsFullStyleHeader", SsFullStyleHeader);
  9969. app.component("SsDialog", SsDialog);
  9970. app.component("SsInp", SsInput); //把SsInput改为SsInp Ben(20251225)
  9971. app.component("SsObjp", SsObjp);
  9972. app.component("SsHidden", SsHidden);
  9973. app.component("SsCcp", SsCcp);
  9974. app.component("SsDatePicker", SsDatePicker);
  9975. app.component("SsIcon", SsIcon);
  9976. app.component("SsCommonIcon", SsCommonIcon);
  9977. app.component("SsBreadcrumb", SsBreadcrumb);
  9978. app.component("SsEditor", SsEditor);
  9979. app.component("SsDialogIcon", SsDialogIcon);
  9980. app.component("SsBottomButton", SsBottomButton);
  9981. app.component("SsNavIcon", SsNavIcon);
  9982. app.component("SsHeaderIcon", SsHeaderIcon);
  9983. app.component("SsGolbalMenuIcon", SsGolbalMenuIcon);
  9984. app.component("SsCartListIcon", SsCartListIcon);
  9985. app.component("SsQuickIcon", SsQuickIcon);
  9986. app.component("SsFormIcon", SsFormIcon);
  9987. app.component("SsBottomDivIcon", SsBottomDivIcon);
  9988. app.component("SsEditorIcon", SsEditorIcon);
  9989. app.component("SsValidate", SsValidate);
  9990. app.component("SsOnoff", Ssonoff);
  9991. app.component("SsonoffArray", SsonoffArray);
  9992. app.component("SsTextarea", SsTextarea);
  9993. app.component("SsLoginInput", SsLoginInput);
  9994. app.component("SsLoginButton", SsLoginButton);
  9995. app.component("SsSearch", SsSearch);
  9996. app.component("SsCartItem", SsCartItem);
  9997. app.component("SsCartItem2", SsCartItem2);
  9998. app.component("SsListCard", SsListCard);
  9999. app.component("ss-cobj-card-list", SsCObjCardList); // 功能说明:二级对象列表使用旧卡片组件 ss-cobj-card-list by xu 20260115
  10000. app.component("SsFolderCard", SsFolderCard);
  10001. // 注册右侧边栏组件(ss-sidebar) by xu 20260106
  10002. app.component("ss-sidebar", SsSidebar);
  10003. app.component("ss-sidebar-buttons", SsSidebarButtons);
  10004. app.component("ss-sidebar-chart", SsSidebarChart);
  10005. app.component("ss-sidebar-chart-hover", SsSidebarChartHover);
  10006. app.component("ss-sidebar-list", SsSidebarList);
  10007. // 功能说明:注册右侧“统计表/报表”面板组件(ss-sidebar-report-table) by xu 20260115
  10008. app.component("ss-sidebar-report-table", SsSidebarReportTable);
  10009. app.component("SsFolderCartView", SsFolderCartView);
  10010. app.component("SsPage", SsPage);
  10011. app.component("SsRightInfo", SSRightInfo);
  10012. app.component("SsSuccessPopup", SsSuccessPopup);
  10013. app.component("SsErrorDialog", SsErrorDialog);
  10014. app.component("SsVerify", SsVerify);
  10015. app.component("SsVerifyNode", SsVerifyNode);
  10016. app.component("SsOrcImgBox", SsOrcImgBox);
  10017. app.component("ss-search-input", SsSearchInput);
  10018. app.component("ss-search-date-picker", SsSearchDatePicker);
  10019. app.component("ss-search-button", SsSearchButton);
  10020. app.component("ss-drop-button", SsDropButton);
  10021. app.component("ss-sub-tab", SsSubTab);
  10022. app.component("ss-img", SsImgUpload);
  10023. // 设置为中文
  10024. app.use(ElementPlus, {
  10025. locale: ElementPlusLocaleZhCn,
  10026. });
  10027. // console.log(ElementPlus);
  10028. // 确保 ElementPlusIconsVue
  10029. // if (window.ElementPlusIconsVue) {
  10030. // // 注册 Element Plus 图标组件
  10031. // for (const [key, component] of Object.entries(
  10032. // window.ElementPlusIconsVue
  10033. // )) {
  10034. // console.log(key, component);
  10035. // app.component(key, component);
  10036. // }
  10037. // }
  10038. // 挂载首页的组件
  10039. for (const componentName in IndexComponents) {
  10040. app.component(componentName, IndexComponents[componentName]);
  10041. }
  10042. // 挂载echarts的组件
  10043. for (const componentName in EchartComponents) {
  10044. app.component(componentName, EchartComponents[componentName]);
  10045. }
  10046. // 挂载 Vue 应用
  10047. let vm;
  10048. try {
  10049. vm = app.mount(el);
  10050. vm.data = vueOptions.data();
  10051. console.log("vm:", vm);
  10052. console.log("vueOptions:", vueOptions);
  10053. } catch (error) {
  10054. alert("Mount failed:" + error); //vue Mount失败报错 Ben(20251206)
  10055. }
  10056. return vm;
  10057. };
  10058. })();