living.vue 208 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335
  1. <template>
  2. <view class="swiper-wrapper" :class="liveItem.showType == 1 ? 'horizontal' : ''">
  3. <view class="container">
  4. <view :class="liveItem.previewUrl?'trailer-box':'trailer-box2'" v-if="liveItem.status == 1">
  5. <view class="video-container2"
  6. :style="{ paddingBottom:liveItem.previewUrl?videoRatio + '%':'' ,height:auto,width:'100%'}">
  7. <video v-if="liveItem.previewUrl" :id="`myVideo_${liveId}`" class="trailer-video"
  8. :src="liveItem.previewUrl" :autoplay="true" :loop="false"
  9. :object-fit="isHeight?'contain':'fill'" :custom-cache="false" :enable-progress-gesture="false"
  10. vslide-gesture-in-fullscreen="false" :show-center-play-btn="false" :http-cache="false"
  11. @error="videoError" @loadedmetadata="onVideoMetaLoaded" @pause="onVideoPause"
  12. @play="onVideoPlay" :disable-progress="true" :enable-play-gesture="true"
  13. @waiting="onVideoWaiting" preload="auto" type="application/x-mpegURL" :controls="false"></video>
  14. </view>
  15. <image v-if="liveItem.status == 1 && !liveItem.previewUrl" class="trailer-placeholder"
  16. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/readyStart.png">
  17. </image>
  18. <view class="countdown-container" v-if="liveItem.status == 1 && liveCountdown">
  19. <view class="live-name" v-if="liveItem.previewUrl">{{ liveItem.liveName }}</view>
  20. <view class="countdown-display">
  21. <text class="countdown-label">距离开播还有</text>
  22. <view class="countdown-unit">
  23. {{ liveCountdown.hours || '00' }}
  24. </view>
  25. <view class="countdown-separator">:</view>
  26. <view class="countdown-unit">
  27. {{ liveCountdown.minutes || '00' }}
  28. </view>
  29. <view class="countdown-separator">:</view>
  30. <view class="countdown-unit">
  31. {{ liveCountdown.seconds || '00' }}
  32. </view>
  33. </view>
  34. </view>
  35. <view class="trailer-actions">
  36. <button open-type="share" class="button-reset share-button">
  37. <view class="action-button mr24" @click="handleAgreement">
  38. <text>分享给好友</text>
  39. </view>
  40. </button>
  41. <view class="action-button reserve-button" @click="handleAgreement">
  42. <text>{{ hasSubscribed ? '已预约' : '预约直播'}}</text>
  43. </view>
  44. </view>
  45. </view>
  46. <!-- 无直播状态 -->
  47. <view class="trailer-box" v-if="!liveItem">
  48. <image class="trailer-placeholder"
  49. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/no_live.png">
  50. </image>
  51. <view class="no-live-title">暂无直播</view>
  52. </view>
  53. <!-- 主要内容区域 -->
  54. <view class="content"
  55. :class="{ 'horizontal-content': isFocus==1, 'trailer-content': liveItem.status==1, 'fullscreen-mode': isFullscreen }">
  56. <!-- 顶部信息栏 -->
  57. <view class="top-info-bar" v-if="!isFullscreen"
  58. :class="{'horizontal-top': liveItem.showType == 1,'hidden-on-fullscreen': isFullscreen}"
  59. :style="{marginTop: menuButtonInfo.top}">
  60. <view class="user-info-section">
  61. <image v-if="!scene&&liveItem.showType==2" @click="goBack" class="back-icon mr4"
  62. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/return3.png" />
  63. <image v-if="!scene&&liveItem.showType==1" @click="goBack" class="w42 h42 mr4"
  64. src="/static/images/return_black.png" />
  65. <view class="user-avatar-container">
  66. <u-avatar
  67. :src="liveItem.liveImgUrl || 'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/avatar.png'"
  68. :size="32"></u-avatar>
  69. <view class="user-name ml10 mr6">
  70. <view>{{ liveItem.liveName ? truncateString(liveItem.liveName, 8) : '未命名' }}</view>
  71. </view>
  72. </view>
  73. </view>
  74. <!-- 观众头像列表 -->
  75. <!-- @click="toggleViewerList" -->
  76. <view v-if="Array.isArray(filteredViewers)" class="viewers-section">
  77. <view style="display: flex;align-items: center;">
  78. <view v-for="(item, index) in (filteredViewers ||[])" :key="index">
  79. <image v-if="item" class="viewer-avatar mr4" :src="item" />
  80. </view>
  81. <view class="viewer-count ">{{formattedWatchCount || 0 }}</view>
  82. </view>
  83. </view>
  84. </view>
  85. <!-- 视频区域 -->
  86. <view class="videolist" v-if="liveItem.status == 2" :class="isFullscreen ? 'screen' : ''">
  87. <view class="video-container"
  88. :style="{ paddingBottom: liveItem.showType==2||isFullscreen?'':videoRatio + '%',height:liveItem.showType==2?'100vh':isFullscreen?'auto':'400rpx',width:isFullscreen?'0':'100%'}"
  89. :class="{'horizontal-layout': liveItem.showType == 1,'fullscreen-mode': isFullscreen}">
  90. <!-- <video v-if="liveItem.videoUrl && liveItem.liveType == 2 && !generating"
  91. :id="`myVideo_${liveId}`" :autoplay="true" class="video-player" :src="liveItem.videoUrl"
  92. object-fit="contain" :custom-cache="false" :enable-progress-gesture="false"
  93. vslide-gesture-in-fullscreen="true" :show-center-play-btn="false" :http-cache="false" loop
  94. @error="videoError" @timeupdate="onVideoTimeUpdate" @loadedmetadata="onVideoMetaLoaded"
  95. @pause="onVideoPause" @play="onVideoPlay" @waiting="onVideoWaiting"
  96. :enable-play-gesture="false" :play-strategy="1" @dblclick="preventDoubleClick"
  97. preload="auto" :enable-stash-buffer="false" :stash-initial-size="0" :stash-max-size="0"
  98. :stash-time="0" type="application/x-mpegURL" :controls="false" :show-fullscreen-btn="true"
  99. show-play-btn show-mute-btn enable-play-gesture="true"
  100. @fullscreenchange="onFullscreenChange" x5-video-player-fullscreen="true"
  101. x5-video-player-type="h5" x5-video-orientation="landscape" :webkit-playsinline="false"
  102. playsinline="false" :x5-playsinline="false">
  103. </video> -->
  104. <video v-if="liveItem.videoUrl && liveItem.liveType == 2 && !generating"
  105. :id="`myVideo_${liveId}`" :autoplay="true" class="video-player" :src="liveItem.videoUrl"
  106. :style="{transform:liveItem.showType!==2?`scale(${scale})`:'', transformOrigin: 'center center', bottom:isFullscreen?'0':''}"
  107. :object-fit="liveItem.showType==2||isFullscreen?'contain':'fill'" :custom-cache="false"
  108. :enable-progress-gesture="false" vslide-gesture-in-fullscreen="false"
  109. :show-center-play-btn="false" :http-cache="false" loop @error="videoError"
  110. @timeupdate="onVideoTimeUpdate" @loadedmetadata="onVideoMetaLoaded" @pause="onVideoPause"
  111. @play="onVideoPlay" @waiting="onVideoWaiting" :enable-play-gesture="false"
  112. :play-strategy="1" @dblclick="preventDoubleClick" preload="auto"
  113. :enable-stash-buffer="false" :stash-initial-size="0" :stash-max-size="0" :stash-time="0"
  114. type="application/x-mpegURL" :controls="false" :show-fullscreen-btn="false" show-play-btn
  115. show-mute-btn enable-play-gesture="true" @fullscreenchange="onFullscreenChange"
  116. x5-video-player-fullscreen="false" x5-video-player-type="h5"
  117. x5-video-orientation="landscape" :webkit-playsinline="true" playsinline="true"
  118. :x5-playsinline="true">
  119. </video>
  120. <view v-else class="txt">回放生成中...</view>
  121. <view v-if="showCustomControls && liveItem.showType==1 && !isFullscreen" class="custom-controls"
  122. @click.stop="toggleFullscreen">
  123. <image src="/static/images/full_screen.png" class="control-icon" />
  124. </view>
  125. <!-- 全屏返回按钮 - 只在全屏状态下显示 -->
  126. <view v-if="isFullscreen" class="fullscreen-exit-btn" @click="exitFullscreen">
  127. <image src="/static/images/half_screen.png" class="exit-fullscreen-icon" />
  128. <text class="exit-text">退出全屏</text>
  129. </view>
  130. <!-- 投诉按钮 -->
  131. <view v-if="liveItem.showType==2||isFullscreen" class="complaint-box"
  132. :class="isFullscreen ? 'complaint-full' : ''"
  133. :style="{top:isFullscreen?'':'15%',bottom:isFullscreen?'86vw':''}"
  134. @click="navgetTo('/pages_shopping/live/complaintList') ">
  135. <image class="image w32 h32 mr10" src="/static/images/complaint.png" mode="widthFix" />
  136. <view class="fs26">投诉</view>
  137. </view>
  138. <!-- 回放标签 -->
  139. <view v-if="isPlayback" class="replay-label"
  140. :style="{top:liveItem.showType === 2?'22%':isFullscreen?'':'10%',bottom:isFullscreen?'75vw':''}"
  141. :class="isFullscreen? 'replay-full' : ''">回放
  142. </view>
  143. </view>
  144. </view>
  145. <!-- 直播结束状态 -->
  146. <!-- v-if="liveItem.status == 3" -->
  147. <view class="videolist" v-if="liveItem.status == 3">
  148. <view class="video-container" style="height: 500rpx;"
  149. :class="{'horizontal-layout': liveItem.showType == 1, 'fullscreen-mode': isFullscreen}">
  150. <view class="live-end-message" :style="{marginTop:liveItem.showType === 2?'50%':''}">直播已结束
  151. </view>
  152. </view>
  153. </view>
  154. <!-- 直播回放 -->
  155. <view class="videolist" v-if="liveItem.status == 4">
  156. <view class="video-container" :class="liveItem.showType == 1 ? 'horizontal-layout' : ''">
  157. <video v-if="liveItem.videoUrl && liveItem.liveType == 3" :id="`myVideo_${liveId}`"
  158. class="video-player" :src="liveItem.videoUrl" :autoplay="true" :controls="true"
  159. :object-fit="isFullscreen?'contain':'contain'" :custom-cache="false"
  160. :enable-progress-gesture="liveItem.isSpeedAllowed" vslide-gesture-in-fullscreen="true"
  161. :show-center-play-btn="true" :http-cache="false" loop @error="videoError"
  162. @timeupdate="onVideoTimeUpdate" @loadedmetadata="onVideoMetaLoaded" @pause="onVideoPause"
  163. @play="onVideoPlay" :enable-play-gesture="true" preload="auto" @waiting="onVideoWaiting"
  164. type="application/x-mpegURL"></video>
  165. <view v-if="liveItem.videoUrl && liveItem.liveType == 3" class="replay-label">直播回放</view>
  166. </view>
  167. </view>
  168. <!-- v-if="!hasReachedTarget" -->
  169. <!-- &&liveItem.completionPointsEnabled -->
  170. <view class="progress-countdown end" :class="liveItem.showType==2?' progress-vertical':''"
  171. v-if="countdownPercentage!=100&&liveItem.completionPointsEnabled&&!receiveStatus&&liveItem.status!= 1">
  172. <image class="title" src="/static/images/points_title.png"></image>
  173. <view class="progress-bar-bg">
  174. <view class="progress-bar-fill" :style="{ width: countdownPercentage + '%' }"></view>
  175. </view>
  176. <!-- 剩余 -->
  177. <view class="progress-text">
  178. 倒计时{{ formattedCountdown.hours||'00' }}:{{ formattedCountdown.minutes||'00' }}:{{ formattedCountdown.seconds||"00" }}
  179. </view>
  180. </view>
  181. <view class="slide-group" v-if="!isFocus && !isFullscreen">
  182. <view class="action-button-group end">
  183. <view :class="[liveItem.showType === 1 ? 'horizontal' : 'vertical','icon-button','ml20']">
  184. <button open-type="share" class="action-icon button-reset">
  185. <image class="action-icon"
  186. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/weixin.png"
  187. mode="widthFix" />
  188. </button>
  189. </view>
  190. </view>
  191. <view class="action-button-group end">
  192. <view
  193. :class="[liveItem.showType === 1 ? 'horizontal' : 'vertical','icon-button','ml20','like-container']"
  194. @click="onLike">
  195. <LikeButton :initialCount="100" :heartsPerClick="5" @like="onLike" />
  196. <view class="like-count">{{formattedLikeCount||0 }}</view>
  197. </view>
  198. </view>
  199. </view>
  200. <!-- 底部聊天区域 -->
  201. <view class="chat-area-container" :class="{
  202. 'chat-area-container2': liveItem.showType == 1,
  203. 'chat-area-container3': liveItem.status == 1 && !liveItem.previewUrl && liveItem.showType == 1,
  204. 'chat-area-container4': liveItem.status == 1 && liveItem.previewUrl && liveItem.showType == 1,
  205. 'chat-area-focused': isFocus
  206. }" :style="{ '--keyboard-height': keyboardHeight + 'rpx' ,'display':isFullscreen?'none':''}">
  207. <view class="tabs_bg" v-if="liveItem.showType == 1">
  208. <u-tabs :list="listTabs" @click="clickTabs" lineColor="#02B176"
  209. :activeStyle="{color: '#02B176',fontWeight: 'bold',transform: 'scale(1.05)'}">
  210. </u-tabs>
  211. <!-- 投诉 -->
  212. <view v-if="liveItem.showType==1" class="complaint-box"
  213. @click="navgetTo('/pages_shopping/live/complaintList') ">
  214. <image class="image w32 h32 mr10" src="/static/images/complaint2.png" mode="widthFix" />
  215. <view class="fs26">投诉</view>
  216. </view>
  217. </view>
  218. <view class="chat-content-wrapper"
  219. :style="{ 'height':liveItem.status == 1 && !liveItem.previewUrl && liveItem.showType == 2?'48vh':''}"
  220. :class="{ 'chat-content-focused': isFocus }">
  221. <view class="notice-message" v-if="isShowNotice"
  222. :style="{ 'display':isFocus&&liveItem.showType == 2?'none':''}"
  223. :class="liveItem.showType == 1 ? 'horizontal-notice' : 'horizontal-notice2'">
  224. 公告消息: {{notice.msg}}
  225. </view>
  226. <scroll-view id="msgScroll" v-if="Array.isArray(talklist)" enable-flex scroll-y="true"
  227. :style="{ height: liveItem.showType === 1 || liveItem.showType == 2 && !isFocus?`calc(100% - ${commonViewHeight}px)`:'',
  228. marginTop:liveItem.showType === 1?commonViewHeight+'px':liveItem.showType == 2 && !isFocus?`calc(${commonViewHeight}px + 20rpx)`:'' }" :enhanced="true" :bounces="false"
  229. :show-scrollbar="false" :fast-deceleration="false" :enable-back-to-top="false"
  230. class="message-scroll-view" :scroll-top="scrollTop" :scroll-into-view="scrollIntoView"
  231. @scroll="onScroll" ref="scrollView">
  232. <view class="message-list" v-for="(item, talkIndex) in (talklist || [])"
  233. :key="item.uniqueId " :id="`list_${item.uniqueId }`" v-show="item.cmd != 'red' ">
  234. <view class="message-item">
  235. <view class="message-content">
  236. <text class="user-nickname">{{ item.nickName || '未命名' }}
  237. <text v-if="item.cmd != 'entry'">:</text>
  238. </text>
  239. <text class="message-text ml8">{{ item.msg }}</text>
  240. </view>
  241. </view>
  242. </view>
  243. </scroll-view>
  244. </view>
  245. <!-- 底部输入和操作区域 -->
  246. <view class="input-actions-container" :class="{ 'input-actions-focused': isFocus }">
  247. <view v-if="liveItem.status!=3&&!isPlayback" class="input-container" :class="{
  248. 'input-container-focused': isFocus,
  249. 'input-container-normal': !isFocus
  250. }">
  251. <input v-if="isIOS" type="text" :placeholder="placeholderText" v-model="value"
  252. :placeholder-style="liveItem.showType == 1 ? 'color:#999999;' : 'color:#e7e7e7;'"
  253. placeholder-class="placeholder-style" class="chat-input"
  254. :class="{ 'input-focused': isFocus }" @focus="inputFocus" @blur="inputBlur"
  255. cursor-spacing="100" :adjust-position="false" :disabled="isEnd" @confirm="sendMsg"
  256. :confirm-type="value?'send':'done'" />
  257. <input v-else :placeholder="placeholderText" v-model="value" type="text"
  258. :placeholder-style="liveItem.showType == 1 ? 'color:#999999;' : 'color:#e7e7e7;'"
  259. placeholder-class="placeholder-style" class="chat-input"
  260. :class="{ 'input-focused': isFocus }" @focus="inputFocus" @blur="inputBlur"
  261. cursor-spacing="100" :adjust-position="false" :disabled="isEnd" @confirm="sendMsg"
  262. :confirm-type="value?'send':'done'" />
  263. <!-- v-if="isFocus&&!isIOS" -->
  264. <view class="send-button" v-if="!isIOS" :class="liveItem.showType == 1 ? 'send2' : ''"
  265. @click="sendMsg()">
  266. 发送</view>
  267. </view>
  268. <!-- :class="{ 'action-buttons-hidden': isFocus }" -->
  269. <view class="action-buttons" v-if="!isFocus">
  270. <view class="action-button-group" v-if="!isFocus">
  271. <view
  272. :class="[liveItem.showType === 1 ? 'horizontal' : 'vertical','icon-button','mr40']"
  273. @click="isMore=true,shopping=false">
  274. <!-- <image
  275. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/more-icon.png"
  276. class="action-icon" /> -->
  277. <image :src="liveItem.showType == 1
  278. ? '/static/images/more2.png'
  279. :'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/more-icon.png'"
  280. class="action-icon" />
  281. </view>
  282. </view>
  283. <view class="icon-button mr20" @tap="openCart()">
  284. <image
  285. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/shopping.png"
  286. class="cart-icon" />
  287. </view>
  288. </view>
  289. </view>
  290. </view>
  291. </view>
  292. <!-- 商品卡片 -->
  293. <view class="goods-card" v-if="isShowGoods&&!isFullscreen"
  294. @click.stop="goShop(goodsCard.productId, goodsCard.goodsId)">
  295. <view class="goods-card-header">
  296. <view class="goods-status">
  297. <image class="status-icon mr8"
  298. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/signal.png" />
  299. 主播讲解中
  300. </view>
  301. <image @click.stop="isShowGoods = false" class="close-icon" src="/static/images/close_w.png" />
  302. </view>
  303. <image class="goods-image" :src="goodsCard.imgUrl" />
  304. <view class="goods-info">
  305. <view class="goods-title oneline-hide">{{ goodsCard.productName }}</view>
  306. <view class="goods-action">
  307. <view class="goods-price">
  308. <text class="price-symbol">¥<text class="price-integer">{{ goodsCard.price }}</text> <text
  309. class="price-decimal"></text></text>
  310. </view>
  311. <image class="shop-icon" src="/static/images/shop.png"></image>
  312. </view>
  313. </view>
  314. </view>
  315. <!-- :show="userlogo" -->
  316. <u-popup :show="userlogo" mode="bottom" round='12'>
  317. <view class="userlogo column">
  318. <view class="bold fs36 mt42">授权你的昵称信息</view>
  319. <view class=" justify-between align-center mt42">
  320. <view class="button-container">
  321. <input type="nickname" class="hidden-input"
  322. placeholder-style="color:#ffffff; font-size:32rpx;" @blur="onNickNameInput"
  323. placeholder="请点击授权微信昵称" @input="onNickNameInput" />
  324. </view>
  325. </view>
  326. <view class="submitname" @click="confimrname">确定</view>
  327. </view>
  328. </u-popup>
  329. <!-- 抽奖弹窗 -->
  330. <u-popup :show="isShowLotteryPop && countdown" round="40rpx">
  331. <view class="lottery-popup">
  332. <image class="lottery-header-image"
  333. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/red_head.png"
  334. mode="widthFix" />
  335. <image class="lottery-background-image"
  336. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/red_bg.png" />
  337. <view class="lottery-content">
  338. <view class="lottery-close-section">
  339. <view class="close-button-wrapper">
  340. <view class="close-button" @click="isShowLotteryPop = false">
  341. <u-icon class="close-icon" name="close" color="#fff" size="20"></u-icon>
  342. </view>
  343. </view>
  344. </view>
  345. <view class="lottery-main-content">
  346. <image class="lottery-title-image"
  347. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/red_title.png" />
  348. <view class="lottery-countdown-section">
  349. <view class="countdown-label">开奖倒计时</view>
  350. <view class="countdown-timer">
  351. <view class="countdown-number">
  352. {{ countdown.hours || '00' }}
  353. </view>
  354. <view class="countdown-number">
  355. {{ countdown.minutes || '00' }}
  356. </view>
  357. <view class="countdown-number">
  358. {{ countdown.seconds || '00' }}
  359. </view>
  360. </view>
  361. </view>
  362. <view class="lottery-products-section">
  363. <ThreeItemSwiper :products="lotteryProducts"></ThreeItemSwiper>
  364. </view>
  365. <view class="lottery-indicators">
  366. <view class="indicator-point" v-for="(item, index) in (lotteryProducts || [])"
  367. :key="index"></view>
  368. </view>
  369. <view class="lottery-tip">观看直播参与抽奖</view>
  370. <view class="lottery-action-button" @click="onClaim">参与抽奖</view>
  371. </view>
  372. </view>
  373. </view>
  374. </u-popup>
  375. <!-- 积分弹窗(完课) -->
  376. <!-- showPoints -->
  377. <!-- :show="!!integral.status" -->
  378. <!-- :show="showPoints" -->
  379. <u-popup :show="shouldShowIntegralPopup" round="20rpx" mode="center" bgColor="#ffffff" zIndex="10076">
  380. <view class="integral-popup color9">
  381. <view class="integral-header">
  382. <view class="integral-title">观看视频领积分</view>
  383. <image class="integral-background-image"
  384. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/integral_bg.png"
  385. mode="widthFix" />
  386. </view>
  387. <view class="integral-content">
  388. <view class="integral-item" v-for="(item,index ) in receiveList" :key="index">
  389. <view class="integral-message">连续签到{{item.continuousDays}}天</view>
  390. <view class="integral-message">获取{{item.pointsAwarded}}积分</view>
  391. <view class="integral-confirm-button" @click="completionReceive(item)">领取</view>
  392. </view>
  393. </view>
  394. </view>
  395. </u-popup>
  396. <!-- 积分弹窗(时长) -->
  397. <u-popup :show="!!integral.status" round="20rpx" mode="center" bgColor="#ffffff" zIndex="10076">
  398. <view class="integral-popup color9">
  399. <view class="integral-header">
  400. <view class="integral-title">观看视频领积分</view>
  401. <image class="integral-background-image"
  402. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/integral_bg.png"
  403. mode="widthFix" />
  404. </view>
  405. <view class="integral-content">
  406. <view class="integral-message">{{ integral.msg }}</view>
  407. <view class="integral-confirm-button" @click="integral.status = false">确认</view>
  408. </view>
  409. </view>
  410. </u-popup>
  411. <!-- 红包弹窗 -->
  412. <u-popup :show="isShowRed" round="20rpx" mode="center" zIndex="10076" bgColor="transparent">
  413. <view class="red-envelope-popup" @click="onRed()">
  414. <image class="red-envelope-close-button"
  415. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/close_white.png"
  416. @click.stop="isShowRed=false"></image>
  417. <image class="red-envelope-background"
  418. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/pop1.png">
  419. </image>
  420. <view class="red-envelope-content">
  421. <view class="red-envelope-header">
  422. <image class="header-decoration-left"
  423. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/top_left.png"
  424. mode="widthFix" />
  425. <view class="header-title">恭喜您可以</view>
  426. <image class="header-decoration-right"
  427. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/top_right.png"
  428. mode="widthFix" />
  429. </view>
  430. <view class="red-envelope-text">领红包</view>
  431. <view class="red-envelope-open-text">开</view>
  432. </view>
  433. </view>
  434. </u-popup>
  435. <!-- 抽奖弹窗2 -->
  436. <u-popup :show="isShowLottery&&countdown" round="20rpx" mode="center" zIndex="10076" bgColor="transparent">
  437. <view class="lottery-popup-v2">
  438. <image class="lottery-close-button-v2"
  439. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/close_white.png"
  440. @click.stop="isShowLottery=false">
  441. </image>
  442. <image class="lottery-top-image"
  443. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/pop_top.png"
  444. mode="widthFix"></image>
  445. <image class="lottery-background-v2"
  446. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/pop2.png">
  447. </image>
  448. <view class="lottery-content-v2">
  449. <view class="lottery-header-v2">
  450. <image class="lottery-header-decoration-left"
  451. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/top_left.png"
  452. mode="widthFix" />
  453. <view class="lottery-header-title">恭喜您可以</view>
  454. <image class="lottery-header-decoration-right"
  455. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/top_right.png"
  456. mode="widthFix" />
  457. </view>
  458. <view class="lottery-text-v2">参与抽奖</view>
  459. <view class="lottery-text-v2">赢好礼</view>
  460. <view class="lottery-action-button-v2" @click="onLottery()">确认</view>
  461. </view>
  462. </view>
  463. </u-popup>
  464. <!-- 红包卡片弹窗 -->
  465. <u-popup :show="isShowRedCard" round="20rpx" mode="center" bgColor="transparent" zIndex="10076">
  466. <view class="red-card-popup">
  467. <image class="red-card-image"
  468. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/red_card.png" />
  469. <view class="red-card-content">
  470. <view class="red-card-message">{{ redCard.msg }}</view>
  471. <view class="red-card-tip">直播惊喜积分</view>
  472. <view class="red-card-confirm-button" @click="isShowRedCard = false">确认</view>
  473. </view>
  474. </view>
  475. </u-popup>
  476. <!-- 消息弹窗 -->
  477. <u-popup :show="isShowPopMsg" round="20rpx" mode="center" zIndex="10076">
  478. <view class="message-popup">
  479. <image class="message-close-icon" src="/static/images/message_icon.png" />
  480. <view class="message-title">消息通知</view>
  481. <view class="message-content">{{popMsg}}</view>
  482. <view class="message-confirm-button" @click="isShowPopMsg = false">确认</view>
  483. <image src="/static/images/close_white_icon.png" class="close" @click="isShowPopMsg = false">
  484. </image>
  485. </view>
  486. </u-popup>
  487. <!-- 中奖和未中奖 -->
  488. <u-popup :show="isShowPrize && havePrize" round="20rpx" mode="center" bgColor="#fff" zIndex="10076">
  489. <view class="prize-popup" v-if="isCurrentUserWon">
  490. <image class="prize-header-image"
  491. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/red_head.png"
  492. mode="widthFix" />
  493. <view class="prize-title">恭喜您 中奖啦!</view>
  494. <view class="prize-info-section" v-for="(item, index) in (prizeInfo || [])" :key="index">
  495. <view class="prize-info-item">{{ item.userName }}</view>
  496. <view class="prize-info-item">{{ item.userId }}</view>
  497. <view class="prize-level item">{{ item.prizeLevel }}等奖</view>
  498. </view>
  499. <view class="prize-tip">请填写收货地址,主播将会将奖品发给您</view>
  500. <view class="prize-action-button"
  501. @click="navgetTo('/pages_shopping/live/confirmCreateOrder?type=win&productId='+getCurrentUserPrizeProductId+'&liveId='+liveId+'&recordId='+getCurrentUserPrizeRecordId),confirm()">
  502. 填写地址</view>
  503. </view>
  504. <view class="no-prize-popup" v-else>
  505. <image class="no-prize-image"
  506. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/no-prize.png"
  507. mode="widthFix" />
  508. <view class="no-prize-tip">很遗憾 您未中奖</view>
  509. <view class="no-prize-confirm-button" @click="confirm">确认</view>
  510. </view>
  511. </u-popup>
  512. <!-- 中奖记录弹窗 -->
  513. <u-popup :show="winning" @close="closeWin" round="20rpx" bgColor="#f3f5f9" zIndex="10076">
  514. <view class="winning-record-popup">
  515. <image class="winning-record-header-bg"
  516. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/red_head.png"
  517. mode="widthFix"></image>
  518. <image class="winning-record-background"
  519. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/red_bg.png">
  520. </image>
  521. <view class="winning-record-content">
  522. <view class="winning-record-title">我的中奖记录</view>
  523. <view class="winning-record-header">
  524. <view class="header-column time-column">时间</view>
  525. <view class="header-column status-column">状态</view>
  526. <view class="header-column prize-column">奖品</view>
  527. </view>
  528. <scroll-view v-if="prizeAll" enable-flex scroll-y class="winning-record-list">
  529. <view class="winning-record-item" v-for="(item, index) in (prizeAll||[])" :key="index">
  530. <view class="record-time">{{ item.createTime }}</view>
  531. <view class="record-status-button" v-if="item.orderStatus=='-9'||!item.orderStatus"
  532. @click="navgetTo('/pages_shopping/live/confirmCreateOrder?type=win&productId='+item.productId+'&liveId='+liveId+'&recordId='+item.id),winning=false">
  533. <text>填地址</text>
  534. </view>
  535. <view class="record-status" v-else>
  536. <text v-if="item.orderStatus=='1'">待支付</text>
  537. <text v-if="item.orderStatus=='2'">待发货</text>
  538. <text v-if="item.orderStatus=='3'">待收货</text>
  539. <text v-if="item.orderStatus=='4'">已完成</text>
  540. <text v-if="item.orderStatus=='-3'">已取消</text>
  541. </view>
  542. <view class="record-prize">
  543. {{ item.productName ? truncateString(item.productName, 6) : '' }}
  544. </view>
  545. </view>
  546. </scroll-view>
  547. <view v-else class="no-winning-records">暂无中奖信息</view>
  548. </view>
  549. </view>
  550. </u-popup>
  551. <!-- 更多操作弹窗 -->
  552. <u-popup :show="isMore" @close="closeMore" round="20rpx" bgColor="#f3f5f9" zIndex="10076">
  553. <view class="more-actions-popup">
  554. <view class="more-action-item"
  555. @click="navgetTo('/pages_shopping/live/integral?liveId'+liveId), (isMore = false)">
  556. <image class="action-icon"
  557. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/order.png" />
  558. <view class="action-label">积分</view>
  559. </view>
  560. <!-- <view class="more-action-item"
  561. @click="navgetTo('/pages_shopping/live/storeOrderRefundList?liveId=' + liveId), (isMore = false)">
  562. <image class="action-icon"
  563. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/after_sales.png" />
  564. <view class="action-label">售后订单</view>
  565. </view> -->
  566. <view class="more-action-item" @click="goMiniProgram(), (isMore = false)">
  567. <image class="action-icon"
  568. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/points.png" />
  569. <view class="action-label">兑换好礼</view>
  570. </view>
  571. <view class="more-action-item" @click="getMyLottery(), (isMore = false), (winning = true)">
  572. <image class="action-icon"
  573. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/health_sel.png" />
  574. <view class="action-label">中奖记录</view>
  575. </view>
  576. </view>
  577. </u-popup>
  578. <!-- 商品弹窗 -->
  579. <u-popup :show="shopping" @close="closeShop" round="20rpx" bgColor="#f3f5f9" zIndex="10075">
  580. <view class="shopping-popup">
  581. <view class="shopping-header">
  582. <view class="search-input-container">
  583. <image class="search-icon"
  584. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/search.png"
  585. mode="widthFix" />
  586. <input placeholder="请搜索商品" v-model="inputInfo" @input="handleSearchInput" />
  587. </view>
  588. <view class="x-f">
  589. <view class="shopping-action-button mr30" @click="navgetTo('/pages_shopping/live/order')">
  590. <image class="action-button-icon"
  591. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/carts.png" />
  592. <view class="action-button-label">订单</view>
  593. </view>
  594. <view class="shopping-action-button" @click="(isMore = true), (shopping = false)">
  595. <image class="action-button-icon"
  596. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/search2.png" />
  597. <view class="action-button-label">更多</view>
  598. </view>
  599. </view>
  600. </view>
  601. <scroll-view enable-flex scroll-y class="products-list">
  602. <view v-for="(item, index) in products" :key="index" class="product-item">
  603. <view class="product-image-container">
  604. <image class="product-image" :src="item.imgUrl" mode="widthFix" />
  605. <view class="product-index-label">{{ index + 1 }}</view>
  606. </view>
  607. <view class="product-info">
  608. <view class="product-name">{{ item.productName }}</view>
  609. <view class="product-sales">{{ item.sales }} 人已购</view>
  610. <view class="product-action-section">
  611. <text class="product-price">
  612. <text class="price-symbol">¥</text>
  613. <text class="price-integer">{{ Math.trunc(item.price) }}</text>
  614. .{{ getPureDecimal(item.price) ? getPureDecimal(item.price) : '00' }}
  615. </text>
  616. <view class="product-action-buttons">
  617. <view class="collect-button">
  618. <image v-if="item.isFavorite" @click="onGoodsCollect(item)"
  619. class="collect-icon"
  620. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/collect_select.png" />
  621. <image v-else @click="onGoodsCollect(item)" class="collect-icon"
  622. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/collect.png" />
  623. </view>
  624. <view v-if="item.status == 1" class="buy-button"
  625. @click="goShop(item.productId, item.goodsId)">去购买</view>
  626. <view v-else-if="item.status == 0" class="buy-button disabled">已下架</view>
  627. </view>
  628. </view>
  629. </view>
  630. </view>
  631. </scroll-view>
  632. </view>
  633. </u-popup>
  634. <!-- 优惠券弹窗 -->
  635. <view class="coupon-popup" v-if="isShowCoupon">
  636. <view class="coupon-container">
  637. <image class="coupon-background"
  638. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/coupon_bg.png" />
  639. <image class="coupon-header"
  640. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/coupon_top.png" />
  641. <image @click="isShowCoupon = false" class="coupon-close-button"
  642. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/close_white.png" />
  643. <view class="coupon-content">
  644. <view class="coupon-name">{{ couponInfo.couponName }}</view>
  645. <view class="coupon-price">
  646. <text class="coupon-price-integer">{{ couponInfo.couponPrice }}</text>
  647. </view>
  648. <view class="coupon-condition">满{{ couponInfo.useMinPrice }}元可用</view>
  649. <view class="coupon-description">指定商品可用</view>
  650. <view class="coupon-validity">自领取起{{ couponInfo.couponTime }}天内有效</view>
  651. <view class="coupon-get-button" @click="onCoupon()">立即领券</view>
  652. </view>
  653. </view>
  654. </view>
  655. <view v-if="liveItem.status == 1" class="ash_bg"></view>
  656. </view>
  657. </view>
  658. </template>
  659. <script>
  660. import LikeButton from '@/pages_course/components/like.vue';
  661. import ThreeItemSwiper from '@/pages_course/components/ThreeItemSwiper.vue';
  662. import CryptoJS from 'crypto-js';
  663. import {
  664. myLiveMsg,
  665. myLottery, // 我的中奖明细
  666. coupon, //领取优惠券
  667. liveLottery, // 抽奖查询
  668. claim, //抽奖
  669. liveRed, // 点击领红包
  670. liveDataLike, // 点赞
  671. collectStore, // 店铺收藏/取消收藏
  672. collectGoods, // 商品收藏/取消收藏
  673. watchUserList, //获取直播间用户(展示在线用户)
  674. liveMsg, //获取最近聊天记录
  675. // 小黄车
  676. liveStore, //店铺展示,
  677. liveOrderUser, //正在购买
  678. getLiveInfo, //获取直播间信息接口
  679. getLiveViewData, //直播间点赞、关注、在线人数数据
  680. currentActivities, //红包 卡片 抽奖
  681. getlive,
  682. subNotifyLive,
  683. internetTraffic, // 流量(缓冲百分比),
  684. liveInternetTraffic, // 直播流量(缓冲百分比),
  685. loginByMp,
  686. getUserInfo
  687. } from '@/api/living.js';
  688. import {
  689. editUser
  690. } from '@/api/user'
  691. import {
  692. nextTick
  693. } from 'vue';
  694. import {
  695. mapGetters
  696. } from 'vuex';
  697. import {
  698. TOKEN_KEYAuto,
  699. generateRandomString
  700. } from '@/utils/courseTool.js'
  701. import {
  702. completionRecords,
  703. completionReceive,
  704. completionUnreceived,
  705. } from '@/api/integral.js'
  706. import dayjs from 'dayjs';
  707. var isSocketOpen = false;
  708. var socket = null;
  709. export default {
  710. components: {
  711. ThreeItemSwiper,
  712. LikeButton
  713. },
  714. data() {
  715. return {
  716. receiveStatus: false,
  717. completionRate: 0,
  718. playbackTime: null, //用户观看回放时间
  719. // 观看时长统计变量
  720. watchStartTime: 0, // 开始观看时间戳
  721. accumulatedWatchDuration: 0, // 累计观看时长(秒)
  722. isPageVisible: true, // 页面是否可见
  723. lastPauseTime: 0,
  724. hasReachedTarget: false,
  725. receiveList: [],
  726. recordId: null, //完课积分记录ID
  727. showPoints: false,
  728. remainingTime: 0, // 剩余时间(秒)
  729. watchProgressKey: '', // 存储键
  730. isCountdownActive: false, // 倒计时是否激活
  731. countdownTimer: null, // 倒计时定时器
  732. lastUpdateTime: 0, // 上次更新时间
  733. totalWatchTime: 0, // 累计观看时间(秒)
  734. watchTimeStorageKey: '', // 存储键
  735. lastSaveTime: 0, // 上次保存时间
  736. watchTimeTimer: null, // 观看时间定时器
  737. menuButtonInfo: {}, // 胶囊按钮布局信息
  738. isFullscreen: false,
  739. isVideoRotated: false,
  740. showNonVideoElementsFlag: true,
  741. videoRatio: 56.25, // 默认16:9比例
  742. scale: 1,
  743. isMuted: false,
  744. showCustomControls: true,
  745. videoContext: null,
  746. isNow: false,
  747. fakeAvatar: Array.from({
  748. length: 11
  749. }, (_, i) =>
  750. `https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/avatar${i + 1}.jpg`),
  751. virtualHuman: null,
  752. userlogo: false,
  753. listTabs: [{
  754. name: '讨论',
  755. }],
  756. generating: false,
  757. hasPlayback: false,
  758. isPlayback: false, //是否是回放
  759. isIOS: false,
  760. myselfFlag: false, // yhq
  761. isEnd: false,
  762. popMsg: null,
  763. // barrageList: ['支持你!', '谢谢你!', '平台真好!', '666', '欢迎回家!'], //一键弹幕列表
  764. uuId: '',
  765. totalTraffic: 0, // 总流量(字节)
  766. bitrate: 800, // 录播默认码率 0.16Mbps
  767. bitrateLive: 1600, // 直播默认码率 0.16Mbps
  768. //定时器
  769. trafficTimer: null,
  770. pingTimeoutTimer: null, // 心跳超时定时器
  771. heartBeatTimer: null, // 心跳定时器
  772. liveViewDataTimer: null,
  773. reconnectTimer: null, // 重连定时器
  774. scrollTimer: null, // 滚动防抖定时器
  775. lastScrollTime: 0, // 上次滚动时间
  776. scrollDebounceDelay: 200, // 滚动防抖延迟(毫秒)
  777. searchTimer: null, //搜索
  778. purchasePromptTimer: null, //购买提示
  779. welcomeTimer: null, //进入离开直播间定时器
  780. redTimer: null,
  781. liveStartTimer: null,
  782. lotteryTimer: null,
  783. memoryMonitorTimer: null, // 内存监控定时器
  784. networkStatusTimer: null, // 网络状态检测定时器
  785. networkRetryTimer: null, // 网络重试定时器
  786. lastHeartBeatTime: 0, // 上次心跳时间
  787. // 性能监控
  788. connectionStartTime: 0, // 连接开始时间
  789. connectionLatency: 0, // 连接延迟
  790. messageCount: 0, // 消息计数
  791. errorCount: 0, // 错误计数
  792. lastPerformanceCheck: 0, // 上次性能检查时间
  793. stayTime: 0,
  794. startTime: 0,
  795. scrollTop: 0,
  796. currentScrollTop: 0, // 当前实际滚动位置
  797. scrollIntoView: '', // scroll-into-view属性
  798. messageIdCounter: 0, // 消息ID计数器,确保每个消息有唯一ID
  799. scrollPending: false, // 滚动操作是否正在等待执行
  800. isOnload: false,
  801. isConnecting: false, // 是否正在连接中
  802. hasInitialized: false,
  803. liveViewersData: [],
  804. liveUserCalled: false, //调用过watchUserList没
  805. userRandomColors: Object.create(null), // 缓存用户ID -> 随机色的映射
  806. heartBeatRetryCount: 0, // 心跳发送重试次数(避免一次失败就重连)
  807. maxHeartBeatRetries: 3, // 心跳最大重试次数(增加到3次)
  808. pingTimeout: 25000, // 心跳超时时间(25秒,适应移动网络)
  809. shownEntryUsers: new Set(), // 存储已显示过「进入提示」的用户ID
  810. socket: null, // WebSocket 实例
  811. isSocketOpen: false, // 连接是否已打开
  812. heartBeatInterval: 15000, // 心跳发送间隔(15秒)
  813. adaptiveHeartBeatInterval: 15000, // 自适应心跳间隔
  814. reconnectCount: 0, // 当前重连次数f
  815. maxReconnectAttempts: 5, // 最大重连次数(增加到5次)
  816. isManualClose: false, // 是否手动关闭(用于区分主动关闭和异常断开)
  817. networkType: 'unknown', // 当前网络类型
  818. isNetworkAvailable: true, // 网络是否可用
  819. templateId: 'IKffTm6HnbLS91hDCUnKSQpC2r4EABo3fOtThnK1dI8', //百域承品
  820. isAgreement: false,
  821. wsNewUrl: 'wss://api.fhhx.runtzh.com/ws/app/webSocket',
  822. // wsNewUrl: 'ws://192.168.10.166:7114/ws/app/webSocket',
  823. qrFrom: null,
  824. scene: '',
  825. liveCountdown: {}, //直播倒计时
  826. countdown: {}, //抽奖倒计时
  827. liveViewData: {},
  828. keyboardHeight: 0,
  829. videoCurrentTime: 0, // 当前视频播放时间
  830. videoProgressKey: '', // 存储进度的key
  831. inAndOut: {},
  832. winning: false,
  833. isShowPrize: false,
  834. isShowCoupon: false,
  835. prizeInfo: [],
  836. havePrize: uni.getStorageSync('havePrize') || false, //是否参与抽奖
  837. countDownKey: 0,
  838. lotteryProducts: [],
  839. lotteryList: [],
  840. talklist: [],
  841. isShowLotteryPop: false,
  842. liveItem: {},
  843. isSending: false,
  844. isMore: false,
  845. value: '',
  846. placeholderText: '说点什么...',
  847. prizeAll: [],
  848. isShowLottery: false,
  849. isShowRedCard: false,
  850. isShowPopMsg: false,
  851. isShowNotice: false,
  852. notice: [],
  853. redCard: null, //点击红包出现弹窗
  854. integral: {},
  855. lotteryInfo: {},
  856. goodsCard: {},
  857. couponInfo: {},
  858. redInfo: {},
  859. storeId: null,
  860. isFocus: false,
  861. shopping: false,
  862. systemInfo: null, // 缓存系统信息,避免重复调用同步API
  863. generatingTimer: null, //回访生成中
  864. inputInfo: '',
  865. showWelcomeMessage: false,
  866. isShowGoods: false,
  867. isShowRed: false,
  868. lastClickTime: 0,
  869. videoRetryCounts: Object.create(null), // 记录每个直播间的视频重试次数,格式: { liveId: 次数 }
  870. clickDelay: 300, // 300ms内只响应一次点击
  871. liveUserTotal: 0,
  872. viewPageSize: 10, // 每页数量
  873. viewPageNum: 1, // 当前页码
  874. viewLoading: false, // 是否正在加载
  875. scrollHeight: 0,
  876. showPurchasePrompt: false,
  877. prevOrderCount: 0, // 用于记录上一次的购买人数
  878. videoUrl: null,
  879. showType: 1, //横屏1 竖屏2
  880. boxHeight: 300, //小黄车高度
  881. liveViewers: [], //观众
  882. livingUrl: '',
  883. products: [],
  884. loadingProducts: false, // 商品加载状态
  885. orderUser: {}, //正在购买
  886. userType: 0,
  887. timestamp: '',
  888. showadd: false,
  889. liveId: null,
  890. userInfo: {}, //用户信息
  891. userData: {},
  892. diffTotalTime: '',
  893. address: '',
  894. commonViewHeight: '',
  895. user: {}
  896. };
  897. },
  898. async onLoad(options) {
  899. this.getMenuButtonInfo(); // 初始化获取胶囊信息
  900. this.initTime()
  901. this.getLocationByIP();
  902. if (options.liveId) {
  903. this.liveId = options.liveId;
  904. }
  905. // 扫码传来的参数
  906. if (options.scene) {
  907. this.scene = options.scene;
  908. const decodedScene = decodeURIComponent(this.scene);
  909. const params = {};
  910. decodedScene.split('&').forEach((item) => {
  911. const [key, value] = item.split('=');
  912. params[key] = value;
  913. this.liveId = params.a;
  914. });
  915. if (params.b && params.c) {
  916. console.log("扫码参数", params)
  917. this.qrFrom = `&companyId=${params.b}&companyUserId=${params.c}`;
  918. }
  919. }
  920. if (options.companyId && options.companyUserId) {
  921. this.qrFrom = `&companyId=${options.companyId}&companyUserId=${options.companyUserId}`;
  922. }
  923. this.userinfo = uni.getStorageSync('userinfo');
  924. // this.userData = uni.getStorageSync('userData');
  925. console.log('全部参数', options);
  926. try {
  927. const isLogin = await this.utils.checkLiveToken();
  928. if (isLogin) {
  929. this.haveLogin()
  930. }
  931. } catch (error) {
  932. console.error('初始化失败:', error);
  933. }
  934. //获取键盘高度 - 针对iPhone优化(使用缓存的系统信息)
  935. uni.onKeyboardHeightChange((res) => {
  936. console.log('键盘高度变化:', res.height, '平台:', this.systemInfo.platform);
  937. if (this.systemInfo.platform === 'ios') {
  938. // iPhone特殊处理
  939. if (res.height > 0) {
  940. // 键盘弹出
  941. this.isKeyboardShow = true;
  942. // iPhone键盘高度计算优化
  943. let calculatedHeight = res.height * 2;
  944. // 根据iPhone型号进行微调
  945. if (this.systemInfo.model) {
  946. if (this.systemInfo.model.includes('iPhone X') ||
  947. this.systemInfo.model.includes('iPhone 11') ||
  948. this.systemInfo.model.includes('iPhone 12') ||
  949. this.systemInfo.model.includes('iPhone 13') ||
  950. this.systemInfo.model.includes('iPhone 14') ||
  951. this.systemInfo.model.includes('iPhone 15') ||
  952. this.systemInfo.model.includes('iPhone 16') ||
  953. this.systemInfo.model.includes('iPhone 17')) {
  954. // 刘海屏设备需要额外调整
  955. calculatedHeight = calculatedHeight + 20; // 增加20rpx补偿
  956. }
  957. }
  958. // 减去底部安全区域,避免过高
  959. const safeAreaBottom = this.systemInfo.safeAreaInsets ? this.systemInfo.safeAreaInsets
  960. .bottom : 0;
  961. if (safeAreaBottom > 0) {
  962. calculatedHeight = calculatedHeight - (safeAreaBottom * 2.0); // 微调10rpx
  963. }
  964. this.keyboardHeight = Math.max(400, calculatedHeight);
  965. } else {
  966. // 键盘隐藏
  967. this.isKeyboardShow = false;
  968. this.keyboardHeight = 0;
  969. }
  970. } else {
  971. console.log("手机型号是>>>>", this.systemInfo.model)
  972. // Android处理保持原有逻辑
  973. if (res.height > 0) {
  974. this.isKeyboardShow = true;
  975. const safeAreaBottom = this.systemInfo.safeAreaInsets ? this.systemInfo.safeAreaInsets
  976. .bottom : 0;
  977. // this.keyboardHeight = (res.height * 2)-50;
  978. // this.keyboardHeight = res.height * 2;
  979. if (this.systemInfo.brand == 'vivo') {
  980. this.keyboardHeight = res.height * 2;
  981. } else {
  982. // this.keyboardHeight = (res.height * 1.78) + 20;''
  983. this.keyboardHeight = (res.height * 1.88) + 20;
  984. }
  985. console.log("高度是", this.keyboardHeight)
  986. } else {
  987. this.isKeyboardShow = false;
  988. this.keyboardHeight = 0;
  989. }
  990. }
  991. });
  992. // 初始化网络状态监听
  993. this.initNetworkStatusListener();
  994. },
  995. onPullDownRefresh() {
  996. this.getLiveMsg(this.liveItem);
  997. // this.getliveUser();
  998. setTimeout(() => {
  999. uni.stopPullDownRefresh();
  1000. }, 1000);
  1001. this.getliving(this.liveId);
  1002. },
  1003. mounted() {
  1004. // 获取 video 上下文
  1005. // 监听视频全屏事件
  1006. setTimeout(() => {
  1007. this.videoContext = uni.createVideoContext(`myVideo_${this.liveId}`, this);
  1008. // 添加全屏状态变化监听(更可靠的方式)
  1009. if (this.videoContext) {
  1010. // 使用 uni.onUserCaptureScreen 作为备选方案
  1011. uni.onUserCaptureScreen(() => {
  1012. console.log('用户截屏,可能是全屏状态');
  1013. });
  1014. }
  1015. }, 1000);
  1016. const systemInfo = uni.getSystemInfoSync();
  1017. this.isIOS = systemInfo.platform === 'ios';
  1018. },
  1019. async onShow() {
  1020. if (this.isVideoPlaying && this.remainingTime > 0) {
  1021. this.startCountdown();
  1022. }
  1023. // 检查视频是否在播放状态
  1024. if (this.videoContext) {
  1025. // 可以通过视频上下文获取播放状态,但小程序可能有限制
  1026. // 或者通过自定义状态管理
  1027. if (this.isVideoPlaying) { // 需要你维护一个播放状态变量
  1028. this.startWatchTimeCounter();
  1029. }
  1030. }
  1031. try {
  1032. const isLogin = await this.utils.checkLiveToken();
  1033. if (isLogin) {
  1034. this.haveLogin()
  1035. } else {
  1036. this.goLogin();
  1037. }
  1038. } catch (error) {
  1039. console.error('初始化失败:', error);
  1040. }
  1041. this.getLocationByIP();
  1042. this.uuId = generateRandomString(16);
  1043. const isLiveLogin = uni.getStorageSync('isLiveLogin');
  1044. this.share = uni.getStorageSync('share');
  1045. this.scene = uni.getStorageSync('scene');
  1046. if (this.share && this.share.length > 0) {
  1047. this.liveId = this.share.liveId;
  1048. this.qrFrom = `&companyId=${this.share.companyId}&companyUserId=${this.share.companyUserId}`;
  1049. uni.removeStorageSync('share');
  1050. }
  1051. if (this.scene) {
  1052. const decodedScene = decodeURIComponent(this.scene);
  1053. const params = {};
  1054. decodedScene.split('&').forEach((item) => {
  1055. const [key, value] = item.split('=');
  1056. params[key] = value;
  1057. this.liveId = params.a;
  1058. });
  1059. if (params.b && params.c) {
  1060. this.qrFrom = `&companyId=${params.b}&companyUserId=${params.c}`;
  1061. }
  1062. uni.removeStorageSync('scene');
  1063. }
  1064. // if (isLiveLogin) {
  1065. // if (this.liveId) {
  1066. // await this.getliving(this.liveId);
  1067. // this.getCurrentActivities();
  1068. // this.getliveOrder();
  1069. // this.initSocket();
  1070. // }
  1071. // this.hasInitialized = true;
  1072. // uni.removeStorageSync('isLiveLogin');
  1073. // }
  1074. // 恢复播放和连接
  1075. await this.resumePageActivity();
  1076. // 恢复观看时长统计(如果之前已经开始统计)
  1077. if (this.watchStartTime > 0 || this.accumulatedWatchDuration > 0) {
  1078. this.resumeWatchDurationTracking();
  1079. }
  1080. // this.userinfo = JSON.parse(uni.getStorageSync('userInfo'));
  1081. this.userinfo = uni.getStorageSync('userinfo');
  1082. this.isAgreement = uni.getStorageSync('isAgreement');
  1083. this.$nextTick(() => {
  1084. this.setVideoProgress();
  1085. });
  1086. if (this.lookTimer) {
  1087. clearInterval(this.lookTimer);
  1088. this.lookTimer = null;
  1089. this.stayTime = 0;
  1090. this.startTime = 0;
  1091. }
  1092. // 启动内存监控,定期清理
  1093. this.startMemoryMonitor();
  1094. if (this.trafficTimer) {
  1095. clearInterval(this.trafficTimer);
  1096. this.trafficTimer = null;
  1097. this.startTime = 0;
  1098. this.totalTraffic = 0;
  1099. }
  1100. this.startTimer();
  1101. this.$nextTick(() => {
  1102. console.log(444, this.userInfo);
  1103. if (!this.userInfo || !this.userInfo.nickname) {
  1104. console.log('用户信息不完整,显示授权弹窗');
  1105. this.userlogo = true;
  1106. return;
  1107. }
  1108. // // 重新计算 isNow
  1109. // const isNowCalculated = dayjs(this.userInfo.updateTime).isSame(dayjs(), 'day');
  1110. // console.log('重新计算 isNow:', isNowCalculated);
  1111. // if (!isNowCalculated) {
  1112. // console.log('不是今天更新过,显示授权弹窗');
  1113. // this.userlogo = true;
  1114. // }
  1115. setTimeout(() => {
  1116. this.videoContext = uni.createVideoContext(`myVideo_${this.liveId}`, this);
  1117. console.log('videoContext 初始化完成:', this.videoContext);
  1118. }, 1000);
  1119. });
  1120. // 初始化横屏支持
  1121. // this.initFullscreenSupport();
  1122. },
  1123. //分享给好友
  1124. onShareAppMessage() {
  1125. return {
  1126. title: '邀请你来观看直播:' + this.liveItem.liveName,
  1127. path: '/pages_course/living?companyId=-2&companyUserId=' + this.userInfo.userId + '&liveId=' + this.liveId,
  1128. imageUrl: '/static/logo.png',
  1129. success(res) {
  1130. console.log('分享成功', res);
  1131. },
  1132. fail(err) {
  1133. console.error('分享失败', err);
  1134. }
  1135. };
  1136. },
  1137. // 分享到朋友圈
  1138. onShareTimeline() {
  1139. return {
  1140. title: '邀请你来观看直播:' + this.liveItem.liveName,
  1141. query: 'companyId=-2&companyUserId=' + this.userInfo.userId + '&liveId=' + this.liveId
  1142. };
  1143. },
  1144. computed: {
  1145. shouldShowIntegralPopup() {
  1146. // 只有当 showPoints 为 true 且 receiveList 有数据时才显示
  1147. return this.showPoints && this.receiveList && this.receiveList.length > 0;
  1148. },
  1149. // 格式化后的倒计时
  1150. formattedCountdown() {
  1151. return this.formatCountdown(this.remainingTime);
  1152. },
  1153. // 倒计时百分比
  1154. countdownPercentage() {
  1155. return this.getCountdownPercentage();
  1156. },
  1157. // 环形进度条样式
  1158. circularStyle() {
  1159. const percentage = this.getCountdownPercentage();
  1160. const strokeDashoffset = 314 - (314 * percentage) / 100; // 2 * π * 50 = 314
  1161. return {
  1162. '--percentage': percentage + '%',
  1163. '--stroke-dashoffset': strokeDashoffset
  1164. };
  1165. },
  1166. // 是否显示倒计时
  1167. showCountdown() {
  1168. return !this.hasReachedTarget &&
  1169. this.liveItem?.liveType === 2 && // 只对录播显示
  1170. this.remainingTime > 0 && // 还有剩余时间
  1171. this.remainingTime < this.liveItem.duration * this.completionRate; // 已经开始计时
  1172. },
  1173. // 控制是否显示非视频元素
  1174. shouldShowNonVideoElements() {
  1175. return !this.isFullscreen && this.showNonVideoElementsFlag;
  1176. },
  1177. // 控制是否显示全屏按钮
  1178. shouldShowFullscreenButton() {
  1179. return this.liveItem.showType == 1 && !this.isFullscreen;
  1180. },
  1181. ...mapGetters(['coureLogin']),
  1182. appid() {
  1183. return this.$store.state.appid
  1184. },
  1185. // 观看人数
  1186. formattedWatchCount() {
  1187. return this.formatNumber(this.virtualHuman || 0);
  1188. },
  1189. //点赞人数
  1190. formattedLikeCount() {
  1191. return this.formatNumber(this.liveViewData.like || 0);
  1192. },
  1193. filteredViewers() {
  1194. // 获取3个随机假头像,而不是显示真实观众
  1195. const avatarCount = 3; // 需要显示的假头像数量
  1196. // 如果fakeAvatar数组为空或不存在,返回空数组
  1197. if (!this.fakeAvatar || !Array.isArray(this.fakeAvatar) || this.fakeAvatar.length === 0) {
  1198. return [];
  1199. }
  1200. // 从fakeAvatar中随机选择3个不同的头像
  1201. const randomAvatars = [];
  1202. const availableAvatars = [...this.fakeAvatar]; // 创建副本避免修改原数组
  1203. // 确保有足够的头像可选
  1204. if (availableAvatars.length < avatarCount) {
  1205. // 如果头像数量不足,可以重复使用
  1206. for (let i = 0; i < avatarCount; i++) {
  1207. const randomIndex = Math.floor(Math.random() * availableAvatars.length);
  1208. randomAvatars.push(availableAvatars[randomIndex]);
  1209. }
  1210. } else {
  1211. // 随机选择不重复的头像
  1212. for (let i = 0; i < avatarCount; i++) {
  1213. const randomIndex = Math.floor(Math.random() * availableAvatars.length);
  1214. randomAvatars.push(availableAvatars[randomIndex]);
  1215. availableAvatars.splice(randomIndex, 1); // 移除已选的头像,避免重复
  1216. }
  1217. }
  1218. console.log("随机选择的头像:", randomAvatars);
  1219. return randomAvatars;
  1220. },
  1221. isCurrentUserWon() {
  1222. if (!Array.isArray(this.prizeInfo) || !this.userInfo?.userId) {
  1223. return false;
  1224. }
  1225. return this.prizeInfo.some((item) => {
  1226. return String(item.userId) === String(this.userInfo.userId);
  1227. });
  1228. },
  1229. getCurrentUserPrizeProductId() {
  1230. if (!Array.isArray(this.prizeInfo) || !this.userInfo?.userId) {
  1231. return null;
  1232. }
  1233. // 在 prizeInfo 中查找当前用户的中奖记录
  1234. const userPrize = this.prizeInfo.find(item => {
  1235. return String(item.userId) == String(this.userInfo.userId);
  1236. });
  1237. // 返回商品ID,如果没有找到则返回null
  1238. return userPrize ? userPrize.productId : null;
  1239. },
  1240. getCurrentUserPrizeRecordId() {
  1241. if (!Array.isArray(this.prizeInfo) || !this.userInfo?.userId) {
  1242. console.log('条件不满足,返回null');
  1243. return null;
  1244. }
  1245. const userPrize = this.prizeInfo.find(item => {
  1246. const match = String(item.userId) == String(this.userInfo.userId);
  1247. return match;
  1248. });
  1249. return userPrize ? userPrize.recordId : null;
  1250. },
  1251. nameuser() {
  1252. return this.userInfo.nickname
  1253. },
  1254. },
  1255. onHide() {
  1256. this.stopCountdown();
  1257. if (this.watchTimeTimer) {
  1258. clearInterval(this.watchTimeTimer);
  1259. this.watchTimeTimer = null;
  1260. }
  1261. this.saveWatchTime();
  1262. // 清除所有定时器
  1263. // this.clearAllTimersEnhanced();
  1264. // this.stopHeartBeat();
  1265. if (this.trafficTimer) {
  1266. clearInterval(this.trafficTimer);
  1267. }
  1268. // 暂停观看时长统计
  1269. this.pauseWatchDurationTracking();
  1270. // 页面隐藏时清理部分数据,减少内存占用
  1271. if (this.talklist && this.talklist.length > 20) {
  1272. // 只保留最新的20条消息
  1273. this.talklist = this.talklist.slice(-20);
  1274. }
  1275. // 触发垃圾回收
  1276. // this.triggerGarbageCollection();
  1277. },
  1278. onVideoStateChange(e) {
  1279. // 这里可以根据视频状态控制倒计时
  1280. if (e.detail.code === 2004) { // 视频开始播放
  1281. this.startCountdown();
  1282. } else if (e.detail.code === 2006) { // 视频播放结束
  1283. this.stopCountdown();
  1284. }
  1285. },
  1286. onUnload() {
  1287. this.stopCountdown();
  1288. if (this.watchTimeTimer) {
  1289. clearInterval(this.watchTimeTimer);
  1290. this.watchTimeTimer = null;
  1291. }
  1292. this.saveWatchTime();
  1293. // 保存视频进度
  1294. this.saveVideoProgress();
  1295. // 停止观看时长统计
  1296. this.stopWatchDurationTracking();
  1297. // 清理直播相关定时器
  1298. if (this.liveItem) {
  1299. this.pauseVideo();
  1300. // 清除直播间的时间定时器
  1301. if (this.liveItem.timeTimer) {
  1302. clearInterval(this.liveItem.timeTimer);
  1303. this.liveItem.timeTimer = null;
  1304. }
  1305. }
  1306. // 关闭WebSocket连接(会自动清理相关资源)
  1307. this.closeWebSocket(true);
  1308. // 清除所有定时器(使用增强清理)
  1309. this.clearAllTimersEnhanced();
  1310. // 暂停视频
  1311. const videoId = `myVideo_${this.liveId}`;
  1312. const videoContext = uni.createVideoContext(videoId, this);
  1313. if (videoContext) {
  1314. videoContext.pause();
  1315. }
  1316. // 移除网络状态监听
  1317. try {
  1318. uni.offNetworkStatusChange();
  1319. } catch (err) {
  1320. console.warn('移除网络状态监听失败:', err);
  1321. }
  1322. // 清理大数据和状态
  1323. this.clearBigData();
  1324. this.resetAllStates();
  1325. // 解锁屏幕方向
  1326. this.unlockOrientation();
  1327. // 强制退出全屏
  1328. this.isFullscreen = false;
  1329. this.showCustomControls = true;
  1330. },
  1331. mounted() {
  1332. // 初始化系统信息缓存,避免在键盘监听器中重复调用同步API
  1333. this.systemInfo = uni.getSystemInfoSync();
  1334. console.log('系统信息:', this.systemInfo.platform, this.systemInfo.model);
  1335. this.getCurrentActivities();
  1336. this.getliveOrder();
  1337. },
  1338. watch: {
  1339. coureLogin: {
  1340. immediate: true, // 页面一进入就检查一次
  1341. handler(val) {
  1342. if (val == 2 && this.isLogin) {
  1343. this.isLogin = false
  1344. this.goLogin()
  1345. }
  1346. }
  1347. },
  1348. // 监听liveItem.status的变化
  1349. 'liveItem.status': {
  1350. handler(newStatus, oldStatus) {
  1351. if (newStatus === undefined || oldStatus === undefined) return;
  1352. if (newStatus === oldStatus) return;
  1353. if (!this.liveId) {
  1354. console.warn('liveId不存在,无法触发getliving请求');
  1355. return;
  1356. }
  1357. // 状态变化时,调用getliving更新直播间数据
  1358. // console.log(`liveItem.status从${oldStatus}变为${newStatus},触发getliving请求`);
  1359. this.getliving(this.liveId);
  1360. },
  1361. deep: true
  1362. },
  1363. // 监听orderUser.count的变化
  1364. 'orderUser.count': {
  1365. handler(newVal, oldVal) {
  1366. if (newVal !== this.prevOrderCount) {
  1367. this.prevOrderCount = newVal;
  1368. this.showPurchaseMessage();
  1369. }
  1370. },
  1371. immediate: true
  1372. },
  1373. 'isShowNotice': {
  1374. handler(newVal, oldVal) {
  1375. setTimeout(() => {
  1376. this.noticeHeightFun()
  1377. }, 800)
  1378. },
  1379. immediate: true
  1380. }
  1381. },
  1382. methods: { // ======================== 观看时长统计相关方法 ========================
  1383. /**
  1384. * 开始观看时长统计
  1385. * WebSocket连接成功后调用
  1386. */
  1387. startWatchDurationTracking() {
  1388. // 防止重复启动
  1389. if (this.watchStartTime > 0) {
  1390. console.log('观看时长统计已启动,跳过');
  1391. return;
  1392. }
  1393. this.watchStartTime = Date.now();
  1394. this.isPageVisible = true;
  1395. console.log('开始观看时长统计', new Date(this.watchStartTime).toLocaleString());
  1396. // 启动页面可见性监听
  1397. this.initPageVisibilityListener();
  1398. },
  1399. /**
  1400. * 获取当前累计观看时长(秒)
  1401. * @returns {number} 观看时长(秒)
  1402. */
  1403. getCurrentWatchDuration() {
  1404. if (this.watchStartTime === 0) {
  1405. return this.accumulatedWatchDuration;
  1406. }
  1407. // 如果页面可见,计算当前时段的时长
  1408. if (this.isPageVisible) {
  1409. const currentDuration = Math.floor((Date.now() - this.watchStartTime) / 1000);
  1410. return this.accumulatedWatchDuration + currentDuration;
  1411. }
  1412. // 页面不可见,返回累计时长
  1413. return this.accumulatedWatchDuration;
  1414. },
  1415. /**
  1416. * 暂停观看时长统计(页面隐藏/后台时)
  1417. */
  1418. pauseWatchDurationTracking() {
  1419. if (!this.isPageVisible || this.watchStartTime === 0) {
  1420. return;
  1421. }
  1422. // 累加本次观看的时长
  1423. const currentSessionDuration = Math.floor((Date.now() - this.watchStartTime) / 1000);
  1424. this.accumulatedWatchDuration += currentSessionDuration;
  1425. this.lastPauseTime = Date.now();
  1426. this.isPageVisible = false;
  1427. console.log(`暂停观看统计: 本次=${currentSessionDuration}秒, 累计=${this.accumulatedWatchDuration}秒`);
  1428. },
  1429. /**
  1430. * 恢复观看时长统计(页面显示/前台时)
  1431. */
  1432. resumeWatchDurationTracking() {
  1433. if (this.isPageVisible) {
  1434. return;
  1435. }
  1436. // 重新记录开始时间
  1437. this.watchStartTime = Date.now();
  1438. this.isPageVisible = true;
  1439. const pauseDuration = this.lastPauseTime > 0 ? Math.floor((Date.now() - this.lastPauseTime) / 1000) : 0;
  1440. console.log(`恢夏观看统计: 暂停了${pauseDuration}秒, 当前累计=${this.accumulatedWatchDuration}秒`);
  1441. },
  1442. /**
  1443. * 停止观看时长统计
  1444. */
  1445. stopWatchDurationTracking() {
  1446. if (this.isPageVisible && this.watchStartTime > 0) {
  1447. const currentSessionDuration = Math.floor((Date.now() - this.watchStartTime) / 1000);
  1448. this.accumulatedWatchDuration += currentSessionDuration;
  1449. }
  1450. console.log(`停止观看统计: 总时长=${this.accumulatedWatchDuration}秒`);
  1451. // 重置状态
  1452. this.watchStartTime = 0;
  1453. this.isPageVisible = true;
  1454. this.lastPauseTime = 0;
  1455. // 移除监听
  1456. this.removePageVisibilityListener();
  1457. },
  1458. /**
  1459. * 初始化页面可见性监听
  1460. */
  1461. initPageVisibilityListener() {
  1462. // uni-app 的页面生命周期已经在 onShow/onHide 中处理
  1463. // 这里主要监听小程序的前后台切换
  1464. uni.onAppShow(() => {
  1465. console.log('小程序回到前台');
  1466. this.resumeWatchDurationTracking();
  1467. });
  1468. uni.onAppHide(() => {
  1469. console.log('小程序切换到后台');
  1470. this.pauseWatchDurationTracking();
  1471. });
  1472. },
  1473. /**
  1474. * 移除页面可见性监听
  1475. */
  1476. removePageVisibilityListener() {
  1477. // uni-app 不需要手动移除监听,生命周期自动管理
  1478. console.log('页面可见性监听已移除');
  1479. },
  1480. // 初始化倒计时
  1481. initCountdown() {
  1482. // 检查 liveItem 和 duration 是否存在
  1483. if (!this.liveItem || !this.liveItem.duration) {
  1484. console.warn('直播时长数据未加载,延迟初始化倒计时');
  1485. // 延迟初始化,等待数据加载
  1486. setTimeout(() => {
  1487. if (this.liveItem && this.liveItem.duration) {
  1488. this.initCountdown();
  1489. } else {
  1490. console.error('无法获取直播时长数据');
  1491. this.remainingTime = 0;
  1492. }
  1493. }, 1000);
  1494. return;
  1495. }
  1496. // 创建基于用户ID和直播ID的存储键
  1497. const userId = this.userInfo?.userId || 'anonymous';
  1498. this.watchProgressKey = `watchProgress_${userId}_${this.liveId}`;
  1499. // 从本地存储加载观看进度
  1500. try {
  1501. const storedProgress = uni.getStorageSync(this.watchProgressKey);
  1502. this.remainingTime = storedProgress ? parseInt(storedProgress) : this.liveItem.duration * this
  1503. .completionRate;
  1504. console.log(`加载剩余时间: ${this.remainingTime}秒`);
  1505. } catch (error) {
  1506. console.error('加载观看进度失败:', error);
  1507. this.remainingTime = this.liveItem.duration * this.completionRate;
  1508. }
  1509. // 如果剩余时间小于等于0,重置为总时长
  1510. if (this.remainingTime <= 0) {
  1511. console.log("超过了")
  1512. this.remainingTime = this.liveItem.duration * this.completionRate;
  1513. }
  1514. // 更新显示
  1515. this.updateCountdownDisplay();
  1516. },
  1517. // 开始倒计时
  1518. startCountdown() {
  1519. if (this.countdownTimer) {
  1520. clearInterval(this.countdownTimer);
  1521. }
  1522. // 如果已经完成,不启动倒计时
  1523. if (this.remainingTime <= 0) {
  1524. this.isCountdownActive = false;
  1525. return;
  1526. }
  1527. this.isCountdownActive = true;
  1528. this.lastUpdateTime = Date.now();
  1529. this.countdownTimer = setInterval(() => {
  1530. this.updateCountdown();
  1531. }, 1000); // 每秒更新一次
  1532. },
  1533. // 更新倒计时
  1534. updateCountdown() {
  1535. if (!this.isCountdownActive || this.remainingTime <= 0) {
  1536. this.stopCountdown();
  1537. return;
  1538. }
  1539. const now = Date.now();
  1540. const elapsedSeconds = Math.floor((now - this.lastUpdateTime) / 1000);
  1541. if (elapsedSeconds > 0) {
  1542. this.remainingTime = Math.max(0, this.remainingTime - elapsedSeconds);
  1543. this.lastUpdateTime = now;
  1544. // 更新显示
  1545. this.updateCountdownDisplay();
  1546. // 每10秒保存一次进度
  1547. if (this.remainingTime % 10 === 0 || this.remainingTime <= 0) {
  1548. this.saveWatchProgress();
  1549. }
  1550. // 倒计时完成
  1551. if (this.remainingTime <= 0) {
  1552. this.onCountdownComplete();
  1553. }
  1554. }
  1555. },
  1556. // 停止倒计时
  1557. stopCountdown() {
  1558. if (this.countdownTimer) {
  1559. clearInterval(this.countdownTimer);
  1560. this.countdownTimer = null;
  1561. }
  1562. this.isCountdownActive = false;
  1563. this.saveWatchProgress(); // 立即保存
  1564. },
  1565. // 倒计时完成
  1566. onCountdownComplete() {
  1567. console.log('倒计时完成!');
  1568. // this.completionUnreceived()
  1569. this.stopCountdown();
  1570. this.remainingTime = 0;
  1571. this.updateCountdownDisplay();
  1572. // 触发完成事件(例如发送通知到服务器)
  1573. this.triggerWatchComplete();
  1574. },
  1575. // 触发观看完成事件
  1576. async triggerWatchComplete() {
  1577. if (!this.userInfo?.userId || !this.liveId) return;
  1578. try {
  1579. // 这里可以调用API通知服务器用户已完成观看
  1580. console.log('用户已完成观看');
  1581. // 清空进度,防止重复计算
  1582. uni.removeStorageSync(this.watchProgressKey);
  1583. } catch (error) {
  1584. console.error('触发观看完成事件失败:', error);
  1585. }
  1586. },
  1587. // 保存观看进度
  1588. saveWatchProgress() {
  1589. if (!this.watchProgressKey) return;
  1590. try {
  1591. uni.setStorageSync(this.watchProgressKey, this.remainingTime);
  1592. console.log(`保存观看进度: 剩余${this.remainingTime}秒`);
  1593. } catch (error) {
  1594. console.error('保存观看进度失败:', error);
  1595. }
  1596. },
  1597. // 重置倒计时
  1598. resetCountdown() {
  1599. this.stopCountdown();
  1600. this.remainingTime = this.liveItem.duration * this.completionRate;
  1601. this.updateCountdownDisplay();
  1602. uni.removeStorageSync(this.watchProgressKey);
  1603. uni.showToast({
  1604. title: '倒计时已重置',
  1605. icon: 'success',
  1606. duration: 1500
  1607. });
  1608. },
  1609. // 补零函数(确保两位数,核心依赖)
  1610. padZero(num) {
  1611. // 先转数字再处理,避免字符串/NaN问题
  1612. const n = Number(num);
  1613. return n < 10 ? `0${n}` : `${n}`;
  1614. },
  1615. // 倒计时格式化函数
  1616. formatCountdown(seconds) {
  1617. // 1. 先校验输入:确保是有效正整数,避免负数/非数字导致计算错误
  1618. const totalSeconds = Math.max(0, Math.floor(Number(seconds) || 0));
  1619. // 2. 重新梳理计算逻辑(核心修复)
  1620. const hours = Math.floor(totalSeconds / 3600);
  1621. const remainingAfterHours = totalSeconds % 3600; // 小时取余后剩余的秒数
  1622. const minutes = Math.floor(remainingAfterHours / 60);
  1623. const secs = remainingAfterHours % 60;
  1624. // 3. 返回格式化结果(补零+原始值)
  1625. return {
  1626. hours: this.padZero(hours),
  1627. minutes: this.padZero(minutes),
  1628. seconds: this.padZero(secs),
  1629. total: totalSeconds // 确保total是处理后的有效数值
  1630. };
  1631. },
  1632. getCountdownPercentage() {
  1633. // 1. 使用倒计时的 remainingTime 计算进度
  1634. if (!this.liveItem || typeof this.liveItem.duration !== 'number' || this.liveItem.duration <= 0) {
  1635. console.log('进度条计算:liveItem.duration无效', this.liveItem?.duration);
  1636. return 0;
  1637. }
  1638. // 2. 计算目标观看时间(90%的总时长)
  1639. const targetWatchTime = this.liveItem.duration * this.completionRate;
  1640. // 3. 如果已经达到目标,直接返回100%
  1641. if (this.hasReachedTarget || this.remainingTime <= 0) {
  1642. return 100;
  1643. }
  1644. // 4. 基于 remainingTime 计算进度
  1645. // remainingTime = 剩余需要观看的时间
  1646. // 已观看时间 = 总目标时间 - 剩余时间
  1647. const watchedTime = targetWatchTime - this.remainingTime;
  1648. // 5. 确保数值有效
  1649. if (watchedTime <= 0) {
  1650. return 0;
  1651. }
  1652. // 6. 计算百分比(已观看时间 / 目标时间)
  1653. const percentage = (watchedTime / targetWatchTime) * 100;
  1654. // 7. 限制在 0-100 范围内
  1655. return Math.min(100, Math.max(0, percentage));
  1656. },
  1657. // 更新倒计时显示(如果需要强制更新视图)
  1658. updateCountdownDisplay() {
  1659. // 如果需要,可以在这里触发视图更新
  1660. this.$forceUpdate();
  1661. },
  1662. // 补零函数
  1663. padZero(num) {
  1664. return num < 10 ? `0${num}` : num.toString();
  1665. },
  1666. // 初始化观看时间
  1667. initWatchTime() {
  1668. // 创建基于用户ID和直播ID的存储键
  1669. const userId = this.userInfo?.userId || 'anonymous';
  1670. this.watchTimeStorageKey = `watchTime_${userId}_${this.liveId}`;
  1671. // 从本地存储加载累计观看时间
  1672. try {
  1673. const storedTime = uni.getStorageSync(this.watchTimeStorageKey);
  1674. this.totalWatchTime = storedTime ? parseInt(storedTime) : 0;
  1675. console.log(`加载累计观看时间: ${this.totalWatchTime}秒`);
  1676. } catch (error) {
  1677. console.error('加载观看时间失败:', error);
  1678. this.totalWatchTime = 0;
  1679. }
  1680. },
  1681. // 开始统计观看时间
  1682. startWatchTimeCounter() {
  1683. if (this.watchTimeTimer) {
  1684. clearInterval(this.watchTimeTimer);
  1685. }
  1686. this.lastSaveTime = Date.now();
  1687. this.watchTimeTimer = setInterval(() => {
  1688. this.updateWatchTime();
  1689. }, 1000); // 每秒更新一次
  1690. },
  1691. // 更新观看时间
  1692. updateWatchTime() {
  1693. const now = Date.now();
  1694. const elapsed = Math.floor((now - this.lastSaveTime) / 1000);
  1695. if (elapsed > 0) {
  1696. this.totalWatchTime += elapsed;
  1697. this.lastSaveTime = now;
  1698. // 每10秒或每次页面隐藏时保存一次
  1699. if (this.totalWatchTime % 10 === 0) {
  1700. this.saveWatchTime();
  1701. }
  1702. }
  1703. },
  1704. // 保存观看时间到本地存储
  1705. saveWatchTime() {
  1706. if (!this.watchTimeStorageKey) return;
  1707. try {
  1708. uni.setStorageSync(this.watchTimeStorageKey, this.totalWatchTime);
  1709. console.log(`保存观看时间: ${this.totalWatchTime}秒`);
  1710. } catch (error) {
  1711. console.error('保存观看时间失败:', error);
  1712. }
  1713. },
  1714. // 格式化显示时间
  1715. formatWatchTime(seconds) {
  1716. const hours = Math.floor(seconds / 3600);
  1717. const minutes = Math.floor((seconds % 3600) / 60);
  1718. const secs = seconds % 60;
  1719. return {
  1720. hours: this.padZero(hours),
  1721. minutes: this.padZero(minutes),
  1722. seconds: this.padZero(secs),
  1723. total: seconds
  1724. };
  1725. },
  1726. padZero(num) {
  1727. return num < 10 ? `0${num}` : num.toString();
  1728. },
  1729. // 获取用户未领取的积分列表
  1730. completionUnreceived() {
  1731. if (!this.liveId) return;
  1732. const data = {
  1733. liveId: this.liveId
  1734. }
  1735. completionUnreceived(data).then((res) => {
  1736. if (res.code == 200) {
  1737. console.log("获取用户未领取的积分列表", res)
  1738. if (res.data) {
  1739. this.receiveList = res.data
  1740. this.showPoints = true
  1741. }
  1742. }
  1743. }).catch((error) => {});
  1744. },
  1745. // 领取完课积分
  1746. completionReceive(item) {
  1747. if (!item.id) return;
  1748. completionReceive(item.id).then((res) => {
  1749. if (res.code == 200) {
  1750. this.showPoints = false
  1751. console.log("领取完课积分", res)
  1752. uni.showToast({
  1753. title: '领取积分成功',
  1754. icon: 'none'
  1755. });
  1756. }
  1757. }).catch((error) => {});
  1758. },
  1759. noticeHeightFun() {
  1760. const query = uni.createSelectorQuery().in(this);
  1761. query.select('.notice-message').boundingClientRect(rect => {
  1762. if (rect) {
  1763. this.commonViewHeight = rect.height;
  1764. this.forceScrollToBottomOnSend();
  1765. } else {
  1766. this.commonViewHeight = ''
  1767. }
  1768. }).exec();
  1769. },
  1770. // 获取胶囊按钮布局参数
  1771. getMenuButtonInfo() {
  1772. // 微信小程序API(Uniapp可直接用uni.getMenuButtonBoundingClientRect)
  1773. const menuBtn = uni.getMenuButtonBoundingClientRect();
  1774. if (menuBtn) {
  1775. this.menuButtonInfo = {
  1776. top: menuBtn.top + 'px', // 胶囊顶部距离
  1777. height: menuBtn.height + 'px', // 胶囊高度
  1778. centerY: (menuBtn.top + menuBtn.height / 2) + 'px', // 胶囊垂直居中Y坐标
  1779. right: menuBtn.right + 'px' // 胶囊右侧距离
  1780. };
  1781. }
  1782. },
  1783. // 退出全屏
  1784. exitFullscreen() {
  1785. console.log('执行退出全屏');
  1786. this.isFullscreen = false;
  1787. // 恢复竖屏样式
  1788. this.restoreVideoList();
  1789. // 显示非videolist元素
  1790. this.showNonVideoElements();
  1791. // 强制页面重排
  1792. this.$forceUpdate();
  1793. },
  1794. // 添加屏幕方向变化监听
  1795. addOrientationChangeListener() {
  1796. if (window.addEventListener) {
  1797. window.addEventListener('orientationchange', this.handleOrientationChange);
  1798. }
  1799. },
  1800. // 移除屏幕方向变化监听
  1801. removeOrientationChangeListener() {
  1802. if (window.removeEventListener) {
  1803. window.removeEventListener('orientationchange', this.handleOrientationChange);
  1804. }
  1805. }, // 处理屏幕方向变化
  1806. handleOrientationChange() {
  1807. uni.onWindowResize((res) => {
  1808. const windowWidth = res.size.windowWidth;
  1809. const windowHeight = res.size.windowHeight;
  1810. const isLandscape = windowWidth > windowHeight;
  1811. if (this.isFullscreen && !isLandscape) {
  1812. console.log('全屏状态下屏幕方向不正确,尝试退出全屏');
  1813. if (this.videoContext) {
  1814. this.videoContext.exitFullScreen();
  1815. }
  1816. }
  1817. });
  1818. }, // 恢复videolist盒子
  1819. restoreVideoList() {
  1820. this.isVideoRotated = false;
  1821. this.isFullscreen = false;
  1822. console.log('恢复视频容器竖屏状态');
  1823. },
  1824. // 强制横屏全屏(备用方案)
  1825. forceLandscapeFullscreen() {
  1826. // 尝试多种全屏方式
  1827. try {
  1828. // 方案1: 使用video的requestFullScreen
  1829. if (this.videoContext) {
  1830. this.videoContext.requestFullScreen({
  1831. direction: 90
  1832. });
  1833. return true;
  1834. }
  1835. } catch (e) {
  1836. console.log('方案1失败:', e);
  1837. }
  1838. try {
  1839. // 方案2: 尝试DOM全屏API
  1840. const videoElement = document.getElementById(`myVideo_${this.liveId}`);
  1841. if (videoElement && videoElement.requestFullscreen) {
  1842. videoElement.requestFullscreen();
  1843. return true;
  1844. }
  1845. } catch (e) {
  1846. console.log('方案2失败:', e);
  1847. }
  1848. try {
  1849. // 方案3: 使用Web API全屏
  1850. if (document.documentElement.requestFullscreen) {
  1851. document.documentElement.requestFullscreen();
  1852. return true;
  1853. }
  1854. } catch (e) {
  1855. console.log('方案3失败:', e);
  1856. }
  1857. console.log('所有全屏方案都失败了');
  1858. uni.showToast({
  1859. title: '无法全屏播放,请尝试手动横屏',
  1860. icon: 'none'
  1861. });
  1862. return false;
  1863. },
  1864. // 切换全屏
  1865. toggleFullscreen() {
  1866. console.log('自定义全屏按钮被点击');
  1867. if (this.isFullscreen) {
  1868. // 退出全屏
  1869. this.exitFullscreen();
  1870. } else {
  1871. // 进入全屏
  1872. this.enterFullscreen();
  1873. }
  1874. }, // 进入全屏
  1875. enterFullscreen() {
  1876. console.log('执行进入全屏');
  1877. this.isFullscreen = true;
  1878. // 设置横屏样式
  1879. this.rotateVideoList();
  1880. // 隐藏非videolist元素
  1881. this.hideNonVideoElements();
  1882. // 强制页面重排
  1883. this.$forceUpdate();
  1884. },
  1885. // 强制全屏的备用方案
  1886. forceFullscreen() {
  1887. console.log('使用备用方案进入全屏');
  1888. const videoId = `myVideo_${this.liveId}`;
  1889. // 在小程序中,只能使用 videoContext
  1890. if (this.videoContext) {
  1891. try {
  1892. this.videoContext.requestFullScreen({
  1893. direction: 90 // 横屏方向
  1894. });
  1895. } catch (error) {
  1896. console.error('请求全屏失败:', error);
  1897. uni.showToast({
  1898. title: '无法全屏播放,请尝试手动横屏',
  1899. icon: 'none'
  1900. });
  1901. }
  1902. }
  1903. }, // 隐藏非videolist元素
  1904. hideNonVideoElements() {
  1905. this.showNonVideoElementsFlag = false;
  1906. console.log('隐藏非视频元素');
  1907. },
  1908. // 显示非videolist元素
  1909. showNonVideoElements() {
  1910. this.showNonVideoElementsFlag = true;
  1911. console.log('显示非视频元素');
  1912. },
  1913. // 旋转videolist盒子
  1914. rotateVideoList() {
  1915. // 在小程序中,通过设置CSS类名来控制样式,而不是直接操作DOM
  1916. this.isVideoRotated = true;
  1917. this.isFullscreen = true;
  1918. // 在小程序中,应该通过绑定class的方式控制样式
  1919. // 而不是直接操作DOM
  1920. console.log('视频容器旋转到横屏状态');
  1921. },
  1922. // 全屏状态变化监听
  1923. onFullscreenChange(e) {
  1924. console.log('全屏状态变化事件详情:', e);
  1925. // 方法1:通过事件参数获取(小程序主要方式)
  1926. let fullScreen = false;
  1927. // 视频组件的全屏事件参数
  1928. if (e.detail && typeof e.detail.fullScreen !== 'undefined') {
  1929. fullScreen = e.detail.fullScreen;
  1930. console.log('通过e.detail.fullScreen获取全屏状态:', fullScreen);
  1931. }
  1932. // 其他可能的参数名
  1933. else if (e.detail && typeof e.detail.fullscreen !== 'undefined') {
  1934. fullScreen = e.detail.fullscreen;
  1935. console.log('通过e.detail.fullscreen获取全屏状态:', fullScreen);
  1936. } else {
  1937. // 在小程序环境中,可以通过屏幕方向判断
  1938. try {
  1939. const systemInfo = uni.getSystemInfoSync();
  1940. fullScreen = systemInfo.windowWidth > systemInfo.windowHeight;
  1941. console.log('通过屏幕方向判断全屏状态:', fullScreen);
  1942. } catch (err) {
  1943. console.error('获取系统信息失败:', err);
  1944. // 默认使用事件参数
  1945. fullScreen = e.detail || false;
  1946. }
  1947. }
  1948. this.isFullscreen = fullScreen;
  1949. console.log('最终设置isFullscreen为:', this.isFullscreen);
  1950. // 根据全屏状态显示/隐藏自定义控件
  1951. this.showCustomControls = !this.isFullscreen;
  1952. // 强制UI更新
  1953. this.$forceUpdate();
  1954. // 全屏时锁定横屏
  1955. if (this.isFullscreen) {
  1956. this.lockOrientation();
  1957. } else {
  1958. this.unlockOrientation();
  1959. }
  1960. },
  1961. // 锁定屏幕方向为横屏
  1962. lockOrientation() {
  1963. // 设置屏幕方向为横屏
  1964. try {
  1965. // 尝试锁定横屏
  1966. plus.screen.lockOrientation('landscape-primary');
  1967. } catch (e) {
  1968. console.log('锁定屏幕方向失败:', e);
  1969. // 备用方案
  1970. try {
  1971. // 使用 Web API(如果支持)
  1972. if (screen.orientation && screen.orientation.lock) {
  1973. screen.orientation.lock('landscape');
  1974. }
  1975. } catch (err) {
  1976. console.log('备用方案也失败了:', err);
  1977. }
  1978. }
  1979. }, // 解锁屏幕方向
  1980. unlockOrientation() {
  1981. try {
  1982. plus.screen.unlockOrientation();
  1983. } catch (e) {
  1984. console.log('解锁屏幕方向失败:', e);
  1985. // 备用方案
  1986. try {
  1987. if (screen.orientation && screen.orientation.unlock) {
  1988. screen.orientation.unlock();
  1989. }
  1990. } catch (err) {
  1991. console.log('备用解锁方案也失败了:', err);
  1992. }
  1993. }
  1994. },
  1995. shouquan() {
  1996. if (this.user.nickname == '') {
  1997. uni.showToast({
  1998. icon: 'none',
  1999. title: "请先授权微信昵称",
  2000. });
  2001. }
  2002. },
  2003. confimrname() {
  2004. if (this.user.nickname == '') {
  2005. uni.showToast({
  2006. icon: 'none',
  2007. title: "请授权微信昵称",
  2008. });
  2009. return
  2010. }
  2011. this.editUser();
  2012. uni.setStorageSync('userInfo', this.userInfo);
  2013. this.userlogo = false;
  2014. },
  2015. editUser() {
  2016. this.user.nickName = this.user.nickname;
  2017. editUser(this.user).then(
  2018. res => {
  2019. if (res.code == 200) {
  2020. uni.showToast({
  2021. icon: 'success',
  2022. title: "修改成功",
  2023. });
  2024. this.getUserInfo()
  2025. } else {
  2026. uni.showToast({
  2027. icon: 'none',
  2028. title: res.msg,
  2029. });
  2030. }
  2031. },
  2032. rej => {}
  2033. );
  2034. },
  2035. onNickNameInput(e) {
  2036. console.log(e)
  2037. this.user.nickname = e.detail.value
  2038. },
  2039. async haveLogin() {
  2040. this.userInfo = uni.getStorageSync('userInfo');
  2041. if (this.userInfo) {
  2042. await this.getUserInfo();
  2043. }
  2044. if (this.liveId) {
  2045. // 先获取直播间信息
  2046. await this.getliving(this.liveId);
  2047. this.isOnload = true;
  2048. await this.getLiveMsg(this.liveItem);
  2049. await this.getliveViewData();
  2050. this.getCurrentActivities();
  2051. this.getliveOrder();
  2052. this.initSocket();
  2053. }
  2054. },
  2055. // 静默登录
  2056. goLogin(data) {
  2057. let provider = 'weixin'
  2058. uni.login({
  2059. provider: provider,
  2060. success: async loginRes => {
  2061. console.log(loginRes)
  2062. uni.getUserInfo({
  2063. provider: provider,
  2064. success: (infoRes) => {
  2065. uni.showToast({
  2066. title: '登录中...',
  2067. icon: 'loading'
  2068. });
  2069. loginByMp({
  2070. code: loginRes.code,
  2071. encryptedData: infoRes.encryptedData,
  2072. iv: infoRes.iv,
  2073. appId: this.appid
  2074. }).then(res => {
  2075. uni.hideLoading();
  2076. if (res.code == 200) {
  2077. uni.setStorageSync('AppToken', res.token);
  2078. uni.setStorageSync('userInfo', JSON.stringify(res
  2079. .user));
  2080. this.userInfo = uni.getStorageSync('userInfo');
  2081. // uni.setStorageSync('auto_userInfo', JSON.stringify(res.user));
  2082. // this.user = res.user
  2083. this.$store.commit('setCoureLogin', 1);
  2084. this.isLogin = true
  2085. this.haveLogin()
  2086. this.userlogo = true
  2087. // console.log("TOKEN_KEYAuto",TOKEN_KEYAuto)
  2088. // this.getIsAddKf()
  2089. } else {
  2090. uni.showToast({
  2091. title: res.msg,
  2092. icon: 'none'
  2093. });
  2094. }
  2095. }).catch(err => {
  2096. uni.hideLoading();
  2097. uni.showToast({
  2098. icon: 'none',
  2099. title: "登录失败,请重新登录",
  2100. });
  2101. });
  2102. }
  2103. });
  2104. }
  2105. })
  2106. },
  2107. clickTabs(item) {},
  2108. getLocationByIP() {
  2109. // 高德IP定位API,需要替换成你的key
  2110. const key = '4e13632be0cc278f56825919603c07cf';
  2111. uni.request({
  2112. url: `https://restapi.amap.com/v3/ip?key=${key}`,
  2113. method: 'GET',
  2114. success: (res) => {
  2115. if (res.data && res.data.status === '1') {
  2116. // 返回数据格式:{ province: '广东省', city: '深圳市', ... }
  2117. // ${res.data.city}
  2118. this.location = `${res.data.province} `;
  2119. this.address = this.location
  2120. console.log('用户所在地:', this.location);
  2121. } else {
  2122. console.error('获取位置失败:', res.data);
  2123. }
  2124. },
  2125. fail: (err) => {
  2126. console.error('请求失败:', err);
  2127. }
  2128. });
  2129. },
  2130. formatNumber(num) {
  2131. if (typeof num !== 'number') {
  2132. num = Number(num) || 0;
  2133. }
  2134. if (num < 10000) {
  2135. return num.toString();
  2136. }
  2137. // 处理万以上的数字
  2138. const wan = num / 10000;
  2139. if (wan < 10) {
  2140. // 1万-10万之间,保留2位小数
  2141. const rounded = Math.round(wan * 100) / 100;
  2142. return rounded.toFixed(2).replace(/\.?0+$/, '') + 'w';
  2143. } else if (wan < 10000) {
  2144. // 10万-1亿之间,保留1位小数
  2145. const rounded = Math.round(wan * 10) / 10;
  2146. return rounded.toFixed(1).replace(/\.0$/, '') + 'w';
  2147. } else {
  2148. // 1亿以上,转换为亿单位
  2149. const yi = wan / 10000;
  2150. const rounded = Math.round(yi * 100) / 100;
  2151. return rounded.toFixed(2).replace(/\.?0+$/, '') + '亿';
  2152. }
  2153. },
  2154. // 重置所有状态
  2155. resetAllStates() {
  2156. this.liveUserCalled = false;
  2157. this.talklist = [];
  2158. this.liveViewersData = [];
  2159. this.liveViewers = [];
  2160. this.products = [];
  2161. this.liveItem = [];
  2162. // 重置WebSocket相关状态
  2163. this.isSocketOpen = false;
  2164. this.isConnecting = false;
  2165. this.isManualClose = true;
  2166. this.reconnectCount = 0;
  2167. this.heartBeatRetryCount = 0;
  2168. this.lastHeartBeatTime = 0;
  2169. this.adaptiveHeartBeatInterval = this.heartBeatInterval;
  2170. this.isNetworkAvailable = true;
  2171. this.networkType = 'unknown';
  2172. // 重置性能监控数据
  2173. this.connectionStartTime = 0;
  2174. this.connectionLatency = 0;
  2175. this.messageCount = 0;
  2176. this.errorCount = 0;
  2177. this.lastPerformanceCheck = 0;
  2178. },
  2179. // 获取WebSocket性能统计
  2180. getWebSocketPerformanceStats() {
  2181. const now = Date.now();
  2182. const uptime = this.connectionStartTime > 0 ? now - this.connectionStartTime : 0;
  2183. return {
  2184. connectionLatency: this.connectionLatency,
  2185. uptime: uptime,
  2186. messageCount: this.messageCount,
  2187. errorCount: this.errorCount,
  2188. errorRate: this.messageCount > 0 ? (this.errorCount / this.messageCount * 100).toFixed(2) : 0,
  2189. messagesPerSecond: uptime > 0 ? (this.messageCount / (uptime / 1000)).toFixed(2) : 0,
  2190. networkType: this.networkType,
  2191. isConnected: this.isSocketAvailable(),
  2192. reconnectCount: this.reconnectCount
  2193. };
  2194. },
  2195. // 定期检查性能并输出统计信息
  2196. performanceCheck() {
  2197. const now = Date.now();
  2198. // 每5分钟输出一次性能统计
  2199. if (now - this.lastPerformanceCheck > 300000) { // 5分钟
  2200. const stats = this.getWebSocketPerformanceStats();
  2201. console.log('WebSocket性能统计:', stats);
  2202. this.lastPerformanceCheck = now;
  2203. // 如果错误率过高,记录警告
  2204. if (stats.errorRate > 10) {
  2205. console.warn(`WebSocket错误率过高: ${stats.errorRate}%`);
  2206. }
  2207. // 如果连接延迟过高,记录警告
  2208. if (stats.connectionLatency > 5000) {
  2209. console.warn(`WebSocket连接延迟过高: ${stats.connectionLatency}ms`);
  2210. }
  2211. }
  2212. },
  2213. // 定期清理视频缓存
  2214. startVideoCacheCleanup() {
  2215. if (this.videoCleanupTimer) {
  2216. clearInterval(this.videoCleanupTimer);
  2217. }
  2218. // 每30秒清理一次视频缓存
  2219. this.videoCleanupTimer = setInterval(() => {
  2220. this.cleanupVideoCache();
  2221. }, 30000);
  2222. },
  2223. // 清理视频缓存
  2224. cleanupVideoCache() {
  2225. if (!this.liveItem || !this.liveId) return;
  2226. // 只对录播视频进行清理
  2227. if (this.liveItem.liveType === 2 && this.liveItem.videoUrl) {
  2228. this.reloadVideoPlayer();
  2229. }
  2230. },
  2231. // 重新加载视频播放器
  2232. reloadVideoPlayer() {
  2233. const videoId = `myVideo_${this.liveId}`;
  2234. const videoContext = uni.createVideoContext(videoId, this);
  2235. if (videoContext) {
  2236. const currentTime = this.videoCurrentTime;
  2237. // 暂停视频
  2238. videoContext.pause();
  2239. // 延迟重新加载
  2240. setTimeout(() => {
  2241. // 重新设置视频源,强制清理缓存
  2242. this.$set(this.liveItem, 'videoUrl', this.liveItem.videoUrl + '&t=' + Date.now());
  2243. // 恢复播放位置
  2244. setTimeout(() => {
  2245. if (videoContext.seek) {
  2246. videoContext.seek(currentTime);
  2247. }
  2248. videoContext.play();
  2249. }, 500);
  2250. }, 100);
  2251. }
  2252. },
  2253. // 启动内存监控
  2254. startMemoryMonitoring() {
  2255. if (this.memoryMonitorTimer) {
  2256. clearInterval(this.memoryMonitorTimer);
  2257. }
  2258. this.memoryMonitorTimer = setInterval(() => {
  2259. this.checkMemoryUsage();
  2260. }, 15000); // 每15秒检查一次
  2261. },
  2262. // 检查内存使用情况
  2263. checkMemoryUsage() {
  2264. try {
  2265. // 检查定时器数量
  2266. const timerCount = this.getActiveTimerCount();
  2267. if (timerCount > 10) {
  2268. console.warn(`检测到过多定时器: ${timerCount}个,可能存在内存泄漏`);
  2269. this.cleanupUnusedTimers();
  2270. }
  2271. // 检查消息队列长度
  2272. if (this.messageQueue && this.messageQueue.length > 100) {
  2273. console.warn(`消息队列过长: ${this.messageQueue.length}条,清理旧消息`);
  2274. this.messageQueue = this.messageQueue.slice(-50); // 只保留最新50条
  2275. }
  2276. // 检查用户颜色缓存
  2277. if (this.userRandomColors && Object.keys(this.userRandomColors).length > 500) {
  2278. console.warn('用户颜色缓存过大,清理部分缓存');
  2279. const keys = Object.keys(this.userRandomColors);
  2280. const keysToRemove = keys.slice(0, keys.length - 200); // 只保留最新200个
  2281. keysToRemove.forEach(key => delete this.userRandomColors[key]);
  2282. }
  2283. // 定期触发垃圾回收
  2284. if (Date.now() - this.lastPerformanceCheck > 60000) { // 每分钟一次
  2285. this.triggerGarbageCollection();
  2286. this.lastPerformanceCheck = Date.now();
  2287. }
  2288. } catch (error) {
  2289. console.error('内存检查失败:', error);
  2290. }
  2291. },
  2292. // 强制内存清理
  2293. forceMemoryCleanup() {
  2294. console.log('执行强制内存清理');
  2295. // 1. 清理视频播放器
  2296. this.cleanupVideoPlayer();
  2297. // 2. 清理大数据
  2298. this.clearBigData();
  2299. // 3. 强制垃圾回收(在支持的环境下)
  2300. this.triggerGarbageCollection();
  2301. },
  2302. // 清理视频播放器
  2303. cleanupVideoPlayer() {
  2304. const videoId = `myVideo_${this.liveId}`;
  2305. const videoContext = uni.createVideoContext(videoId, this);
  2306. if (videoContext) {
  2307. // 停止播放
  2308. videoContext.stop();
  2309. // 重置视频源
  2310. setTimeout(() => {
  2311. this.$set(this.liveItem, 'videoUrl', '');
  2312. setTimeout(() => {
  2313. this.$set(this.liveItem, 'videoUrl', this.getFreshVideoUrl());
  2314. this.playVideo();
  2315. }, 500);
  2316. }, 100);
  2317. }
  2318. },
  2319. // 获取带时间戳的新视频URL
  2320. getFreshVideoUrl() {
  2321. if (!this.liveItem.originalVideoUrl) {
  2322. this.liveItem.originalVideoUrl = this.liveItem.videoUrl;
  2323. }
  2324. // 添加时间戳参数,避免缓存
  2325. const separator = this.liveItem.originalVideoUrl.includes('?') ? '&' : '?';
  2326. return this.liveItem.originalVideoUrl + separator + 't=' + Date.now();
  2327. },
  2328. // 清理大数据
  2329. clearBigData() {
  2330. // 清理聊天记录,只保留最近50条
  2331. if (this.talklist.length > 50) {
  2332. this.talklist = this.talklist.slice(-50);
  2333. }
  2334. // 清理虚拟数据
  2335. if (this.liveViewers.length > 100) {
  2336. this.liveViewers = this.liveViewers.slice(-100);
  2337. }
  2338. // 清理商品数据
  2339. if (this.products.length > 50) {
  2340. this.products = this.products.slice(0, 50);
  2341. }
  2342. // 强制垃圾回收(如果支持)
  2343. if (typeof gc === 'function') {
  2344. gc();
  2345. }
  2346. },
  2347. // 触发垃圾回收(在支持的环境下)
  2348. triggerGarbageCollection() {
  2349. if (wx && wx.triggerGC) {
  2350. wx.triggerGC();
  2351. }
  2352. },
  2353. // 强制滚动到底部(备用方案)
  2354. forceScrollToBottom() {
  2355. // console.log('执行强制滚动到底部');
  2356. // 方案1:直接设置一个很大的scrollTop值
  2357. this.scrollTop = 999999;
  2358. // 方案2:使用scroll-into-view滚动到最后一个元素
  2359. this.$nextTick(() => {
  2360. if (this.talklist && this.talklist.length > 0) {
  2361. const lastMessage = this.talklist[this.talklist.length - 1];
  2362. const targetId = `list_${lastMessage.uniqueId || (this.talklist.length - 1)}`;
  2363. console.log(
  2364. `尝试滚动到元素: ${targetId}, 当前消息数量: ${this.talklist.length}, 最后消息ID: ${lastMessage.uniqueId}`
  2365. );
  2366. this.scrollIntoView = targetId;
  2367. // 清除scroll-into-view,避免影响后续滚动
  2368. setTimeout(() => {
  2369. this.scrollIntoView = '';
  2370. }, 200);
  2371. }
  2372. // 方案3:使用更大的延迟再次设置scrollTop
  2373. setTimeout(() => {
  2374. this.scrollTop = 999999;
  2375. console.log('延迟设置scrollTop为999999');
  2376. }, 100);
  2377. // 方案4:300ms后使用原生API作为最终备用方案
  2378. setTimeout(() => {
  2379. this.nativeScrollToBottom();
  2380. }, 300);
  2381. });
  2382. },
  2383. // 强制滚动到底部(用于发送消息,绕过防抖限制)
  2384. forceScrollToBottomOnSend() {
  2385. // console.log('执行强制滚动到底部(发送消息专用)');
  2386. // 清理之前的定时器
  2387. if (this.scrollTimer) {
  2388. clearTimeout(this.scrollTimer);
  2389. this.scrollTimer = null;
  2390. }
  2391. // 强制更新滚动时间,确保不被防抖阻止
  2392. this.lastScrollTime = Date.now();
  2393. // 清除scroll-into-view,避免冲突
  2394. this.scrollIntoView = '';
  2395. // 方案1:立即使用scroll-into-view滚动到最后一条消息
  2396. if (this.talklist.length > 0) {
  2397. const lastMessage = this.talklist[this.talklist.length - 1];
  2398. if (lastMessage && lastMessage.uniqueId) {
  2399. this.scrollIntoView = `list_${lastMessage.uniqueId}`;
  2400. //console.log(`强制滚动方案1: 设置scrollIntoView为list_${lastMessage.uniqueId}`);
  2401. }
  2402. }
  2403. // 方案2:同时设置scrollTop为一个非常大的值
  2404. const targetScrollTop = Date.now(); // 使用时间戳确保每次都不同
  2405. this.scrollTop = targetScrollTop;
  2406. //console.log('强制滚动方案2: 设置scrollTop为', targetScrollTop);
  2407. // 方案3:立即尝试原生滚动
  2408. this.nativeScrollToBottom();
  2409. // 方案4:使用nextTick确保DOM更新后再次滚动
  2410. this.$nextTick(() => {
  2411. // 清除scroll-into-view后重新设置scrollTop
  2412. this.scrollIntoView = '';
  2413. this.scrollTop = targetScrollTop + 1;
  2414. //console.log('强制滚动方案3: nextTick后重新设置scrollTop');
  2415. // 再次尝试原生滚动
  2416. setTimeout(() => {
  2417. this.nativeScrollToBottom();
  2418. }, 50);
  2419. });
  2420. // 方案5:延迟检查和强制滚动(最终保障)
  2421. this.scrollTimer = setTimeout(() => {
  2422. this.checkAndForceScroll(targetScrollTop);
  2423. this.scrollTimer = null;
  2424. }, 200);
  2425. },
  2426. // 检查并强制滚动
  2427. checkAndForceScroll(targetScrollTop) {
  2428. //console.log(`检查滚动状态: 当前位置=${this.currentScrollTop}, 目标位置=${targetScrollTop}`);
  2429. // 无论当前位置如何,都尝试强制滚动到底部
  2430. // 使用scroll-into-view滚动到最后一条消息
  2431. if (this.talklist.length > 0) {
  2432. const lastMessage = this.talklist[this.talklist.length - 1];
  2433. if (lastMessage && lastMessage.uniqueId) {
  2434. this.scrollIntoView = `list_${lastMessage.uniqueId}`;
  2435. //console.log(`最终检查: 设置scrollIntoView为list_${lastMessage.uniqueId}`);
  2436. }
  2437. }
  2438. // 设置一个新的scrollTop值
  2439. this.scrollTop = Date.now() + Math.random() * 1000;
  2440. //console.log('最终检查: 设置新的scrollTop值');
  2441. // 多次尝试原生滚动
  2442. this.nativeScrollToBottom();
  2443. setTimeout(() => {
  2444. this.nativeScrollToBottom();
  2445. }, 100);
  2446. },
  2447. // 使用原生API强制滚动(最终备用方案)
  2448. nativeScrollToBottom() {
  2449. try {
  2450. // 方案1:使用uni.createSelectorQuery直接操作DOM
  2451. const query = uni.createSelectorQuery().in(this);
  2452. query.select('#msgScroll').node((res) => {
  2453. if (res && res.node) {
  2454. // console.log('找到msgScroll节点,执行原生滚动');
  2455. const scrollHeight = res.node.scrollHeight;
  2456. res.node.scrollTop = scrollHeight;
  2457. // 确保滚动生效,延迟再次设置
  2458. setTimeout(() => {
  2459. res.node.scrollTop = scrollHeight + 100;
  2460. }, 50);
  2461. }
  2462. }).exec();
  2463. } catch (error) {
  2464. console.error('原生滚动失败:', error);
  2465. }
  2466. },
  2467. // 监听滚动事件
  2468. onScroll(e) {
  2469. this.currentScrollTop = e.detail.scrollTop;
  2470. // console.log('当前滚动位置:', this.currentScrollTop);
  2471. },
  2472. // 停止内存监控
  2473. stopMemoryMonitor() {
  2474. if (this.memoryMonitorTimer) {
  2475. clearInterval(this.memoryMonitorTimer);
  2476. this.memoryMonitorTimer = null;
  2477. }
  2478. },
  2479. // 获取活跃定时器数量
  2480. getActiveTimerCount() {
  2481. const timers = [
  2482. 'trafficTimer', 'pingTimeoutTimer', 'heartBeatTimer', 'liveViewDataTimer',
  2483. 'reconnectTimer', 'scrollTimer', 'searchTimer', 'purchasePromptTimer',
  2484. 'welcomeTimer', 'redTimer', 'liveStartTimer', 'lotteryTimer',
  2485. 'memoryMonitorTimer', 'networkStatusTimer', 'networkRetryTimer', 'generatingTimer'
  2486. ];
  2487. return timers.filter(timer => this[timer] !== null).length;
  2488. },
  2489. // 清理未使用的定时器
  2490. cleanupUnusedTimers() {
  2491. // 检查并清理可能泄漏的定时器
  2492. const timers = [
  2493. 'trafficTimer', 'pingTimeoutTimer', 'heartBeatTimer', 'liveViewDataTimer',
  2494. 'reconnectTimer', 'scrollTimer', 'searchTimer', 'purchasePromptTimer',
  2495. 'welcomeTimer', 'redTimer', 'liveStartTimer', 'lotteryTimer', 'noticeTimer',
  2496. 'networkStatusTimer', 'networkRetryTimer', 'generatingTimer'
  2497. ];
  2498. timers.forEach(timerName => {
  2499. if (this[timerName] && typeof this[timerName] === 'number') {
  2500. // 检查定时器是否还在运行
  2501. if (timerName.includes('Interval')) {
  2502. clearInterval(this[timerName]);
  2503. } else {
  2504. clearTimeout(this[timerName]);
  2505. }
  2506. this[timerName] = null;
  2507. console.log(`清理了可能泄漏的定时器: ${timerName}`);
  2508. }
  2509. });
  2510. },
  2511. // 增强的定时器清理方法
  2512. clearAllTimersEnhanced() {
  2513. // 先调用原有的清理方法
  2514. this.clearAllTimers();
  2515. // 额外清理新增的定时器
  2516. if (this.networkRetryTimer) {
  2517. clearTimeout(this.networkRetryTimer);
  2518. this.networkRetryTimer = null;
  2519. }
  2520. // 停止内存监控
  2521. this.stopMemoryMonitor();
  2522. // 清理缓存数据
  2523. this.userRandomColors = Object.create(null);
  2524. this.shownEntryUsers.clear();
  2525. // 重置性能监控数据
  2526. this.messageCount = 0;
  2527. this.errorCount = 0;
  2528. this.connectionLatency = 0;
  2529. this.lastPerformanceCheck = 0;
  2530. console.log('增强清理完成:所有定时器和缓存已清理');
  2531. },
  2532. goMiniProgram() {
  2533. uni.navigateTo({
  2534. url: '/pages_user/user/integralGoodsList'
  2535. })
  2536. },
  2537. // 阻止双击事件
  2538. preventDoubleClick(e) {
  2539. e.preventDefault();
  2540. e.stopPropagation();
  2541. return false;
  2542. },
  2543. // 清理所有定时器和内存
  2544. clearAllTimers() {
  2545. // 清理所有定时器
  2546. const timers = [
  2547. 'scrollTimer',
  2548. 'liveViewDataTimer',
  2549. 'redTimer',
  2550. 'liveStartTimer',
  2551. 'lotteryTimer',
  2552. 'noticeTimer',
  2553. 'welcomeTimer',
  2554. 'trafficInterval',
  2555. 'lookTimer',
  2556. 'trafficTimer',
  2557. 'intervalId',
  2558. 'reconnectTimer',
  2559. 'searchTimer',
  2560. 'purchasePromptTimer',
  2561. 'heartBeatTimer',
  2562. 'pingTimeoutTimer',
  2563. 'generatingTimer'
  2564. ];
  2565. timers.forEach((timer) => {
  2566. if (this[timer]) {
  2567. if (timer.includes('Interval') || timer.includes('Timer')) {
  2568. clearInterval(this[timer]);
  2569. } else {
  2570. clearTimeout(this[timer]);
  2571. }
  2572. this[timer] = null;
  2573. }
  2574. });
  2575. // 重置相关状态
  2576. this.stayTime = 0;
  2577. this.startTime = 0;
  2578. this.totalTraffic = 0;
  2579. this.scrollPending = false;
  2580. this.reconnectCount = 0;
  2581. this.heartBeatRetryCount = 0;
  2582. },
  2583. //直播计算流量
  2584. startTrafficCalculation() {
  2585. if (this.trafficTimer) {
  2586. clearInterval(this.trafficTimer);
  2587. this.trafficTimer = null;
  2588. }
  2589. this.startTime = Date.now();
  2590. var that = this;
  2591. // 计算码率
  2592. let bitrate = this.calculateBitrate();
  2593. this.trafficTimer = setInterval(() => {
  2594. that.calculateTraffic(bitrate);
  2595. }, 10000); // 每10秒计算一次
  2596. },
  2597. // 计算流量
  2598. // calculateTraffic(bitrate) {
  2599. // const currentTime = Date.now();
  2600. // const duration = (currentTime - this.startTime) / 1000; // 持续时间(秒)
  2601. // // 流量 = 码率 × 时间
  2602. // // 码率单位: bps, 时间单位: 秒, 流量单位: 比特
  2603. // const trafficBits = bitrate * duration;
  2604. // // 转换为字节
  2605. // this.totalTraffic = trafficBits / 8;
  2606. // this.getLiveInternetTraffic();
  2607. // },
  2608. calculateBitrate() {
  2609. // 如果接口返回了视频文件大小和时长,使用这些数据计算码率
  2610. if (this.liveItem.videoFileSize && this.liveItem.videoDuration) {
  2611. // 码率 = 文件大小(字节) / 时长(秒) × 8 (转换为bps) × 5
  2612. const calculatedBitrate = (this.liveItem.videoFileSize / this.liveItem.videoDuration) * 8 * 5;
  2613. console.log(
  2614. `使用接口数据计算码率: ${calculatedBitrate} bps (文件大小: ${this.liveItem.videoFileSize} 字节, 时长: ${this.liveItem.videoDuration} 秒)`
  2615. );
  2616. return calculatedBitrate;
  2617. } else {
  2618. // 如果任一字段为空,使用默认码率 1500 bps
  2619. console.log('接口数据不完整,使用默认码率: 1500 bps');
  2620. return 1500;
  2621. }
  2622. },
  2623. calculateTraffic(bitrate) {
  2624. const currentTime = Date.now();
  2625. const duration = (currentTime - this.startTime) / 1000; // 持续时间(秒)
  2626. // 流量 = 码率 × 时间
  2627. // 码率单位: bps, 时间单位: 秒, 流量单位: 比特
  2628. const trafficBits = bitrate * duration;
  2629. // 转换为字节
  2630. this.totalTraffic = trafficBits / 8;
  2631. // 调用流量上报接口
  2632. this.getLiveInternetTraffic();
  2633. },
  2634. startTimer() {
  2635. this.startTime = Date.now();
  2636. this.lookTimer = setInterval(() => {
  2637. this.stayTime = Math.floor((Date.now() - this.startTime) / 1000);
  2638. }, 1000);
  2639. },
  2640. //直播、录播缓冲
  2641. getLiveInternetTraffic() {
  2642. if (!this.liveId) return;
  2643. const currentTime = (this.stayTime / this.liveItem.duration) * 100;
  2644. const param = {
  2645. userId: this.userInfo.userId || '',
  2646. liveId: this.liveId || '',
  2647. uuId: dayjs().format('YYYYMMDD') + this.uuId,
  2648. internetTraffic: this.totalTraffic
  2649. };
  2650. liveInternetTraffic(param);
  2651. },
  2652. // 回放、预告缓冲
  2653. getInternetTraffic() {
  2654. if (!this.liveId || !this.liveId || !this.userInfo.userId || !this.uuId) return;
  2655. const currentTime = (this.stayTime / this.liveItem.duration) * 100;
  2656. const param = {
  2657. videoType: this.liveItem.videoType,
  2658. videoId: this.liveItem.videoId,
  2659. userId: this.userInfo.userId,
  2660. liveId: this.liveId,
  2661. uuId: dayjs().format('YYYYMMDD') + this.uuId,
  2662. duration: this.liveItem.duration,
  2663. bufferRate: currentTime
  2664. };
  2665. if (this.liveItem.status == 1) {
  2666. param.videoType = this.liveItem.previewVideoType || '';
  2667. param.videoId = this.liveItem.previewVideoId || '';
  2668. }
  2669. if (this.liveItem.liveType == 1) {
  2670. param.bufferRate = this.totalTraffic;
  2671. }
  2672. internetTraffic(param);
  2673. },
  2674. scrollToBottom() {
  2675. const now = Date.now();
  2676. // 防抖检查:如果距离上次滚动时间太短,则忽略
  2677. if (now - this.lastScrollTime < this.scrollDebounceDelay) {
  2678. return;
  2679. }
  2680. this.lastScrollTime = now;
  2681. // 清理之前的定时器
  2682. if (this.scrollTimer) {
  2683. clearTimeout(this.scrollTimer);
  2684. this.scrollTimer = null;
  2685. }
  2686. // 直接设置滚动位置
  2687. this.scrollTop = 999999999;
  2688. },
  2689. // 启动内存监控
  2690. startMemoryMonitor() {
  2691. // 清理之前的监控
  2692. if (this.memoryMonitorTimer) {
  2693. clearInterval(this.memoryMonitorTimer);
  2694. }
  2695. // 每5分钟检查一次内存使用情况
  2696. this.memoryMonitorTimer = setInterval(() => {
  2697. this.checkAndCleanMemory();
  2698. }, 5 * 60 * 1000); // 5分钟
  2699. },
  2700. // 检查并清理内存
  2701. checkAndCleanMemory() {
  2702. try {
  2703. // 清理过多的聊天消息
  2704. if (this.talklist && this.talklist.length > 25) {
  2705. const keepCount = 20;
  2706. this.talklist.splice(0, this.talklist.length - keepCount);
  2707. }
  2708. // 清理用户颜色缓存(保留最近的50个)
  2709. if (this.userRandomColors && Object.keys(this.userRandomColors).length > 50) {
  2710. const entries = Object.entries(this.userRandomColors);
  2711. const keepEntries = entries.slice(-50);
  2712. this.userRandomColors = Object.fromEntries(keepEntries);
  2713. }
  2714. // 清理已显示用户集合(保留最近的100个)
  2715. if (this.shownEntryUsers && this.shownEntryUsers.size > 100) {
  2716. const array = Array.from(this.shownEntryUsers);
  2717. this.shownEntryUsers.clear();
  2718. array.slice(-100).forEach((id) => this.shownEntryUsers.add(id));
  2719. }
  2720. console.log('内存清理完成');
  2721. } catch (error) {
  2722. console.error('内存清理失败:', error);
  2723. }
  2724. },
  2725. // 恢复页面活动
  2726. async resumePageActivity() {
  2727. if (this.liveItem) {
  2728. await this.getliving(this.liveId);
  2729. this.startTimeTimer(this.liveItem);
  2730. }
  2731. if (!this.isSocketAvailable()) {
  2732. this.initSocket();
  2733. }
  2734. },
  2735. // 获取用户专属随机色(缓存机制:同一用户始终用同一颜色)
  2736. getUserRandomColor(userId) {
  2737. if (!userId) {
  2738. return '#8978e2'; // 默认颜色
  2739. }
  2740. //如果缓存中已有该用户的颜色,直接返回
  2741. if (this.userRandomColors[userId]) {
  2742. return this.userRandomColors[userId];
  2743. }
  2744. //为新用户生成固定颜色(基于用户ID生成,不是完全随机)
  2745. const color = this.generateStableColor(userId);
  2746. this.userRandomColors[userId] = color;
  2747. // 存储到本地缓存,确保页面刷新后颜色不变
  2748. this.saveUserColorsToStorage();
  2749. return color;
  2750. }, // 基于用户ID生成稳定颜色(不是完全随机)
  2751. generateStableColor(userId) {
  2752. // 将用户ID转换为数字种子
  2753. let seed = 0;
  2754. for (let i = 0; i < userId.length; i++) {
  2755. seed = (seed * 31 + userId.charCodeAt(i)) % 1000000;
  2756. }
  2757. const colorPool = [
  2758. '#FF6B6B',
  2759. '#4ECDC4',
  2760. '#45B7D1',
  2761. '#96CEB4',
  2762. '#FFEAA7',
  2763. '#DDA0DD',
  2764. '#98D8C8',
  2765. '#F7DC6F',
  2766. '#BB8FCE',
  2767. '#85C1E9',
  2768. '#F8C471',
  2769. '#82E0AA',
  2770. '#F1948A',
  2771. '#85C1E9',
  2772. '#D7BDE2'
  2773. ];
  2774. return colorPool[seed % colorPool.length];
  2775. },
  2776. // 保存颜色映射到本地存储
  2777. saveUserColorsToStorage() {
  2778. try {
  2779. uni.setStorageSync('userRandomColors', this.userRandomColors);
  2780. } catch (e) {
  2781. console.warn('保存用户颜色缓存失败:', e);
  2782. }
  2783. },
  2784. // 从本地存储加载颜色映射
  2785. loadUserColorsFromStorage() {
  2786. try {
  2787. const cached = uni.getStorageSync('userRandomColors');
  2788. if (cached) {
  2789. this.userRandomColors = cached;
  2790. }
  2791. } catch (e) {
  2792. console.warn('加载用户颜色缓存失败:', e);
  2793. }
  2794. },
  2795. //头像名字
  2796. getNicknameInitial(nickName) {
  2797. if (!nickName || typeof nickName !== 'string') return '未';
  2798. if (/^[\u4e00-\u9fa5]/.test(nickName[0])) {
  2799. return nickName[0];
  2800. }
  2801. return nickName[0].toUpperCase();
  2802. },
  2803. async getUserInfo() {
  2804. await getUserInfo().then(
  2805. (res) => {
  2806. if (res.code == 200) {
  2807. this.userInfo = res.user;
  2808. this.isNow = dayjs(this.userInfo.updateTime).isSame(dayjs(), 'day')
  2809. } else {
  2810. uni.showToast({
  2811. icon: 'none',
  2812. title: '请求失败'
  2813. });
  2814. }
  2815. },
  2816. (rej) => {}
  2817. );
  2818. },
  2819. //订阅消息
  2820. handleAgreement() {
  2821. const templateId = this.templateId;
  2822. uni.requestSubscribeMessage({
  2823. tmplIds: [templateId],
  2824. success: (res) => {
  2825. if (res[templateId] === 'accept') {
  2826. uni.showToast({
  2827. title: '订阅成功,开播将提醒您',
  2828. icon: 'success'
  2829. });
  2830. this.callSendMessageApi();
  2831. } else if (res[templateId] === 'reject') {
  2832. uni.showToast({
  2833. title: '您已拒绝订阅,将无法收到提醒',
  2834. icon: 'none'
  2835. });
  2836. } else if (res[templateId] === 'ban') {
  2837. uni.showToast({
  2838. title: '您已关闭所有订阅权限,请在设置中开启',
  2839. icon: 'none'
  2840. });
  2841. }
  2842. },
  2843. fail: (err) => {
  2844. console.error('订阅消息失败', err);
  2845. uni.showToast({
  2846. title: '订阅失败,请重试',
  2847. icon: 'none'
  2848. });
  2849. }
  2850. });
  2851. },
  2852. async callSendMessageApi() {
  2853. if (!this.userInfo.userId) return;
  2854. const templateData = {
  2855. liveId: this.liveId,
  2856. userId: this.userInfo.userId,
  2857. templateId: this.templateId, // 模板ID
  2858. maOpenId: this.userInfo.maOpenId,
  2859. appId: this.appid,
  2860. data: {
  2861. thing6: this.liveItem.liveName,
  2862. date7: this.liveItem.startTime
  2863. }
  2864. };
  2865. subNotifyLive(templateData).then(
  2866. (res) => {
  2867. if (res.code == 200) {
  2868. this.isAgreement = true;
  2869. uni.setStorageSync('isAgreement', true);
  2870. } else {
  2871. uni.showToast({
  2872. title: res.msg,
  2873. icon: 'none'
  2874. });
  2875. }
  2876. },
  2877. (rej) => {}
  2878. );
  2879. },
  2880. //发送心跳
  2881. sendHeartBeat() {
  2882. if (!this.isSocketAvailable() || !this.isNetworkAvailable) {
  2883. console.warn('网络不可用或Socket连接异常,跳过心跳发送');
  2884. return;
  2885. }
  2886. this.lastHeartBeatTime = Date.now();
  2887. // 计算当前累计观看时长
  2888. const currentWatchDuration = this.getCurrentWatchDuration();
  2889. try {
  2890. const heartBeatMsg = JSON.stringify({
  2891. cmd: 'heartbeat',
  2892. msg: 'ping',
  2893. userId: this.userInfo.userId || '',
  2894. liveId: this.liveId,
  2895. timestamp: this.lastHeartBeatTime,
  2896. networkType: this.networkType,
  2897. data: String(currentWatchDuration)
  2898. });
  2899. this.socket.send({
  2900. data: heartBeatMsg,
  2901. success: () => {
  2902. this.heartBeatRetryCount = 0; // 成功后重置重试次数
  2903. this.adjustHeartBeatInterval(true); // 网络良好,可适当延长间隔
  2904. this.startPingTimeout(); // 启动超时检测
  2905. },
  2906. fail: (err) => {
  2907. console.error('心跳包发送失败:', err);
  2908. this.heartBeatRetryCount++;
  2909. this.adjustHeartBeatInterval(false); // 网络不稳定,缩短间隔
  2910. // 根据网络类型调整重试策略
  2911. const retryDelay = this.getRetryDelay();
  2912. if (this.heartBeatRetryCount < this.maxHeartBeatRetries) {
  2913. setTimeout(() => this.sendHeartBeat(), retryDelay);
  2914. } else {
  2915. this.heartBeatRetryCount = 0;
  2916. this.handleReconnect(); // 重试用完才重连
  2917. }
  2918. }
  2919. });
  2920. } catch (err) {
  2921. console.error('心跳发送异常:', err);
  2922. this.heartBeatRetryCount++;
  2923. const retryDelay = this.getRetryDelay();
  2924. if (this.heartBeatRetryCount < this.maxHeartBeatRetries) {
  2925. setTimeout(() => this.sendHeartBeat(), retryDelay);
  2926. } else {
  2927. this.heartBeatRetryCount = 0;
  2928. this.handleReconnect();
  2929. }
  2930. }
  2931. },
  2932. // 根据网络状态获取重试延迟
  2933. getRetryDelay() {
  2934. const baseDelay = 2000;
  2935. const retryMultiplier = Math.pow(1.5, this.heartBeatRetryCount); // 指数退避
  2936. // 根据网络类型调整延迟
  2937. let networkMultiplier = 1;
  2938. switch (this.networkType) {
  2939. case '2g':
  2940. networkMultiplier = 3;
  2941. break;
  2942. case '3g':
  2943. networkMultiplier = 2;
  2944. break;
  2945. case '4g':
  2946. case '5g':
  2947. networkMultiplier = 1;
  2948. break;
  2949. case 'wifi':
  2950. networkMultiplier = 0.8;
  2951. break;
  2952. default:
  2953. networkMultiplier = 1.5;
  2954. }
  2955. return Math.min(baseDelay * retryMultiplier * networkMultiplier, 10000); // 最大10秒
  2956. },
  2957. // 自适应调整心跳间隔
  2958. adjustHeartBeatInterval(isSuccess) {
  2959. if (isSuccess) {
  2960. // 连接稳定,可适当延长间隔(最大30秒)
  2961. this.adaptiveHeartBeatInterval = Math.min(this.adaptiveHeartBeatInterval * 1.1, 30000);
  2962. } else {
  2963. // 连接不稳定,缩短间隔(最小10秒)
  2964. this.adaptiveHeartBeatInterval = Math.max(this.adaptiveHeartBeatInterval * 0.9, 10000);
  2965. }
  2966. },
  2967. // 启动心跳超时检测
  2968. startPingTimeout() {
  2969. // 清除现有的ping超时定时器
  2970. if (this.pingTimeoutTimer) {
  2971. clearTimeout(this.pingTimeoutTimer);
  2972. this.pingTimeoutTimer = null;
  2973. }
  2974. // 启动新的ping超时定时器
  2975. this.pingTimeoutTimer = setTimeout(() => {
  2976. console.warn('心跳超时,触发重连');
  2977. // 清理当前定时器引用
  2978. this.pingTimeoutTimer = null;
  2979. // 增加心跳重试次数
  2980. this.heartBeatRetryCount++;
  2981. // 如果重试次数未达到上限,先尝试重发心跳
  2982. if (this.heartBeatRetryCount < this.maxHeartBeatRetries) {
  2983. console.log(`心跳超时,尝试重发 (${this.heartBeatRetryCount}/${this.maxHeartBeatRetries})`);
  2984. setTimeout(() => this.sendHeartBeat(), 1000);
  2985. } else {
  2986. console.log('心跳重试次数用尽,触发重连');
  2987. this.heartBeatRetryCount = 0;
  2988. this.handleReconnect();
  2989. }
  2990. }, this.pingTimeout);
  2991. },
  2992. stopHeartBeat() {
  2993. if (this.heartBeatTimer) {
  2994. clearInterval(this.heartBeatTimer);
  2995. this.heartBeatTimer = null;
  2996. }
  2997. if (this.pingTimeoutTimer) {
  2998. clearTimeout(this.pingTimeoutTimer);
  2999. this.pingTimeoutTimer = null;
  3000. }
  3001. }, // 封装通用的连接状态校验方法
  3002. isSocketAvailable() {
  3003. // WebSocket readyState:0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
  3004. return this.socket && this.isSocketOpen && this.socket.readyState === 1;
  3005. },
  3006. // 处理重连逻辑
  3007. handleReconnect() {
  3008. if (this.isManualClose) {
  3009. console.log('手动关闭连接,不进行重连');
  3010. return;
  3011. }
  3012. // 防止重复重连
  3013. if (this.reconnectTimer) {
  3014. console.log('重连已在进行中,跳过重复重连');
  3015. return;
  3016. }
  3017. this.stopHeartBeat();
  3018. // 检查网络状态
  3019. if (!this.isNetworkAvailable) {
  3020. console.warn('网络不可用,延迟重连');
  3021. this.reconnectTimer = setTimeout(() => {
  3022. this.reconnectTimer = null;
  3023. this.handleReconnect();
  3024. }, 5000);
  3025. return;
  3026. }
  3027. if (this.reconnectCount < this.maxReconnectAttempts) {
  3028. this.reconnectCount++;
  3029. // 指数退避算法:基础延迟 * 2^(重连次数-1) + 随机抖动
  3030. const baseDelay = 1000;
  3031. const exponentialDelay = baseDelay * Math.pow(2, this.reconnectCount - 1);
  3032. const jitter = Math.random() * 1000; // 随机抖动,避免同时重连
  3033. const totalDelay = Math.min(exponentialDelay + jitter, 30000); // 最大30秒
  3034. console.log(`第${this.reconnectCount}次重连,延迟${Math.round(totalDelay)}ms,网络类型:${this.networkType}`);
  3035. this.reconnectTimer = setTimeout(() => {
  3036. this.reconnectTimer = null; // 清理定时器引用
  3037. // 重连前再次检查网络状态和连接状态
  3038. if (this.isNetworkAvailable && !this.isManualClose && !this.isSocketAvailable()) {
  3039. console.log('开始执行重连...');
  3040. this.initSocket();
  3041. } else if (this.isSocketAvailable()) {
  3042. console.log('连接已恢复,取消重连');
  3043. this.reconnectCount = 0; // 重置重连计数
  3044. } else {
  3045. console.warn('重连时网络不可用或已手动关闭');
  3046. this.handleReconnect(); // 递归重试
  3047. }
  3048. }, totalDelay);
  3049. } else {
  3050. console.log(`已达最大重连次数(${this.maxReconnectAttempts}),停止重连`);
  3051. this.showReconnectFailedMessage();
  3052. }
  3053. },
  3054. // 显示重连失败提示
  3055. showReconnectFailedMessage() {
  3056. uni.showToast({
  3057. title: '网络连接异常,请检查网络后重新进入',
  3058. icon: 'none',
  3059. duration: 3000
  3060. });
  3061. },
  3062. // 统一处理连接错误
  3063. handleConnectionError(errorType, error) {
  3064. console.error(`WebSocket ${errorType}:`, error);
  3065. // 根据错误类型进行不同处理
  3066. if (errorType === '连接请求失败') {
  3067. // 连接请求失败,可能是网络问题或服务器问题
  3068. if (!this.isNetworkAvailable) {
  3069. console.warn('网络不可用,等待网络恢复后重连');
  3070. return;
  3071. }
  3072. }
  3073. // 触发重连
  3074. this.handleReconnect();
  3075. },
  3076. // 重置重连状态
  3077. resetReconnectState() {
  3078. this.reconnectCount = 0;
  3079. this.heartBeatRetryCount = 0;
  3080. // 清理重连定时器
  3081. if (this.reconnectTimer) {
  3082. clearTimeout(this.reconnectTimer);
  3083. this.reconnectTimer = null;
  3084. }
  3085. // 重置自适应心跳间隔
  3086. this.adaptiveHeartBeatInterval = this.heartBeatInterval;
  3087. },
  3088. // 初始化网络状态监听
  3089. initNetworkStatusListener() {
  3090. // 获取当前网络状态
  3091. uni.getNetworkType({
  3092. success: (res) => {
  3093. this.networkType = res.networkType;
  3094. this.isNetworkAvailable = res.networkType !== 'none';
  3095. console.log('当前网络类型:', res.networkType);
  3096. }
  3097. });
  3098. // 监听网络状态变化
  3099. uni.onNetworkStatusChange((res) => {
  3100. const oldNetworkType = this.networkType;
  3101. const oldNetworkAvailable = this.isNetworkAvailable;
  3102. this.networkType = res.networkType;
  3103. this.isNetworkAvailable = res.isConnected;
  3104. console.log(`网络状态变化: ${oldNetworkType} -> ${res.networkType}, 连接状态: ${res.isConnected}`);
  3105. // 网络从不可用变为可用时,尝试重连
  3106. if (!oldNetworkAvailable && res.isConnected) {
  3107. console.log('网络恢复,尝试重连WebSocket');
  3108. this.resetReconnectState();
  3109. setTimeout(() => {
  3110. if (!this.isSocketAvailable()) {
  3111. this.initSocket();
  3112. }
  3113. }, 1000);
  3114. }
  3115. // 网络类型变化时,调整心跳策略
  3116. if (oldNetworkType !== res.networkType && res.isConnected) {
  3117. this.adjustHeartBeatForNetworkType(res.networkType);
  3118. }
  3119. // 网络断开时停止心跳
  3120. if (!res.isConnected) {
  3121. console.warn('网络断开,停止心跳');
  3122. this.stopHeartBeat();
  3123. }
  3124. });
  3125. },
  3126. // 根据网络类型调整心跳策略
  3127. adjustHeartBeatForNetworkType(networkType) {
  3128. let newInterval = this.heartBeatInterval;
  3129. switch (networkType) {
  3130. case '2g':
  3131. newInterval = 30000; // 2G网络,30秒间隔
  3132. break;
  3133. case '3g':
  3134. newInterval = 20000; // 3G网络,20秒间隔
  3135. break;
  3136. case '4g':
  3137. case '5g':
  3138. newInterval = 15000; // 4G/5G网络,15秒间隔
  3139. break;
  3140. case 'wifi':
  3141. newInterval = 10000; // WiFi网络,10秒间隔
  3142. break;
  3143. default:
  3144. newInterval = 15000; // 默认15秒
  3145. }
  3146. if (newInterval !== this.adaptiveHeartBeatInterval) {
  3147. this.adaptiveHeartBeatInterval = newInterval;
  3148. console.log(`根据网络类型(${networkType})调整心跳间隔为${newInterval}ms`);
  3149. // 如果当前有心跳在运行,重启心跳以应用新间隔
  3150. if (this.heartBeatTimer && this.isSocketAvailable()) {
  3151. this.startHeartBeat();
  3152. }
  3153. }
  3154. },
  3155. //输入框
  3156. inputFocus() {
  3157. // 立即更新状态,确保UI零延迟响应
  3158. this.isFocus = true;
  3159. this.isKeyboardShow = true;
  3160. },
  3161. inputBlur() {
  3162. this.isFocus = false;
  3163. },
  3164. getTimeDifferenceInSeconds(createTimeStr) {
  3165. if (!createTimeStr) return;
  3166. const createTime = new Date(createTimeStr.replace(/-/g, '/'));
  3167. const now = new Date();
  3168. const timeDiffMs = now - createTime;
  3169. const timeDiffSeconds = Math.floor(timeDiffMs / 1000);
  3170. return Math.max(0, timeDiffSeconds);
  3171. },
  3172. // 录播时间点
  3173. onVideoMetaLoaded(e) {
  3174. console.log("录播时间点", e)
  3175. const width = e.detail.width;
  3176. const height = e.detail.height;
  3177. const res = uni.getSystemInfoSync();
  3178. const winH = res.windowHeight;
  3179. const winW = res.windowWidth;
  3180. //横屏
  3181. const wW = (winW / winH) * 100;
  3182. //竖屏
  3183. const wH = (winH / winW) * 100;
  3184. this.videoProgressKey = `videoProgress_${this.liveId}`;
  3185. this.setVideoProgress();
  3186. },
  3187. setVideoProgress() {
  3188. // 只有录播和回放需要设置进度
  3189. if (this.liveItem.liveType !== 2 && this.liveItem.liveType !== 3) {
  3190. return;
  3191. }
  3192. let currentTime = 0;
  3193. if (this.liveItem.liveType === 2) {
  3194. // 录播:计算当前时间与开始时间的差值,对视频总时长取模
  3195. const diff = this.getTimeDifferenceInSeconds(this.liveItem.startTime);
  3196. if (diff > this.liveItem.duration) {
  3197. const storedProgress = uni.getStorageSync(this.videoProgressKey) || 0;
  3198. currentTime = storedProgress >= this.liveItem.duration ? 0 : storedProgress || 0;
  3199. this.isPlayback = true;
  3200. this.hasPlayback = true;
  3201. } else {
  3202. currentTime = diff % this.liveItem.duration;
  3203. }
  3204. } else if (this.liveItem.liveType === 3) {
  3205. // 回放:从存储中获取进度
  3206. const storedProgress = uni.getStorageSync(this.videoProgressKey);
  3207. currentTime = storedProgress || 0;
  3208. }
  3209. const videoId = `myVideo_${this.liveId}`;
  3210. const videoContext = uni.createVideoContext(videoId, this);
  3211. if (videoContext) {
  3212. videoContext.seek(currentTime);
  3213. }
  3214. },
  3215. // onVideoWaiting(e) {
  3216. // // console.log('视频等待加载', e);
  3217. // if (this.liveItem.liveType == 2) {
  3218. // this.startTrafficCalculation(this.bitrate);
  3219. // } else {
  3220. // let that = this;
  3221. // if (this.trafficInterval) {
  3222. // clearInterval(this.trafficInterval);
  3223. // this.trafficInterval = null;
  3224. // }
  3225. // this.trafficInterval = setInterval(function() {
  3226. // that.getInternetTraffic();
  3227. // }, 10000);
  3228. // }
  3229. // },
  3230. onVideoWaiting(e) {
  3231. // console.log('视频等待加载', e);
  3232. if (this.liveItem.liveType == 2) {
  3233. // 修改这里:不再传入固定码率,而是在方法内部计算
  3234. this.startTrafficCalculation();
  3235. } else {
  3236. let that = this;
  3237. if (this.trafficInterval) {
  3238. clearInterval(this.trafficInterval);
  3239. this.trafficInterval = null;
  3240. }
  3241. this.trafficInterval = setInterval(function() {
  3242. that.getInternetTraffic();
  3243. }, 10000);
  3244. }
  3245. },
  3246. // 视频时间更新
  3247. onVideoTimeUpdate(e) {
  3248. // 获取当前播放时间
  3249. this.videoCurrentTime = e.detail.currentTime;
  3250. // 每隔10秒保存一次进度(避免频繁存储)
  3251. if (Math.floor(this.videoCurrentTime) % 10 === 0) {
  3252. this.saveVideoProgress();
  3253. }
  3254. const diff = this.getTimeDifferenceInSeconds(this.liveItem.startTime)
  3255. if (diff >= this.liveItem.duration) {
  3256. if (!this.hasPlayback) {
  3257. this.generating = true;
  3258. this.pauseVideo();
  3259. setTimeout(() => {
  3260. this.playVideo();
  3261. this.generating = false;
  3262. this.hasPlayback = true;
  3263. }, 180000);
  3264. // this.closeWebSocket(true);
  3265. this.isEnd = true;
  3266. }
  3267. }
  3268. // LJX
  3269. // this.$nextTick(() => {
  3270. // const difPlayback = currentTime
  3271. // const totalSeconds = Math.floor(difPlayback / 1000);
  3272. // const hours = this.padZero(Math.floor(totalSeconds / 3600));
  3273. // const minutes = this.padZero(Math.floor((totalSeconds % 3600) / 60));
  3274. // const seconds = this.padZero(totalSeconds % 60);
  3275. // this.playbackTime = `${hours}:${minutes}:${seconds}`;
  3276. // console.log("这个时间", difPlayback)
  3277. // });
  3278. },
  3279. // 视频暂停
  3280. onVideoPause(e) {
  3281. this.stopCountdown();
  3282. if (this.liveItem.liveType === 2) {
  3283. const videoId = `myVideo_${this.liveId}`;
  3284. const videoContext = uni.createVideoContext(videoId, this);
  3285. setTimeout(() => {
  3286. videoContext.play();
  3287. }, 100);
  3288. }
  3289. // 暂停时保存进度
  3290. this.saveVideoProgress();
  3291. },
  3292. // 视频播放
  3293. onVideoPlay(e) {
  3294. this.startCountdown();
  3295. if (this.watchTimeTimer) {
  3296. clearInterval(this.watchTimeTimer);
  3297. this.watchTimeTimer = null;
  3298. this.saveWatchTime();
  3299. console.log("视频开始播放")
  3300. // 立即保存
  3301. }
  3302. },
  3303. // 设置视频当前时间
  3304. // setVideoCurrentTime(time) {
  3305. // if (!time) return;
  3306. // const videoId = `myVideo_${this.liveId}`;
  3307. // const videoContext = uni.createVideoContext(videoId, this);
  3308. // if (videoContext) {
  3309. // videoContext.seek(time);
  3310. // console.log(`设置视频播放位置: ${time}秒`);
  3311. // }
  3312. // },
  3313. // 保存视频进度
  3314. saveVideoProgress() {
  3315. if (this.videoProgressKey) {
  3316. uni.setStorage({
  3317. key: this.videoProgressKey,
  3318. data: this.videoCurrentTime,
  3319. success: () => {},
  3320. fail: (err) => {
  3321. console.error('保存视频进度失败:', err);
  3322. }
  3323. });
  3324. }
  3325. },
  3326. // 点击红包
  3327. onRed() {
  3328. if (!this.liveId) return;
  3329. if (!this.redInfo?.redId) return;
  3330. if (this.redTimer) {
  3331. clearInterval(this.redTimer);
  3332. this.redTimer = null;
  3333. }
  3334. let data = {
  3335. liveId: this.liveId,
  3336. userId: this.userInfo.userId,
  3337. redId: this.redInfo.redId
  3338. };
  3339. liveRed(data).then((res) => {
  3340. this.isShowRed = false;
  3341. this.redCard = res;
  3342. this.isShowRedCard = true;
  3343. },
  3344. (rej) => {}
  3345. );
  3346. },
  3347. //领取优惠券
  3348. onCoupon() {
  3349. if (!this.couponInfo.couponIssueId) return;
  3350. let data = {
  3351. goodsId: this.couponInfo.goodsId,
  3352. couponIssueId: this.couponInfo.couponIssueId,
  3353. liveId: this.liveId
  3354. };
  3355. coupon(data).then((res) => {
  3356. this.isShowCoupon = false;
  3357. if (res.code == 200) {
  3358. uni.showToast({
  3359. title: res.msg,
  3360. icon: 'none'
  3361. });
  3362. } else {
  3363. uni.showToast({
  3364. title: res.msg,
  3365. icon: 'none'
  3366. });
  3367. }
  3368. })
  3369. .catch((rej) => {});
  3370. },
  3371. // 我的中奖名单
  3372. getMyLottery() {
  3373. this.winning = true;
  3374. myLottery()
  3375. .then((res) => {
  3376. if (res.code == 200) {
  3377. // console.log("我的中奖名单", res)
  3378. this.prizeAll = res.data.list || {};
  3379. } else {}
  3380. })
  3381. .catch((rej) => {});
  3382. },
  3383. // 抽奖
  3384. onLottery() {
  3385. if (!this.lotteryInfo) return;
  3386. this.isShowLottery = false
  3387. let data = {
  3388. lotteryId: this.lotteryInfo.lotteryId
  3389. };
  3390. liveLottery(data)
  3391. .then((res) => {
  3392. if (res.code == 200) {
  3393. const resData = res.data || {};
  3394. this.lotteryList = Array.isArray(resData) ? resData : [];
  3395. this.lotteryProducts = Array.isArray(resData.products) ? resData.products : [];
  3396. if (resData.duration) {
  3397. this.isShowLotteryPop = true;
  3398. }
  3399. } else {
  3400. uni.showToast({
  3401. title: res.msg,
  3402. icon: 'none'
  3403. });
  3404. this.lotteryList = [];
  3405. this.lotteryProducts = [];
  3406. }
  3407. })
  3408. .catch((rej) => {
  3409. uni.showToast({
  3410. title: '获取抽奖信息失败',
  3411. icon: 'none'
  3412. });
  3413. // 失败时强制重置为数组
  3414. this.lotteryList = [];
  3415. this.lotteryProducts = [];
  3416. });
  3417. },
  3418. // 参与抽奖
  3419. onClaim() {
  3420. let data = {
  3421. liveId: this.liveId,
  3422. lotteryId: this.lotteryInfo.lotteryId
  3423. };
  3424. claim(data).then(
  3425. (res) => {
  3426. if (res.code == 200) {
  3427. uni.showToast({
  3428. title: res.msg,
  3429. icon: 'none'
  3430. });
  3431. this.isShowLotteryPop = false;
  3432. this.havePrize = true;
  3433. uni.setStorageSync('havePrize', this.havePrize);
  3434. } else {
  3435. uni.showToast({
  3436. title: res.msg,
  3437. icon: 'none'
  3438. });
  3439. }
  3440. },
  3441. (rej) => {}
  3442. );
  3443. },
  3444. confirm() {
  3445. this.isShowPrize = false;
  3446. this.havePrize = false;
  3447. uni.setStorageSync('havePrize', this.havePrize);
  3448. },
  3449. // 商品收藏
  3450. onGoodsCollect(item) {
  3451. if (!item || item.length === 0 || !item.goodsId) {
  3452. return;
  3453. }
  3454. collectGoods(item.goodsId).then(
  3455. (res) => {
  3456. if (res.code == 200) {
  3457. uni.showToast({
  3458. title: res.msg,
  3459. icon: 'none'
  3460. });
  3461. item.isFavorite = !item.isFavorite;
  3462. } else {
  3463. uni.showToast({
  3464. title: res.msg,
  3465. icon: 'none'
  3466. });
  3467. }
  3468. },
  3469. (rej) => {}
  3470. );
  3471. },
  3472. //正在购买
  3473. getliveOrder(item) {
  3474. if (!this.liveId) {
  3475. return;
  3476. }
  3477. liveOrderUser(this.liveId).then(
  3478. (res) => {
  3479. if (res.code == 200) {
  3480. this.orderUser = res;
  3481. } else {
  3482. console.log('获取正在购买用户失败');
  3483. }
  3484. },
  3485. (rej) => {}
  3486. );
  3487. },
  3488. onLiveStateChange(e, liveItem) {
  3489. // 可以根据状态码处理不同的直播状态
  3490. const stateCode = e.detail.code;
  3491. if (e.detail.code == -2301 || e.detail.code == -2302) {
  3492. this.playVideo();
  3493. } else if (e.detail.code == 2004) {
  3494. this.calculateTimeDiff(this.liveItem);
  3495. // this.startTrafficCalculation(this.bitrateLive);
  3496. this.startTrafficCalculation();
  3497. }
  3498. // 2001: 已经连接服务器
  3499. // 2002: 已经连接服务器,开始拉流
  3500. // 2003: 网络接收到首个视频数据包(IDR)
  3501. // 2004: 视频播放开始
  3502. // 2005: 视频播放进度
  3503. // 2006: 视频播放结束
  3504. // 2007: 视频播放Loading
  3505. // 2008: 解码器启动
  3506. // -2301: 网络断连,且经多次重连抢救无效,更多重试请自行重启播放
  3507. // -2302: 获取加速拉流地址失败
  3508. }, // 直播错误事件
  3509. onLiveError(e, liveItem) {
  3510. this.videoError(e, liveItem);
  3511. console.log('错误');
  3512. },
  3513. // 红包 卡片 抽奖
  3514. getCurrentActivities() {
  3515. if (!this.liveId) return;
  3516. currentActivities(this.liveId).then(
  3517. (res) => {
  3518. if (res.code === 200) {
  3519. // 提取数据(默认空数组/对象避免报错)
  3520. this.redInfo = (Array.isArray(res.red) ? res.red : [])[0] || {};
  3521. this.lotteryInfo = (Array.isArray(res.lottery) ? res.lottery : [])[0] || {};
  3522. this.goodsCard = res.goods || {};
  3523. this.notice = res.topMsg || {};
  3524. this.isShowGoods = this.goodsCard && this.goodsCard.status == 1;
  3525. this.isShowRed = this.redInfo && this.redInfo.redStatus == 1;
  3526. this.isShowLottery = this.lotteryInfo && this.lotteryInfo.lotteryStatus == 1;
  3527. if (this.notice.msg) {
  3528. this.noticeTimer = setInterval(() => {
  3529. const noticeCountdown = this.handleTime(this.notice.endTime, 0);
  3530. if (!noticeCountdown) {
  3531. this.isShowNotice = false;
  3532. clearInterval(this.noticeTimer);
  3533. } else {
  3534. this.isShowNotice = true
  3535. }
  3536. }, 1000);
  3537. }
  3538. // if(this.notice.msg&&this.notice){
  3539. // this.isShowNotice = true
  3540. // }else{
  3541. // this.isShowNotice = false
  3542. // }
  3543. if (this.isShowRed) {
  3544. this.redTimer = setInterval(() => {
  3545. const redCountdown = this.handleTime(this.redInfo.updateTime, this.redInfo
  3546. .duration);
  3547. if (!redCountdown) {
  3548. this.isShowRed = false;
  3549. clearInterval(this.redTimer);
  3550. }
  3551. }, 1000);
  3552. }
  3553. // 处理抽奖定时器
  3554. if (this.isShowLottery) {
  3555. this.lotteryTimer = setInterval(() => {
  3556. this.countdown = this.handleTime(this.lotteryInfo.updateTime, this
  3557. .lotteryInfo.duration);
  3558. }, 1000);
  3559. }
  3560. } else {
  3561. uni.showToast({
  3562. title: res.msg,
  3563. icon: 'none'
  3564. });
  3565. }
  3566. },
  3567. (rej) => {}
  3568. );
  3569. },
  3570. // 计算当前时间与 liveItem.startTime 的差值,并更新 totalTime
  3571. calculateTimeDiff(item, type) {
  3572. if (!item.startTime) return;
  3573. // 提取对应场景的时间字符串(直播用startTime,抽奖用updateTime)
  3574. let timeStr = item.startTime;
  3575. // 转换时间格式(适配iOS,将 "-" 替换为 "/")
  3576. const time = new Date(timeStr.replace(/-/g, '/'));
  3577. if (isNaN(time.getTime())) {
  3578. return;
  3579. }
  3580. const now = new Date();
  3581. let diffMs = Math.max(0, now.getTime() - time.getTime());
  3582. // 转换为 时:分:秒(补零处理)
  3583. const totalSeconds = Math.floor(diffMs / 1000);
  3584. const hours = this.padZero(Math.floor(totalSeconds / 3600));
  3585. const minutes = this.padZero(Math.floor((totalSeconds % 3600) / 60));
  3586. const seconds = this.padZero(totalSeconds % 60);
  3587. this.diffTotalTime = `${hours}:${minutes}:${seconds}`;
  3588. if (type) {
  3589. return `${hours}:${minutes}:${seconds}`;
  3590. }
  3591. },
  3592. padZero(num) {
  3593. return num < 10 ? `0${num}` : num;
  3594. },
  3595. // 启动当前直播间的时间差值定时器
  3596. startTimeTimer(item) {
  3597. if (!item) return;
  3598. // 立即计算一次(避免等待1秒才显示)
  3599. const totalTime = this.calculateTimeDiff(item);
  3600. item.timeTimer = setInterval(() => {
  3601. const totalTime = this.calculateTimeDiff(item);
  3602. }, 1000);
  3603. },
  3604. // toggleViewerList() {
  3605. // const now = Date.now();
  3606. // if (now - this.lastClickTime > this.clickDelay) {
  3607. // this.showadd = !this.showadd;
  3608. // if (this.showadd) {
  3609. // this.getliveUser(false); // 加载第一页观众
  3610. // }
  3611. // this.lastClickTime = now;
  3612. // }
  3613. // },
  3614. // 播放视频
  3615. playVideo() {
  3616. if (!this.liveItem) {
  3617. console.log('liveItem 为空,无法播放视频');
  3618. return;
  3619. }
  3620. try {
  3621. if (this.liveItem.liveType === 1 && this.liveItem.livingUrl && this.liveItem.status == 2) {
  3622. } else if (this.liveItem.status == 1 && this.liveItem.previewUrl) {
  3623. const videoId = `myVideo_${this.liveId}`;
  3624. const videoContext = uni.createVideoContext(videoId, this);
  3625. if (videoContext) {
  3626. videoContext.play();
  3627. }
  3628. } else if (this.liveItem.liveType === 2 && this.liveItem.videoUrl && this.liveItem.status == 2) {
  3629. const videoId = `myVideo_${this.liveId}`;
  3630. const videoContext = uni.createVideoContext(videoId, this);
  3631. // console.log("录播")
  3632. if (videoContext) {
  3633. videoContext.play();
  3634. }
  3635. } // 回放视频使用video
  3636. else if (this.liveItem.liveType === 3 && this.liveItem.videoUrl && this.liveItem.status == 4) {
  3637. const videoId = `myVideo_${this.liveId}`;
  3638. const videoContext = uni.createVideoContext(videoId, this);
  3639. // console.log("回放")
  3640. if (videoContext) {
  3641. videoContext.play();
  3642. }
  3643. }
  3644. } catch (error) {
  3645. console.error('播放视频失败:', error);
  3646. }
  3647. },
  3648. pauseVideo() {
  3649. if (!this.liveItem) return;
  3650. try {
  3651. if (this.liveItem.status == 1) {
  3652. const videoId = `myVideo_${this.liveId}`;
  3653. const videoContext = uni.createVideoContext(videoId, this);
  3654. if (videoContext) {
  3655. videoContext.pause();
  3656. }
  3657. } else if (this.liveItem.status == 2) {
  3658. if (this.liveItem.liveType === 1) {
  3659. const livePlayerId = `myLivePlayer_${this.liveId}`;
  3660. const livePlayerContext = uni.createLivePlayerContext(livePlayerId, this);
  3661. if (livePlayerContext) {
  3662. livePlayerContext.pause();
  3663. }
  3664. } else if (this.liveItem.liveType === 2) {
  3665. const videoId = `myVideo_${this.liveId}`;
  3666. const videoContext = uni.createVideoContext(videoId, this);
  3667. if (videoContext) {
  3668. videoContext.pause();
  3669. }
  3670. }
  3671. }
  3672. } catch (error) {
  3673. console.error('暂停视频失败:', error);
  3674. }
  3675. },
  3676. // 视频错误处理
  3677. videoError(e, liveItem) {
  3678. if (!liveItem || !this.liveId) return;
  3679. // 初始化重试计数
  3680. if (this.videoRetryCounts[liveItem.liveId] === undefined) {
  3681. this.videoRetryCounts[liveItem.liveId] = 0;
  3682. }
  3683. // 限制重试次数
  3684. if (this.videoRetryCounts[liveItem.liveId] >= 3) {
  3685. console.error(`直播间 ${this.liveId} 视频加载失败,停止重试`);
  3686. // 显示错误提示
  3687. uni.showToast({
  3688. title: '视频加载失败,请检查网络',
  3689. icon: 'none',
  3690. duration: 2000
  3691. });
  3692. return;
  3693. }
  3694. this.videoRetryCounts[this.liveId]++;
  3695. // 延迟重试
  3696. setTimeout(() => {
  3697. if (this.liveId === this.liveId) {
  3698. console.log(`第${this.videoRetryCounts[this.liveId]}次重试播放视频`);
  3699. this.playVideo();
  3700. }
  3701. }, 2000);
  3702. },
  3703. openViews() {
  3704. // 计算scroll-view高度
  3705. this.$nextTick(() => {
  3706. const query = uni.createSelectorQuery().in(this);
  3707. query
  3708. .select('.view-box')
  3709. .boundingClientRect((data) => {
  3710. if (data) {
  3711. this.scrollHeight = data.height - 80; // 80是标题高度,120是底部高度
  3712. }
  3713. })
  3714. .exec();
  3715. });
  3716. },
  3717. // 滚动到底部触发
  3718. handleScrollToLower() {
  3719. // 清除上一次未执行的定时器,避免重复请求
  3720. if (this.scrollTimer) {
  3721. clearTimeout(this.scrollTimer);
  3722. }
  3723. // 延迟1秒执行接口请求
  3724. this.scrollTimer = setTimeout(() => {
  3725. this.getliveUser(true);
  3726. }, 1000);
  3727. },
  3728. async getMyLiveMsg(liveItem) {
  3729. console.log("不可见了")
  3730. if (!liveItem || !this.liveId) {
  3731. console.error('getLiveMsg 错误:无效的 liveItem');
  3732. return;
  3733. }
  3734. try {
  3735. const res = await myLiveMsg(this.liveId, 30, 1);
  3736. if (res.code == 200) {
  3737. const rows = Array.isArray(res.rows) ? res.rows : [];
  3738. const reversedTalkList = [...rows].reverse();
  3739. this.talklist = Array.isArray(reversedTalkList) ? reversedTalkList : [];
  3740. // 获取历史消息后也滚动到底部
  3741. this.$nextTick(() => {
  3742. this.scrollToBottom();
  3743. });
  3744. } else {
  3745. this.talklist = [];
  3746. }
  3747. } catch (error) {
  3748. this.talklist = [];
  3749. console.error('获取聊天记录失败:', error);
  3750. }
  3751. },
  3752. async getLiveMsg(liveItem) {
  3753. if (!liveItem || !this.liveId) {
  3754. console.error('getLiveMsg 错误:无效的 liveItem');
  3755. return;
  3756. }
  3757. try {
  3758. const res = await liveMsg(this.liveId, 30, 1);
  3759. if (res.code == 200) {
  3760. const rows = Array.isArray(res.rows) ? res.rows : [];
  3761. const reversedTalkList = [...rows].reverse();
  3762. this.talklist = Array.isArray(reversedTalkList) ? reversedTalkList : [];
  3763. // 获取历史消息后也滚动到底部
  3764. this.$nextTick(() => {
  3765. this.scrollToBottom();
  3766. });
  3767. } else {
  3768. this.talklist = [];
  3769. }
  3770. } catch (error) {
  3771. this.talklist = [];
  3772. console.error('获取聊天记录失败:', error);
  3773. }
  3774. },
  3775. // 获取直播间用户
  3776. // async getliveUser(isLoadMore = false) {
  3777. // this.viewLoading = true;
  3778. // try {
  3779. // const res = await watchUserList(this.liveId, this.viewPageSize, this.viewPageNum, false);
  3780. // console.log("getliveUser", res);
  3781. // if (res.code === 200) {
  3782. // const userRows = Array.isArray(res.rows) ? res.rows : [];
  3783. // let array = userRows.map((item) => ({
  3784. // avatar: item.avatar || '',
  3785. // userId: item.userId || '',
  3786. // nickName: item.nickName || '未命名'
  3787. // }));
  3788. // // 虚拟数据
  3789. // let virtualData = [];
  3790. // let virtualTotal = res.total * 10;
  3791. // this.liveUserTotal = virtualTotal;
  3792. // for (let i = 0; i < virtualTotal; i++) {
  3793. // let data = {
  3794. // avatar: '',
  3795. // userId: '8565' + i,
  3796. // nickName: '微信用户'
  3797. // };
  3798. // virtualData.push(data);
  3799. // }
  3800. // this.liveViewersData = [...array, ...virtualData];
  3801. // const newRows = Array.isArray(res.rows) ? res.rows : [];
  3802. // const currentViewers = Array.isArray(this.liveViewers) ? this.liveViewers : [];
  3803. // let viewlist = isLoadMore ? [...currentViewers, ...newRows] : newRows;
  3804. // this.liveViewers = [...viewlist, ...virtualData];
  3805. // this.viewPageNum++;
  3806. // }
  3807. // } catch (error) {
  3808. // console.error('获取观众列表失败:', error);
  3809. // } finally {
  3810. // this.viewLoading = false;
  3811. // }
  3812. // },
  3813. //小黄车搜索商品
  3814. handleSearchInput() {
  3815. // 使用防抖优化性能,避免频繁请求
  3816. clearTimeout(this.searchTimer);
  3817. this.searchTimer = setTimeout(() => {
  3818. this.queryCollect();
  3819. }, 500); // 500毫秒延迟
  3820. },
  3821. // 显示购买提示信息
  3822. showPurchaseMessage() {
  3823. // 清除之前的定时器
  3824. if (this.purchasePromptTimer) {
  3825. clearTimeout(this.purchasePromptTimer);
  3826. }
  3827. // 显示提示
  3828. this.showPurchasePrompt = true;
  3829. // 2秒后自动隐藏
  3830. this.purchasePromptTimer = setTimeout(() => {
  3831. this.showPurchasePrompt = false;
  3832. }, 2000);
  3833. },
  3834. //名字超过省略
  3835. truncateString(str, maxLength) {
  3836. if (typeof str !== 'string' || str.length <= maxLength) {
  3837. return str;
  3838. }
  3839. return str.slice(0, maxLength) + '...'; // 截断后加省略号
  3840. },
  3841. // 跳转页面
  3842. navgetTo(url) {
  3843. uni.navigateTo({
  3844. url: url
  3845. });
  3846. },
  3847. // 查询用户积分领取记录
  3848. completionRecords() {
  3849. if (!this.liveId) return;
  3850. const data = {
  3851. liveId: this.liveId
  3852. }
  3853. completionRecords(data).then((res) => {
  3854. if (res.code == 200) {
  3855. const targetData = res.data.find(item => item.liveId == this.liveId);
  3856. if (targetData) {
  3857. // 找到匹配的数据
  3858. const receiveStatus = targetData.receiveStatus;
  3859. this.receiveStatus = receiveStatus;
  3860. } else {
  3861. console.log('未找到liveId为', this.liveId, '的数据');
  3862. this.receiveStatus = false; // 或其他默认值
  3863. }
  3864. console.log("查询用户积分领取记录", res)
  3865. }
  3866. }).catch((error) => {
  3867. });
  3868. },
  3869. // 修改获取直播信息方法
  3870. async getliving(liveId) {
  3871. if (!liveId) return;
  3872. const param = {
  3873. id: liveId
  3874. };
  3875. try {
  3876. const res = await getlive(param);
  3877. if (res.code !== 200) {
  3878. uni.showToast({
  3879. title: res.msg,
  3880. icon: 'none'
  3881. });
  3882. return;
  3883. }
  3884. this.liveItem = Object.assign({}, this.liveItem, res.data);
  3885. // 在获取到直播数据后初始化倒计时
  3886. this.$nextTick(() => {
  3887. this.initCountdown();
  3888. });
  3889. if (res.data.globalVisible || res.data.singleVisible) {
  3890. this.myselfFlag = true;
  3891. }
  3892. // this.talklist = Array.isArray((res.data || {}).talklist) ? res.data.talklist : [];
  3893. this.startTimeTimer(this.liveItem);
  3894. // 清除旧定时器(如预告倒计时)
  3895. if (this.liveStartTimer) {
  3896. clearInterval(this.liveStartTimer);
  3897. this.liveStartTimer = null;
  3898. }
  3899. if (res.data.status == 1) {
  3900. // 直播预告
  3901. this.liveStartTimer = setInterval(async () => {
  3902. this.liveCountdown = this.handleTime(res.data.startTime, 0);
  3903. if (!this.liveCountdown) {
  3904. uni.removeStorageSync('isAgreement');
  3905. await this.getliving(this.liveId);
  3906. clearInterval(this.liveStartTimer);
  3907. }
  3908. }, 1000);
  3909. this.$set(this.liveItem, 'previewUrl', res.data.previewUrl);
  3910. this.$set(this.liveItem, 'livingUrl', ''); // 清空直播流
  3911. this.$set(this.liveItem, 'videoUrl', ''); // 清空回放视频
  3912. } else if (res.data.status == 2) {
  3913. if (res.data.liveType == 1) {
  3914. // 直播流
  3915. let cTime = Math.floor(Math.random() * 10000) + 1;
  3916. let livingUrl = res.data.flvHlsUrl + '&t=' + cTime;
  3917. console.log('地址在', this.liveItem.livingUrl);
  3918. this.$set(this.liveItem, 'livingUrl', livingUrl);
  3919. this.$set(this.liveItem, 'videoUrl', ''); // 清空回放视频
  3920. } else if (res.data.liveType === 2) {
  3921. // 回放视频 2录播 3直播回放
  3922. // let urlStr = 'https://fs-1319721001.cos.ap-chongqing.myqcloud.com/10%E6%9C%8820%E6%97%A5%20%281%29.m3u8'
  3923. // this.$set(this.liveItem, 'videoUrl', urlStr);
  3924. this.$set(this.liveItem, 'videoUrl', res.data.videoUrl);
  3925. this.$set(this.liveItem, 'livingUrl', '');
  3926. }
  3927. } else if (res.data.status == 4 && res.data.liveType == 3) {
  3928. this.$set(this.liveItem, 'videoUrl', res.data.videoUrl);
  3929. this.$set(this.liveItem, 'livingUrl', '');
  3930. } else {
  3931. // 未开播
  3932. this.$set(this.liveItem, 'livingUrl', '');
  3933. this.$set(this.liveItem, 'videoUrl', '');
  3934. }
  3935. if (this.liveItem.configJson) {
  3936. this.completionRate = JSON.parse(this.liveItem.configJson).completionRate * 0.01
  3937. }
  3938. this.$set(this.liveItem, 'autoplay', res.data.liveType !== 0);
  3939. this.$set(this.liveItem, 'showType', res.data.showType);
  3940. this.storeId = res.storeId;
  3941. this.startLiveViewDataTimer();
  3942. this.completionRecords();
  3943. // 初始化观看时间统计
  3944. this.initWatchTime();
  3945. const diff = this.getTimeDifferenceInSeconds(this.liveItem.startTime)
  3946. if (diff >= this.liveItem.duration) {
  3947. const diffTime = diff - this.liveItem.duration
  3948. if (diffTime < 180) {
  3949. this.generating = true;
  3950. // 计算剩余等待时间
  3951. const remainingTime = Math.max(0, 180 - diffTime) * 1000;
  3952. if (this.generatingTimer) {
  3953. clearTimeout(this.generatingTimer);
  3954. }
  3955. this.generatingTimer = setTimeout(() => {
  3956. this.generating = false;
  3957. this.hasPlayback = true;
  3958. this.playVideo();
  3959. // this.closeWebSocket(true);
  3960. this.isEnd = true;
  3961. }, remainingTime);
  3962. } else {
  3963. // 已经超过18秒,应该直接播放
  3964. this.generating = false;
  3965. this.hasPlayback = true;
  3966. this.playVideo();
  3967. // this.closeWebSocket(true);
  3968. this.isEnd = true;
  3969. }
  3970. } else {
  3971. // 直播还未结束,正常播放
  3972. this.playVideo();
  3973. }
  3974. } catch (err) {
  3975. console.error('获取直播信息失败:', err);
  3976. uni.showToast({
  3977. title: '获取直播信息失败',
  3978. icon: 'none'
  3979. });
  3980. }
  3981. },
  3982. getPureDecimal(num, precision = 6) {
  3983. const decimalPart = Math.abs(num).toFixed(precision).split('.')[1];
  3984. return decimalPart?.replace(/0+$/, '') || ''; // 移除末尾多余的0
  3985. },
  3986. goBack() {
  3987. // 暂停当前视频
  3988. if (this.liveItem) {
  3989. this.pauseVideo();
  3990. }
  3991. // 关闭WebSocket连接
  3992. this.closeWebSocket(true);
  3993. // 导航返回
  3994. // const pages = getCurrentPages();
  3995. // if (pages.length > 1) {
  3996. // uni.navigateBack();
  3997. // } else {
  3998. // uni.reLaunch({
  3999. // url: '/pages_course/livingList'
  4000. // });
  4001. // }
  4002. const pages = getCurrentPages();
  4003. uni.reLaunch({
  4004. url: '/pages/home/index'
  4005. });
  4006. },
  4007. // 点赞
  4008. async onLike() {
  4009. if (!this.liveId) return;
  4010. try {
  4011. const res = await liveDataLike(this.liveId);
  4012. if (res?.like) {
  4013. this.liveViewData.like++; //只更新当前直播间的点赞数
  4014. } else {
  4015. uni.showToast({
  4016. title: res.msg,
  4017. icon: 'none'
  4018. });
  4019. }
  4020. } catch (error) {
  4021. console.error('点赞失败:', error);
  4022. }
  4023. },
  4024. //直播间点赞、关注、在线人数数据
  4025. getliveViewData() {
  4026. if (!this.liveId) return;
  4027. getLiveViewData(this.liveId).then((res) => {
  4028. if (res.code == 200) {
  4029. // 强制响应式更新,确保数据实时显示
  4030. this.liveViewData = res;
  4031. }
  4032. }).catch((error) => {
  4033. console.error('获取直播间数据失败:', error);
  4034. // 失败时兜底,避免显示异常
  4035. this.liveViewData = {
  4036. like: 0,
  4037. watchCount: 0
  4038. };
  4039. });
  4040. },
  4041. // 30秒刷新一下直播间点赞数
  4042. startLiveViewDataTimer() {
  4043. // 先清除旧定时器(防止重复创建)
  4044. if (this.liveViewDataTimer) {
  4045. clearInterval(this.liveViewDataTimer);
  4046. this.liveViewDataTimer = null;
  4047. }
  4048. this.getliveViewData();
  4049. this.liveViewDataTimer = setInterval(() => {
  4050. // 安全校验:确保liveItem和liveId存在(避免无效请求)
  4051. if (this.liveId) {
  4052. this.getliveViewData();
  4053. }
  4054. }, 20000);
  4055. },
  4056. // 去购买,跳商品详情
  4057. goShop(productId, goodsId) {
  4058. if (!this.liveId) return;
  4059. uni.navigateTo({
  4060. url: '/pages_shopping/live/goods?productId=' + productId + '&liveId=' + this.liveId +
  4061. '&goodsId=' + goodsId + '&storeId=' + this.storeId
  4062. });
  4063. },
  4064. // 查询店铺
  4065. async queryCollect() {
  4066. this.loadingProducts = true;
  4067. if (!this.liveId) return;
  4068. if (this.inputInfo == null) this.inputInfo = '';
  4069. uni.showLoading({
  4070. title: '加载中'
  4071. });
  4072. try {
  4073. const res = await liveStore(this.liveId, this.inputInfo);
  4074. uni.hideLoading();
  4075. this.shopping = true;
  4076. if (res.code === 200) {
  4077. // 数据绑定到当前 liveItem,避免全局污染
  4078. this.products = Array.isArray(res.data) ? res.data : [];
  4079. }
  4080. } catch (error) {
  4081. console.error('获取小黄车商品失败:', error);
  4082. } finally {
  4083. this.loadingProducts = false;
  4084. }
  4085. },
  4086. // 时间戳
  4087. initTime() {
  4088. const now = new Date();
  4089. this.timestamp = now.getTime();
  4090. },
  4091. openCart() {
  4092. this.queryCollect();
  4093. },
  4094. close() {
  4095. this.showadd = false;
  4096. },
  4097. // 关闭小黄车
  4098. closeShop() {
  4099. this.shopping = false;
  4100. },
  4101. closeMore() {
  4102. this.isMore = false;
  4103. },
  4104. closeWin() {
  4105. this.winning = false;
  4106. },
  4107. // 关闭WebSocket连接(isManual:是否手动关闭)
  4108. closeWebSocket(isManual = true) {
  4109. if (!this.socket || !this.isSocketOpen) {
  4110. // console.warn('WebSocket 任务不存在或未打开,无需关闭');
  4111. return;
  4112. }
  4113. console.log(`WebSocket连接关闭 - ${isManual ? '手动' : '自动'}`);
  4114. this.isManualClose = isManual;
  4115. // 清理所有定时器和状态
  4116. this.cleanupAllResources();
  4117. try {
  4118. // 先保存引用,避免在关闭过程中被置为null
  4119. const socketToClose = this.socket;
  4120. this.socket = null;
  4121. this.isSocketOpen = false;
  4122. this.isConnecting = false;
  4123. socketToClose.close({
  4124. code: 1000,
  4125. reason: isManual ? '主动关闭' : '异常关闭'
  4126. });
  4127. console.log('WebSocket连接已发起关闭');
  4128. } catch (err) {
  4129. console.error('关闭WebSocket失败:', err);
  4130. // 即使关闭失败,也要重置状态
  4131. this.socket = null;
  4132. this.isSocketOpen = false;
  4133. this.isConnecting = false;
  4134. }
  4135. },
  4136. // 清理所有资源
  4137. cleanupAllResources() {
  4138. // 停止心跳相关
  4139. this.stopHeartBeat();
  4140. // 清理ping超时定时器
  4141. if (this.pingTimeoutTimer) {
  4142. clearTimeout(this.pingTimeoutTimer);
  4143. this.pingTimeoutTimer = null;
  4144. }
  4145. // 清理网络状态定时器
  4146. if (this.networkStatusTimer) {
  4147. clearTimeout(this.networkStatusTimer);
  4148. this.networkStatusTimer = null;
  4149. }
  4150. // 重置重连状态
  4151. this.resetReconnectState();
  4152. // 重置心跳相关状态
  4153. this.heartBeatRetryCount = 0;
  4154. this.lastHeartBeatTime = 0;
  4155. this.adaptiveHeartBeatInterval = this.heartBeatInterval;
  4156. },
  4157. startHeartBeat() {
  4158. this.stopHeartBeat(); // 先停止现有心跳,防止重复
  4159. // 使用自适应间隔循环发送心跳
  4160. this.heartBeatTimer = setInterval(() => {
  4161. this.sendHeartBeat();
  4162. }, this.adaptiveHeartBeatInterval);
  4163. },
  4164. initSocket() {
  4165. // 检查是否正在连接中
  4166. if (this.isConnecting) {
  4167. console.log('WebSocket正在连接中,跳过重复连接');
  4168. return;
  4169. }
  4170. // 检查网络状态
  4171. if (!this.isNetworkAvailable) {
  4172. console.warn('网络不可用,延迟WebSocket连接');
  4173. // 使用定时器而不是递归调用,避免内存泄漏
  4174. if (!this.networkRetryTimer) {
  4175. this.networkRetryTimer = setTimeout(() => {
  4176. this.networkRetryTimer = null;
  4177. this.initSocket();
  4178. }, 3000);
  4179. }
  4180. return;
  4181. }
  4182. // 如果已经存在连接且状态为open,则退出
  4183. if (this.socket && this.socket.readyState === 1) {
  4184. console.log('WebSocket连接已存在且正常,无需重新连接');
  4185. // 重置重连计数
  4186. this.reconnectCount = 0;
  4187. return;
  4188. }
  4189. // 关闭现有连接
  4190. if (this.socket && (this.socket.readyState === 0 || this.socket.readyState === 1)) {
  4191. console.log('关闭现有WebSocket连接,创建新连接');
  4192. this.closeWebSocket(false); // 非手动关闭
  4193. // 等待一小段时间确保连接完全关闭
  4194. setTimeout(() => {
  4195. this.createWebSocketConnection();
  4196. }, 100);
  4197. return;
  4198. }
  4199. this.createWebSocketConnection();
  4200. },
  4201. createWebSocketConnection() {
  4202. // 校验必要参数
  4203. if (!this.liveId) {
  4204. console.error('缺失直播间ID,无法初始化WebSocket');
  4205. return;
  4206. }
  4207. if (!this.userInfo || !this.userInfo.userId) {
  4208. console.error('用户信息缺失,无法初始化WebSocket');
  4209. return;
  4210. }
  4211. // 设置连接状态
  4212. this.isConnecting = true;
  4213. this.isSocketOpen = false;
  4214. // 记录连接开始时间(性能监控)
  4215. this.connectionStartTime = Date.now();
  4216. // 清除之前的重连定时器
  4217. this.resetReconnectState();
  4218. const now = new Date();
  4219. this.timestamp = now.getTime(); // 生成签名
  4220. const signature = CryptoJS.HmacSHA256(
  4221. `${this.liveId}${this.userInfo.userId}${this.userType}${this.timestamp}`, this.timestamp.toString()
  4222. ).toString(CryptoJS.enc.Hex);
  4223. try {
  4224. const baseWsUrl = 'wss://ws.klbycp.com/ws/app/webSocket';
  4225. // const baseWsUrl = 'wss://api.fhhx.runtzh.com/ws/app/webSocket';
  4226. // const baseWsUrl = 'ws://d6998672.natappfree.cc/ws/app/webSocket';
  4227. // const baseWsUrl = 'ws://nd967d83.natappfree.cc/ws/app/webSocket';
  4228. let wsUrl =
  4229. `${baseWsUrl}?userId=${this.userInfo.userId}&liveId=${this.liveId}&userType=${this.userType}&timestamp=${this.timestamp}&signature=${signature}`;
  4230. // let wsUrl =
  4231. // 'ws://q96d9752.natappfree.cc/ws/app/webSocket?liveId=128&userId=9769&userType=1&timestamp=1762501143712&signature=0498c66c3fca64ab009586200c04d51312ce44e36d5639e99b22e34f3bac8d13';
  4232. if (this.qrFrom) {
  4233. wsUrl += this.qrFrom;
  4234. }
  4235. // if(this.address){
  4236. // console.log("this.address是什么>>>", this.address)
  4237. wsUrl += `&location=${this.address}`;
  4238. // wsUrl += `&location=${encodeURIComponent(liveWatchUser)}`;
  4239. // }
  4240. console.log("qxj wsUrl", wsUrl);
  4241. console.log(`尝试连接WebSocket: ${wsUrl.replace(/signature=[^&]+/, 'signature=***')}`);
  4242. const socketTask = uni.connectSocket({
  4243. url: wsUrl,
  4244. success: () => {
  4245. console.log("WebSocket连接请求发送成功");
  4246. },
  4247. fail: (err) => {
  4248. console.error('WebSocket连接请求失败:', err);
  4249. this.isConnecting = false;
  4250. this.handleConnectionError('连接请求失败', err);
  4251. }
  4252. });
  4253. // 连接打开事件
  4254. socketTask.onOpen((res) => {
  4255. console.log("WebSocket连接已打开");
  4256. this.socket = socketTask;
  4257. this.isConnecting = false;
  4258. this.isSocketOpen = true;
  4259. // 计算连接延迟(性能监控)
  4260. if (this.connectionStartTime > 0) {
  4261. this.connectionLatency = Date.now() - this.connectionStartTime;
  4262. console.log(`WebSocket连接延迟: ${this.connectionLatency}ms`);
  4263. }
  4264. this.reconnectCount = 0;
  4265. this.resetReconnectState();
  4266. this.heartBeatRetryCount = 0; // 重置心跳重试次数
  4267. this.shownEntryUsers.clear(); // 重连后重置进入提示记录
  4268. // 连接成功后显示提示(仅首次连接)
  4269. if (this.reconnectCount === 0) {
  4270. console.log('WebSocket连接建立成功');
  4271. } else {
  4272. console.log(`WebSocket重连成功(第${this.reconnectCount}次尝试)`);
  4273. }
  4274. this.startHeartBeat();
  4275. // 启动观看时长统计
  4276. this.startWatchDurationTracking();
  4277. });
  4278. // 消息接收事件
  4279. socketTask.onMessage((res) => {
  4280. // 消息计数(性能监控)
  4281. this.messageCount++;
  4282. try {
  4283. //console.log("qxj onMessage handleSocketMessage",res);
  4284. const data = JSON.parse(res.data);
  4285. // 处理服务端心跳响应 - 修复:后端返回的是heartBeat而不是heartBeatAck
  4286. if (!!data.data && data.data.cmd === 'heartbeat') {
  4287. // console.log('收到心跳响应:', data.data);
  4288. // 清除ping超时定时器
  4289. if (this.pingTimeoutTimer) {
  4290. clearTimeout(this.pingTimeoutTimer);
  4291. this.pingTimeoutTimer = null;
  4292. }
  4293. // 重置心跳重试次数
  4294. this.heartBeatRetryCount = 0;
  4295. // 更新网络状态为良好
  4296. this.adjustHeartBeatInterval(true);
  4297. return;
  4298. }
  4299. this.handleSocketMessage(res);
  4300. } catch (err) {
  4301. console.error('消息解析异常:', err);
  4302. this.errorCount++; // 错误计数
  4303. }
  4304. });
  4305. // 连接错误事件
  4306. socketTask.onError((err) => {
  4307. console.error('WebSocket连接错误:', err);
  4308. this.errorCount++; // 错误计数
  4309. this.isSocketOpen = false;
  4310. this.isConnecting = false;
  4311. this.stopHeartBeat();
  4312. this.handleConnectionError('连接错误', err);
  4313. });
  4314. // 连接关闭事件
  4315. socketTask.onClose((res) => {
  4316. console.log('WebSocket连接关闭:', res);
  4317. this.isSocketOpen = false;
  4318. this.isConnecting = false;
  4319. this.stopHeartBeat(); // 清除心跳定时器
  4320. // 暂停观看时长统计
  4321. this.pauseWatchDurationTracking();
  4322. // 根据关闭原因决定是否重连
  4323. if (!this.isManualClose) {
  4324. if (res.code === 1000) {
  4325. console.log('WebSocket正常关闭,不进行重连');
  4326. } else {
  4327. console.warn(`WebSocket异常关闭 (code: ${res.code}, reason: ${res.reason})`);
  4328. this.handleReconnect();
  4329. }
  4330. } else {
  4331. console.log('WebSocket手动关闭,不进行重连');
  4332. }
  4333. });
  4334. } catch (e) {
  4335. console.error('创建WebSocket异常:', e);
  4336. this.handleReconnect();
  4337. }
  4338. },
  4339. handleTime(time, duration) {
  4340. let timeStamp;
  4341. if (typeof time === 'number' && time > 0 && time < 9999999999999) {
  4342. timeStamp = time;
  4343. } else if (typeof time === 'string' && time.trim() !== '') {
  4344. // 手动解析 CST 时间字符串
  4345. const match = time.match(/^(\w{3}) (\w{3}) (\d{1,2}) (\d{1,2}):(\d{2}):(\d{2}) CST (\d{4})$/);
  4346. if (match) {
  4347. const [, day, month, date, hours, minutes, seconds, year] = match;
  4348. // 月份映射
  4349. const monthMap = {
  4350. 'Jan': 0,
  4351. 'Feb': 1,
  4352. 'Mar': 2,
  4353. 'Apr': 3,
  4354. 'May': 4,
  4355. 'Jun': 5,
  4356. 'Jul': 6,
  4357. 'Aug': 7,
  4358. 'Sep': 8,
  4359. 'Oct': 9,
  4360. 'Nov': 10,
  4361. 'Dec': 11
  4362. };
  4363. // 创建日期对象(CST 是 UTC+8,与中国时区一致)
  4364. const jsDate = new Date(
  4365. parseInt(year),
  4366. monthMap[month],
  4367. parseInt(date),
  4368. parseInt(hours),
  4369. parseInt(minutes),
  4370. parseInt(seconds)
  4371. );
  4372. timeStamp = jsDate.getTime();
  4373. } else {
  4374. // 如果不是预期格式,回退到普通解析
  4375. const date = new Date(time);
  4376. if (!isNaN(date.getTime())) {
  4377. timeStamp = date.getTime();
  4378. } else {
  4379. console.error('无效的日期格式:', time);
  4380. return false;
  4381. }
  4382. }
  4383. } else {
  4384. console.error('time参数必须是有效的时间戳(数字)或日期字符串');
  4385. return false;
  4386. }
  4387. const targetTimestamp = timeStamp + duration * 60 * 1000;
  4388. const currentTimestamp = Date.now();
  4389. const timeDiffMs = targetTimestamp - currentTimestamp;
  4390. if (timeDiffMs <= 0) {
  4391. return false;
  4392. }
  4393. const hours = Math.floor(timeDiffMs / (1000 * 60 * 60));
  4394. const minutes = Math.floor((timeDiffMs % (1000 * 60 * 60)) / (1000 * 60));
  4395. const seconds = Math.floor((timeDiffMs % (1000 * 60)) / 1000);
  4396. const formatNum = (num) => num.toString().padStart(2, '0');
  4397. return {
  4398. hours: formatNum(hours),
  4399. minutes: formatNum(minutes),
  4400. seconds: formatNum(seconds)
  4401. };
  4402. },
  4403. // 限制聊天消息数量,防止内存泄漏
  4404. addToTalkList(message) {
  4405. // console.log("修改后的message>>", message)
  4406. // 减少最大消息数量,提升iPhone性能
  4407. const MAX_TALK_ITEMS = 30;
  4408. // 直接操作数组,避免创建新数组
  4409. if (!Array.isArray(this.talklist)) {
  4410. this.talklist = [];
  4411. }
  4412. const wasAtLimit = this.talklist.length >= MAX_TALK_ITEMS;
  4413. //console.log(`添加消息前: 当前消息数量=${this.talklist.length}, 是否达到限制=${wasAtLimit}`);
  4414. // 检查是否是自己发送的消息
  4415. const isMyMessage = message.userId === this.userInfo.userId;
  4416. //console.log(`消息来源: ${isMyMessage ? '自己' : '他人'}, userId=${message.userId}`);
  4417. // 给消息添加唯一ID
  4418. message.uniqueId = ++this.messageIdCounter;
  4419. let msgdata = JSON.parse(message.data);
  4420. message.msgId = msgdata.msgId;
  4421. this.talklist.push(message);
  4422. // 当消息超过限制时,批量删除旧消息
  4423. if (this.talklist.length > MAX_TALK_ITEMS) {
  4424. const removeCount = this.talklist.length - MAX_TALK_ITEMS;
  4425. //console.log(`消息超过限制,删除前${removeCount}条旧消息`);
  4426. this.talklist.splice(0, removeCount);
  4427. }
  4428. //console.log(`添加消息后: 当前消息数量=${this.talklist.length}`);
  4429. // 强制触发视图更新
  4430. this.$forceUpdate();
  4431. // 使用单次nextTick确保DOM更新后再滚动
  4432. this.$nextTick(() => {
  4433. this.forceScrollToBottomOnSend();
  4434. });
  4435. },
  4436. // 处理Socket消息
  4437. async handleSocketMessage(message) {
  4438. try {
  4439. let data = JSON.parse(message.data);
  4440. const socketMessage = data.data; // 服务端返回的消息体
  4441. //console.log("qxj handleSocketMessage socketMessage",socketMessage);
  4442. if (data.code == 200) {
  4443. const messageData = {
  4444. ...socketMessage,
  4445. cmd: socketMessage.cmd || '', // 确保cmd字段存在
  4446. ts: Date.now() // 时间戳
  4447. };
  4448. // 处理服务端返回的sendMsg消息,加入本地列表
  4449. if (socketMessage.cmd == 'sendMsg') {
  4450. if (!this.isSocketAvailable()) {
  4451. uni.showToast({
  4452. title: '连接已断开,正在重试...',
  4453. icon: 'none'
  4454. });
  4455. this.handleReconnect();
  4456. return;
  4457. }
  4458. this.addToTalkList(messageData);
  4459. } else if (socketMessage.cmd == 'red') {
  4460. const redData = socketMessage.data ? JSON.parse(socketMessage.data) : {};
  4461. this.redInfo = redData || {};
  4462. this.isShowRed = socketMessage.status === 1;
  4463. if (this.isShowRed) {
  4464. this.redTimer = setInterval(() => {
  4465. const redCountdown = this.handleTime(this.redInfo.updateTime, this.redInfo
  4466. .duration);
  4467. if (!redCountdown) {
  4468. this.isShowRed = false;
  4469. clearInterval(this.redTimer);
  4470. }
  4471. }, 1000);
  4472. }
  4473. } else if (socketMessage.cmd == 'goods') {
  4474. const goodsData = socketMessage.data ? JSON.parse(socketMessage.data) : {};
  4475. this.goodsCard = goodsData || {};
  4476. this.isShowGoods = socketMessage.status == 1;
  4477. } else if (socketMessage.cmd == 'coupon') {
  4478. const couponData = socketMessage.data ? JSON.parse(socketMessage.data) : {};
  4479. this.couponInfo = couponData || {};
  4480. this.isShowCoupon = socketMessage.status === 1;
  4481. if (this.isShowCoupon) {}
  4482. } else if (socketMessage.cmd == 'likeDetail') {
  4483. this.liveViewData.like = socketMessage.data;
  4484. } else if (socketMessage.cmd == 'lottery') {
  4485. const lotteryData = socketMessage.data ? JSON.parse(socketMessage.data) : {};
  4486. this.lotteryInfo = lotteryData || {};
  4487. this.isShowLottery = socketMessage.status === 1;
  4488. if (socketMessage.status != 1) {
  4489. this.isShowLotteryPop = false;
  4490. }
  4491. // 清除已有定时器(无论状态是否为1,先清掉旧的)
  4492. clearInterval(this.lotteryTimer);
  4493. if (this.isShowLottery) {
  4494. this.lotteryTimer = setInterval(() => {
  4495. this.countdown = this.handleTime(this.lotteryInfo.updateTime, this
  4496. .lotteryInfo.duration);
  4497. if (!this.countdown) {
  4498. console.log('倒计时', this.countdown);
  4499. this.isShowLottery = false;
  4500. this.isShowLotteryPop = false;
  4501. clearInterval(this.lotteryTimer);
  4502. }
  4503. }, 1000);
  4504. } else {
  4505. this.isShowLottery = false;
  4506. }
  4507. } else if (socketMessage.cmd == 'globalVisible' || socketMessage.cmd == 'singleVisible') {
  4508. // status = 1的时候,用户消息自可见,其他消息丢失}
  4509. if (socketMessage.status == 1) {
  4510. this.myselfFlag = true;
  4511. } else {
  4512. this.myselfFlag = false;
  4513. }
  4514. } else if (socketMessage.cmd == 'sendPopMsg') {
  4515. this.popMsg = socketMessage.msg
  4516. this.isShowPopMsg = true
  4517. } else if (socketMessage.cmd == 'sendTopMsg') {
  4518. clearInterval(this.noticeTimer);
  4519. const noticeData = socketMessage.data ? JSON.parse(socketMessage.data) : {};
  4520. this.notice = noticeData || {};
  4521. this.isShowNotice = true
  4522. if (this.isShowNotice) {
  4523. this.noticeTimer = setInterval(() => {
  4524. const noticeCountdown = this.handleTime(this.notice.endTime, 0);
  4525. if (!noticeCountdown) {
  4526. this.isShowNotice = false;
  4527. clearInterval(this.noticeTimer);
  4528. }
  4529. }, 1000);
  4530. }
  4531. } else if (socketMessage.cmd == 'entry') {
  4532. try {
  4533. if (!this.liveUserCalled) {
  4534. this.liveUserCalled = true;
  4535. }
  4536. const userIdToEntry = socketMessage.userId;
  4537. const existingIndex = this.liveViewersData.findIndex((item) => item.userId ===
  4538. userIdToEntry);
  4539. if (existingIndex === -1) {
  4540. const liveViewers = {
  4541. userId: socketMessage.userId,
  4542. nickName: socketMessage.nickName,
  4543. avatar: socketMessage.avatar
  4544. };
  4545. this.liveViewersData.push(liveViewers);
  4546. this.liveUserTotal++;
  4547. }
  4548. // 解析用户ID(根据实际接口字段调整,此处假设data含userId)
  4549. const userInfo = JSON.parse(socketMessage.data || '{}');
  4550. const userId = userInfo.userId || socketMessage.userId; // 兼容不同字段
  4551. if (!userId) return; // 无用户ID不处理
  4552. // 仅新用户(未显示过)才触发提示
  4553. if (!this.shownEntryUsers.has(userId)) {
  4554. this.inAndOut = socketMessage;
  4555. this.showWelcomeMessage = true;
  4556. this.shownEntryUsers.add(userId); // 加入已显示集合
  4557. messageData.msg = '来了'
  4558. this.addToTalkList(messageData);
  4559. // 3秒后隐藏提示(可调整时长)
  4560. if (this.welcomeTimer) clearTimeout(this.welcomeTimer);
  4561. this.welcomeTimer = setTimeout(() => {
  4562. this.showWelcomeMessage = false;
  4563. }, 3000);
  4564. }
  4565. } catch (err) {
  4566. console.error('解析entry用户数据失败:', err);
  4567. }
  4568. } else if (socketMessage.cmd == 'out') {
  4569. if (this.liveUserTotal > 0) {
  4570. const userIdToRemove = socketMessage.userId;
  4571. const index = this.liveViewersData.findIndex((item) => item.userId === userIdToRemove);
  4572. if (index !== -1) {
  4573. this.liveViewersData.splice(index, 1);
  4574. this.liveUserTotal--; // 根据userId删除对应的用户数据
  4575. }
  4576. }
  4577. this.inAndOut = socketMessage;
  4578. this.showWelcomeMessage = true;
  4579. // 3秒后隐藏提示
  4580. if (this.welcomeTimer) clearTimeout(this.welcomeTimer);
  4581. this.welcomeTimer = setTimeout(() => {
  4582. this.showWelcomeMessage = false;
  4583. }, 3000);
  4584. } else if (socketMessage.cmd == 'live_start' || socketMessage.cmd == 'live_end') {
  4585. // 开始直播,关闭直播
  4586. if (this.liveStartTimer) {
  4587. clearInterval(this.liveStartTimer);
  4588. this.liveStartTimer = null;
  4589. }
  4590. if (this.redTimer) {
  4591. clearInterval(this.redTimer);
  4592. this.redTimer = null;
  4593. }
  4594. // 请求最新直播间数据
  4595. this.$nextTick(() => {
  4596. this.getliving(this.liveId);
  4597. });
  4598. } else if (socketMessage.cmd == 'Integral') {
  4599. this.integral = {
  4600. msg: socketMessage.msg,
  4601. status: true
  4602. };
  4603. }
  4604. // else if (socketMessage.cmd == 'completionPoints') {
  4605. // uni.showToast({
  4606. // title: socketMessage.msg,
  4607. // icon: 'none',
  4608. // duration: 3000
  4609. // });
  4610. // this.completionUnreceived()
  4611. // }
  4612. else if (socketMessage.cmd == 'completionPoints') {
  4613. // 先显示toast
  4614. uni.showToast({
  4615. title: socketMessage.msg,
  4616. icon: 'none',
  4617. duration: 3000
  4618. });
  4619. // 1秒后关闭弹窗并执行completionUnreceived
  4620. setTimeout(() => {
  4621. // 如果需要手动关闭toast(虽然设置了3秒自动关闭,但这里提前关闭)
  4622. uni.hideToast();
  4623. // 执行completionUnreceived方法
  4624. this.completionUnreceived();
  4625. }, 1000); // 1000毫秒 = 1秒
  4626. } else if (socketMessage.cmd == 'userCount') {
  4627. // 广播在线人数
  4628. this.virtualHuman = socketMessage.data * 10
  4629. } else if (socketMessage.cmd == 'deleteMsg') {
  4630. const index = this.talklist.findIndex(item => item.msgId == socketMessage.msg);
  4631. if (index !== -1) {
  4632. this.talklist.splice(index, 1);
  4633. }
  4634. } else if (socketMessage.cmd == 'LotteryDetail') {
  4635. try {
  4636. this.prizeInfo = Array.isArray(JSON.parse(socketMessage.data || '[]')) ? JSON.parse(
  4637. socketMessage.data || '[]') : [];
  4638. } catch (err) {
  4639. console.error('解析抽奖结果失败:', err);
  4640. this.prizeInfo = [];
  4641. }
  4642. this.isShowPrize = true;
  4643. this.isShowLottery = false;
  4644. this.isShowLotteryPop = false;
  4645. } else if (socketMessage.cmd == 'blockUser') {
  4646. uni.removeStorage({
  4647. key: 'AppToken',
  4648. success: () => {
  4649. uni.reLaunch({
  4650. url: '/pages/auth/login'
  4651. });
  4652. }
  4653. });
  4654. }
  4655. } else {
  4656. uni.showToast({
  4657. title: data.msg,
  4658. icon: 'none'
  4659. });
  4660. }
  4661. } catch (error) {
  4662. console.error('Socket消息处理失败:', error);
  4663. }
  4664. },
  4665. sendMsg(retries = 1) {
  4666. // 防止连续点击发送两次(800ms短时锁定)
  4667. if (this.isSending) return;
  4668. this.isSending = true;
  4669. setTimeout(() => {
  4670. this.isSending = false;
  4671. }, 800);
  4672. const text = (this.value || '').trim();
  4673. if (!text) {
  4674. uni.showToast({
  4675. title: '不能发送空消息',
  4676. icon: 'none'
  4677. });
  4678. return;
  4679. }
  4680. if (!this.isSocketAvailable()) {
  4681. if (retries > 0) {
  4682. uni.showToast({
  4683. title: `连接不稳定,正在重试(${retries}次)...`,
  4684. icon: 'none'
  4685. });
  4686. // 延迟500ms重试,重试次数减1
  4687. setTimeout(() => this.sendMsg(retries - 1), 500);
  4688. } else {
  4689. uni.showToast({
  4690. title: '连接已断开,发送失败',
  4691. icon: 'none'
  4692. });
  4693. this.value = text; // 恢复输入框内容
  4694. }
  4695. return;
  4696. }
  4697. const liveId = this.liveId;
  4698. this.value = ''; // 立即清空输入框
  4699. // 构造发送给服务端的消息数据
  4700. const data = {
  4701. liveId,
  4702. userId: this.userInfo.userId,
  4703. userType: 0,
  4704. cmd: 'sendMsg',
  4705. msg: text,
  4706. nickName: this.userInfo.nickname || '未命名',
  4707. avatar: this.userInfo.avatar ||
  4708. 'https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/avatar.png'
  4709. };
  4710. // 发送socket消息
  4711. try {
  4712. if (this.myselfFlag) {
  4713. this.addToTalkList(data);
  4714. } else {
  4715. this.socket.send({
  4716. data: JSON.stringify(data),
  4717. success: () => {
  4718. this.value = '';
  4719. // 发送成功后强制滚动到底部,无论当前滚动位置如何
  4720. this.forceScrollToBottomOnSend();
  4721. },
  4722. fail: (err) => {
  4723. console.error('消息发送失败:', err);
  4724. if (retries > 0) {
  4725. uni.showToast({
  4726. title: `发送失败,正在重试(${retries}次)`,
  4727. icon: 'none'
  4728. });
  4729. setTimeout(() => this.sendMsg(retries - 1), 500);
  4730. } else {
  4731. uni.showToast({
  4732. title: '发送失败,请稍后再试',
  4733. icon: 'none'
  4734. });
  4735. this.value = text; // 恢复输入框内容
  4736. }
  4737. }
  4738. });
  4739. }
  4740. } catch (err) {
  4741. console.error('发送消息异常:', err);
  4742. if (retries > 0) {
  4743. setTimeout(() => this.sendMsg(retries - 1), 500);
  4744. } else {
  4745. uni.showToast({
  4746. title: '发送失败,请稍后再试',
  4747. icon: 'none'
  4748. });
  4749. this.value = text;
  4750. }
  4751. }
  4752. },
  4753. formatUserNum() {
  4754. let userTotal = 0;
  4755. if (!this.liveUserTotal) {
  4756. this.liveUserTotal = 0;
  4757. }
  4758. if (this.liveUserTotal > 0) {
  4759. userTotal = this.liveUserTotal = 0;
  4760. }
  4761. if (this.liveUserTotal > 10000) {
  4762. userTotal = (this.liveUserTotal / 10000.0).toFixed(1);
  4763. userTotal = userTotal + "万";
  4764. }
  4765. console.log("qxj userTotal", userTotal);
  4766. return userTotal;
  4767. },
  4768. }
  4769. };
  4770. </script>
  4771. <style scoped lang="scss">
  4772. /* 退出全屏按钮 */
  4773. .fullscreen-exit-btn {
  4774. position: fixed;
  4775. bottom: 40rpx;
  4776. left: 82vh;
  4777. display: flex;
  4778. flex-direction: column;
  4779. align-items: center;
  4780. justify-content: center;
  4781. z-index: 100001;
  4782. background: rgba(57, 57, 57, 0.6);
  4783. border-radius: 40rpx;
  4784. padding: 20rpx 24rpx;
  4785. transition: all 0.3s ease;
  4786. &:active {
  4787. transform: scale(0.95);
  4788. background: rgba(50, 50, 50, 0.6);
  4789. }
  4790. .exit-fullscreen-icon {
  4791. width: 40rpx;
  4792. height: 40rpx;
  4793. margin-bottom: 10rpx;
  4794. }
  4795. .exit-text {
  4796. color: #ffffff;
  4797. font-size: 22rpx;
  4798. white-space: nowrap;
  4799. }
  4800. }
  4801. .video-container.fullscreen-mode .fullscreen-exit-btn {
  4802. z-index: 999999 !important;
  4803. }
  4804. .fullscreen-mode {
  4805. background-color: #000 !important;
  4806. }
  4807. /* 全屏模式样式 */
  4808. .video-container.fullscreen-mode {
  4809. z-index: 99999 !important;
  4810. background-color: #000 !important;
  4811. transform: rotate(90deg) !important;
  4812. transform-origin: center center !important;
  4813. .video-player {
  4814. width: 100vh !important;
  4815. height: 100vw !important;
  4816. }
  4817. }
  4818. /* 全屏时隐藏其他元素 */
  4819. .video-container.fullscreen-mode~* {
  4820. display: none !important;
  4821. }
  4822. .video-container {
  4823. position: relative;
  4824. bottom: -1px;
  4825. transition: all 0.3s ease;
  4826. }
  4827. .video-container2 {
  4828. position: relative;
  4829. bottom: -1px;
  4830. transition: all 0.3s ease;
  4831. }
  4832. .video-player.fullscreen {
  4833. width: 100vh !important;
  4834. height: 100vw !important;
  4835. object-fit: contain !important;
  4836. }
  4837. /* 全屏按钮样式优化 */
  4838. .custom-controls {
  4839. position: absolute;
  4840. bottom: 10%;
  4841. right: 40rpx;
  4842. z-index: 9999;
  4843. background: rgba(0, 0, 0, 0.6);
  4844. border-radius: 50%;
  4845. width: 80rpx;
  4846. height: 80rpx;
  4847. display: flex;
  4848. align-items: center;
  4849. justify-content: center;
  4850. transition: all 0.3s ease;
  4851. &:active {
  4852. transform: scale(0.95);
  4853. background: rgba(0, 0, 0, 0.8);
  4854. }
  4855. }
  4856. .control-icon {
  4857. width: 46rpx;
  4858. height: 46rpx;
  4859. }
  4860. @media screen and (orientation: landscape) {
  4861. .video-container.horizontal-layout {
  4862. width: 100%;
  4863. height: 100%;
  4864. }
  4865. // .video-player {
  4866. // width: 100vh !important;
  4867. // height: 100vw !important;
  4868. // }
  4869. }
  4870. // 重置按钮样式
  4871. .button-reset {
  4872. background-color: transparent !important;
  4873. padding: 0 !important;
  4874. line-height: 1 !important;
  4875. margin: 0 !important;
  4876. width: auto !important;
  4877. font-weight: 500 !important;
  4878. border-radius: none !important;
  4879. &::after {
  4880. border: none !important;
  4881. padding: 0 !important;
  4882. margin: 0 !important;
  4883. }
  4884. }
  4885. // :deep(.u-tabs__wrapper__nav) {
  4886. // background-color: #fff !important;
  4887. // }
  4888. .swiper-wrapper {
  4889. /* 深色主题变量 */
  4890. --bottom-color: transparent;
  4891. --bg-color: #242424;
  4892. --text-color: #ffffff;
  4893. --name-color: #8CE7FF;
  4894. --normal-bg: rgba(0, 0, 0, 0.3);
  4895. --input-bg: rgba(0, 0, 0, 0.3);
  4896. --chat-bg: transparent;
  4897. --video-height: 100vh;
  4898. --chat-height: 30vh;
  4899. --pop-bg: #333333;
  4900. --notice-bg: rgba(9, 9, 9, 0.4);
  4901. }
  4902. .swiper-wrapper.horizontal {
  4903. --notice-bg: #fefae8;
  4904. --pop-bg: #ECF5F4;
  4905. /* 默认变量(浅色主题) */
  4906. --bottom-color: #fff;
  4907. --name-color: #666666;
  4908. --bg-color: #ffffff;
  4909. --text-color: #333333;
  4910. --normal-bg: #fff;
  4911. --input-bg: #F5F7FA;
  4912. --chat-bg: #ECF5F4; //对话聊天
  4913. --video-height: 500rpx;
  4914. --chat-height: clac(100% - 650rpx);
  4915. }
  4916. // 主容器
  4917. .swiper-wrapper {
  4918. position: relative;
  4919. width: 100%;
  4920. height: 100vh;
  4921. overflow: hidden;
  4922. /* 应用变量 */
  4923. background: var(--bg-color);
  4924. color: var(--text-color);
  4925. .container {
  4926. width: 100%;
  4927. height: 100%;
  4928. position: relative;
  4929. transition: opacity 0.3s ease;
  4930. transform: translateZ(0);
  4931. will-change: opacity;
  4932. // 预告直播盒子
  4933. .trailer-box {
  4934. width: calc(100% - 80rpx);
  4935. background: var(--pop-bg);
  4936. border-radius: 24rpx;
  4937. position: absolute;
  4938. top: 16%;
  4939. left: 50%;
  4940. transform: translateX(-50%);
  4941. display: flex;
  4942. flex-direction: column;
  4943. align-items: center;
  4944. color: var(--text-color);
  4945. padding: 20rpx;
  4946. z-index: 999;
  4947. box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.12);
  4948. transition: box-shadow 0.3s ease;
  4949. .trailer-video {
  4950. width: 100%;
  4951. height: 100%;
  4952. position: absolute;
  4953. z-index: 1;
  4954. border-radius: 20rpx;
  4955. }
  4956. .trailer-placeholder {
  4957. margin-bottom: 40rpx;
  4958. width: 240rpx;
  4959. height: 240rpx;
  4960. }
  4961. .countdown-container {
  4962. margin: 20rpx 0;
  4963. display: flex;
  4964. flex-direction: column;
  4965. align-items: center;
  4966. .live-name {
  4967. font-weight: 600;
  4968. font-size: 36rpx;
  4969. }
  4970. .countdown-display {
  4971. display: flex;
  4972. align-items: center;
  4973. margin: 30rpx 0;
  4974. .countdown-label {
  4975. font-size: 24rpx;
  4976. color: #999999;
  4977. }
  4978. .countdown-separator {
  4979. font-size: 24rpx;
  4980. color: #999999;
  4981. }
  4982. .countdown-unit {
  4983. width: 40rpx;
  4984. height: 40rpx;
  4985. background: #4D4D4D;
  4986. border-radius: 8rpx;
  4987. text-align: center;
  4988. overflow: hidden;
  4989. margin: 0 8rpx;
  4990. font-weight: 500;
  4991. font-size: 28rpx;
  4992. color: #FFFFFF;
  4993. line-height: 40rpx;
  4994. }
  4995. }
  4996. }
  4997. .trailer-actions {
  4998. display: flex;
  4999. justify-content: center;
  5000. align-items: center;
  5001. .action-button {
  5002. width: 280rpx;
  5003. height: 72rpx;
  5004. border-radius: 36rpx;
  5005. line-height: 72rpx;
  5006. text-align: center;
  5007. color: #fff;
  5008. &.reserve-button {
  5009. background: linear-gradient(136deg, #38D97D 0%, #02B176 100%);
  5010. }
  5011. &:not(.reserve-button) {
  5012. background: #F4A007;
  5013. }
  5014. .button-icon {
  5015. width: 32rpx;
  5016. height: 32rpx;
  5017. }
  5018. }
  5019. }
  5020. .no-live-title {
  5021. margin-top: 30rpx;
  5022. font-size: 42rpx;
  5023. font-weight: 500;
  5024. }
  5025. }
  5026. // 新预告直播盒子
  5027. .trailer-box2 {
  5028. width: 100%;
  5029. position: absolute;
  5030. top: 16%;
  5031. display: flex;
  5032. flex-direction: column;
  5033. align-items: center;
  5034. z-index: 999;
  5035. transition: box-shadow 0.3s ease;
  5036. height: 400rpx;
  5037. .trailer-video {
  5038. width: 100%;
  5039. height: 100%;
  5040. position: absolute;
  5041. z-index: 1;
  5042. }
  5043. .trailer-placeholder {
  5044. position: absolute;
  5045. top: 0;
  5046. left: 0;
  5047. width: 100%;
  5048. height: 100%;
  5049. z-index: -1;
  5050. /* 置于内容下方 */
  5051. }
  5052. .countdown-container {
  5053. padding-top: 15%;
  5054. margin: 20rpx 0;
  5055. display: flex;
  5056. flex-direction: column;
  5057. align-items: center;
  5058. .live-name {
  5059. font-weight: 600;
  5060. font-size: 36rpx;
  5061. }
  5062. .countdown-display {
  5063. display: flex;
  5064. align-items: center;
  5065. margin: 30rpx 0;
  5066. .countdown-label {
  5067. font-size: 32rpx;
  5068. color: rgba(250, 214, 176, 1);
  5069. }
  5070. .countdown-separator {
  5071. font-size: 24rpx;
  5072. color: #999999;
  5073. }
  5074. .countdown-unit {
  5075. width: 60rpx;
  5076. height: 60rpx;
  5077. background: rgba(255, 246, 214, 1);
  5078. border-radius: 8rpx;
  5079. text-align: center;
  5080. overflow: hidden;
  5081. margin: 0 8rpx;
  5082. font-weight: 500;
  5083. font-size: 36rpx;
  5084. color: rgba(0, 75, 59, 1);
  5085. line-height: 60rpx;
  5086. }
  5087. }
  5088. }
  5089. .trailer-actions {
  5090. display: flex;
  5091. justify-content: center;
  5092. align-items: center;
  5093. .action-button {
  5094. width: 280rpx;
  5095. height: 72rpx;
  5096. border-radius: 36rpx;
  5097. display: flex;
  5098. justify-content: center;
  5099. align-items: center;
  5100. text-align: center;
  5101. border: 1px solid rgba(151, 151, 151, 1);
  5102. font-size: 36rpx;
  5103. font-weight: bold;
  5104. &.reserve-button {
  5105. color: rgba(0, 110, 80, 1);
  5106. background: linear-gradient(231.51deg, rgba(249, 215, 178, 1) 0%, rgba(255, 255, 191, 1) 47.83%, rgba(255, 250, 229, 1) 65.77%, rgba(248, 214, 178, 1) 100%);
  5107. }
  5108. &:not(.reserve-button) {
  5109. color: rgba(255, 246, 214, 1);
  5110. background: rgba(0, 110, 80, 1);
  5111. }
  5112. .button-icon {
  5113. width: 32rpx;
  5114. height: 32rpx;
  5115. }
  5116. }
  5117. }
  5118. .no-live-title {
  5119. margin-top: 30rpx;
  5120. font-size: 42rpx;
  5121. font-weight: 500;
  5122. }
  5123. }
  5124. // 主要内容区域
  5125. .content {
  5126. position: relative;
  5127. z-index: 2;
  5128. height: 100%;
  5129. width: 100%;
  5130. top: 0;
  5131. left: 0;
  5132. display: flex;
  5133. flex-direction: column;
  5134. &.horizontal-content {
  5135. z-index: 999;
  5136. }
  5137. &.trailer-content {
  5138. justify-content: space-between;
  5139. }
  5140. // 顶部信息栏
  5141. .top-info-bar {
  5142. position: fixed;
  5143. top: 0;
  5144. z-index: 5;
  5145. width: 100%;
  5146. padding: 0 24rpx;
  5147. box-sizing: border-box;
  5148. // margin-top:calc(var(--status-bar-height) + 10rpx);
  5149. display: flex;
  5150. justify-content: space-between;
  5151. flex-direction: column;
  5152. &.horizontal-top {
  5153. position: static;
  5154. margin-bottom: 30rpx;
  5155. }
  5156. .user-info-section {
  5157. display: flex;
  5158. align-items: center;
  5159. .back-icon {
  5160. width: 60rpx;
  5161. height: 64rpx;
  5162. }
  5163. .user-avatar-container {
  5164. padding: 6rpx 8rpx;
  5165. height: 64rpx;
  5166. background: var(--normal-bg);
  5167. border-radius: 32rpx;
  5168. display: flex;
  5169. align-items: center;
  5170. .user-name {
  5171. color: var(--text-color);
  5172. }
  5173. }
  5174. }
  5175. .viewers-section {
  5176. display: flex;
  5177. justify-content: flex-end;
  5178. align-items: center;
  5179. margin-top: 30rpx;
  5180. .viewer-avatar {
  5181. width: 52rpx;
  5182. height: 52rpx;
  5183. border-radius: 26rpx;
  5184. }
  5185. .viewer-avatar-placeholder {
  5186. width: 52rpx;
  5187. height: 52rpx;
  5188. border-radius: 50%;
  5189. display: flex;
  5190. align-items: center;
  5191. justify-content: center;
  5192. .avatar-initial {
  5193. color: var(--text-color);
  5194. font-size: 18rpx;
  5195. }
  5196. }
  5197. .viewer-count {
  5198. width: 80rpx;
  5199. height: 52rpx;
  5200. background: var(--normal-bg);
  5201. border-radius: 26rpx;
  5202. font-size: 30rpx;
  5203. color: var(--text-color);
  5204. text-align: center;
  5205. line-height: 52rpx;
  5206. }
  5207. }
  5208. }
  5209. // 购买提示
  5210. .purchase-prompt {
  5211. position: absolute;
  5212. bottom: 750rpx;
  5213. left: 24rpx;
  5214. padding: 6rpx 20rpx;
  5215. background: rgba(230, 154, 34, 0.7);
  5216. border-radius: 24rpx;
  5217. z-index: 9;
  5218. font-weight: 500;
  5219. color: #ffffff;
  5220. transition: opacity 0.3s ease;
  5221. font-size: 30rpx;
  5222. display: flex;
  5223. align-items: center;
  5224. .prompt-icon {
  5225. width: 32rpx;
  5226. height: 32rpx;
  5227. }
  5228. }
  5229. // 视频区域
  5230. .videolist {
  5231. position: relative;
  5232. // height: var(--video-height);
  5233. width: 100%;
  5234. &.screen {
  5235. width: 100%;
  5236. height: 100%;
  5237. //height: 100vh;
  5238. }
  5239. .video-container {
  5240. //height: var(--video-height);
  5241. width: 100%;
  5242. transform: translateZ(0);
  5243. backface-visibility: hidden;
  5244. perspective: 1000;
  5245. &.horizontal-layout {
  5246. // position: absolute;
  5247. // top: 20%;
  5248. // max-height: 450rpx;
  5249. // z-index: 99;
  5250. }
  5251. .complaint-box {
  5252. position: absolute;
  5253. // top: 20%;
  5254. right: 20rpx;
  5255. width: 140rpx;
  5256. margin-top: 20rpx;
  5257. display: flex;
  5258. align-items: center;
  5259. justify-content: center;
  5260. background-color: rgba(57, 57, 57, 0.6);
  5261. padding: 16rpx 0;
  5262. color: #fff;
  5263. border-radius: 28rpx;
  5264. z-index: 999999 !important;
  5265. &.complaint-full {
  5266. // top: 10rpx;
  5267. left: 82vh;
  5268. }
  5269. &:active {
  5270. transform: scale(0.95);
  5271. background: rgba(50, 50, 50, 0.6);
  5272. }
  5273. }
  5274. .txt {
  5275. text-align: center;
  5276. font-weight: 500;
  5277. font-size: 40rpx;
  5278. line-height: var(--video-height);
  5279. }
  5280. .video-player {
  5281. width: 100%;
  5282. height: 100%;
  5283. position: absolute;
  5284. z-index: 1;
  5285. }
  5286. .live-end-message {
  5287. position: absolute;
  5288. top: 50%;
  5289. left: 50%;
  5290. transform: translate(-50%, -50%);
  5291. font-size: 36rpx;
  5292. color: var(--text-color);
  5293. }
  5294. .replay-label {
  5295. position: absolute;
  5296. // top: 10%;
  5297. right: 24rpx;
  5298. background-color: rgba(57, 57, 57, 0.6);
  5299. width: 100rpx;
  5300. height: 70rpx;
  5301. line-height: 70rpx;
  5302. text-align: center;
  5303. color: #fff;
  5304. border-radius: 15rpx;
  5305. z-index: 999999 !important;
  5306. &.replay-full {
  5307. left: 82vh;
  5308. // top: 16% !important;
  5309. }
  5310. }
  5311. }
  5312. }
  5313. }
  5314. .progress-countdown {
  5315. position: absolute;
  5316. right: 20rpx;
  5317. top: calc(60% - 128rpx);
  5318. z-index: 9000;
  5319. width: 180rpx;
  5320. background: rgba(0, 0, 0, 0.3);
  5321. border-radius: 16rpx;
  5322. padding: 16rpx;
  5323. flex-direction: column;
  5324. &.progress-vertical {
  5325. top: 15%;
  5326. left: 24rpx;
  5327. }
  5328. .title {
  5329. width: 148rpx;
  5330. height: 28rpx;
  5331. }
  5332. .progress-bar-bg {
  5333. width: 148rpx;
  5334. height: 8rpx;
  5335. margin-top: 16rpx;
  5336. background: #FFFFFF;
  5337. border-radius: 6rpx 6rpx 6rpx 6rpx;
  5338. overflow: hidden;
  5339. margin-bottom: 16rpx;
  5340. .progress-bar-fill {
  5341. height: 100%;
  5342. // background: linear-gradient(90deg, #4CAF50, #8BC34A);
  5343. background: #FACE15;
  5344. border-radius: 6rpx 6rpx 6rpx 6rpx;
  5345. transition: width 1s linear;
  5346. }
  5347. }
  5348. .progress-text {
  5349. color: #fff;
  5350. font-size: 20rpx;
  5351. }
  5352. }
  5353. // 侧边
  5354. .slide-group {
  5355. position: absolute;
  5356. right: 20rpx;
  5357. top: 60%;
  5358. z-index: 9000;
  5359. .action-button-group {
  5360. display: flex;
  5361. justify-content: space-between;
  5362. align-items: center;
  5363. margin-top: 50rpx;
  5364. }
  5365. .end {
  5366. justify-content: flex-end;
  5367. }
  5368. .action-icon {
  5369. width: 88rpx;
  5370. height: 88rpx;
  5371. }
  5372. .icon-button {
  5373. // background: var(--normal-bg);
  5374. // border-radius: 50%;
  5375. width: 88rpx;
  5376. height: 88rpx;
  5377. display: flex;
  5378. justify-content: center;
  5379. align-items: center;
  5380. transition: transform 0.2s ease;
  5381. // margin-left: 10rpx;
  5382. &.like-container {
  5383. display: flex;
  5384. flex-direction: column;
  5385. justify-content: center;
  5386. align-items: center;
  5387. position: relative;
  5388. .like-count {
  5389. position: absolute;
  5390. bottom: -40rpx;
  5391. font-size: 24rpx;
  5392. color: #FA341E;
  5393. padding: 6rpx 16rpx;
  5394. background: #FFFFFF;
  5395. border-radius: 30rpx;
  5396. }
  5397. }
  5398. }
  5399. }
  5400. // 聊天区域
  5401. .chat-area-container {
  5402. background: var(--chat-bg);
  5403. position: fixed;
  5404. width: 100%;
  5405. bottom: 0;
  5406. z-index: 999;
  5407. transform: translateZ(0);
  5408. will-change: transform;
  5409. backface-visibility: hidden;
  5410. &.chat-area-container2 {
  5411. flex: 1;
  5412. min-height: 0; // 重要:允许收缩到0
  5413. position: relative;
  5414. transform: none; // 移除之前的变换
  5415. will-change: auto;
  5416. display: flex;
  5417. flex-direction: column;
  5418. }
  5419. &.chat-area-container3 {
  5420. height: calc(85% - 420rpx);
  5421. flex: 1;
  5422. transform: none; // 移除之前的变换
  5423. will-change: auto;
  5424. display: flex;
  5425. flex-direction: column;
  5426. position: absolute;
  5427. }
  5428. &.chat-area-container4 {
  5429. height: calc(85% - 680rpx);
  5430. flex: 1;
  5431. transform: none; // 移除之前的变换
  5432. will-change: auto;
  5433. display: flex;
  5434. flex-direction: column;
  5435. position: absolute;
  5436. // max-height: 40%;
  5437. // flex: 1;
  5438. // min-height: 0; // 重要:允许收缩到0
  5439. // // position: relative;
  5440. // transform: none; // 移除之前的变换
  5441. // will-change: auto;
  5442. // display: flex;
  5443. // flex-direction: column;
  5444. // position: absolute;
  5445. }
  5446. &.chat-area-container5 {
  5447. height: calc(85% - 450rpx);
  5448. flex: 1;
  5449. transform: none; // 移除之前的变换
  5450. will-change: auto;
  5451. display: flex;
  5452. flex-direction: column;
  5453. position: absolute;
  5454. // max-height: 40%;
  5455. // flex: 1;
  5456. // min-height: 0; // 重要:允许收缩到0
  5457. // // position: relative;
  5458. // transform: none; // 移除之前的变换
  5459. // will-change: auto;
  5460. // display: flex;
  5461. // flex-direction: column;
  5462. // position: absolute;
  5463. }
  5464. &.chat-area-focused {
  5465. transform: translateY(calc(-1 * var(--keyboard-height, 0rpx))) translateZ(0);
  5466. z-index: 1000;
  5467. }
  5468. .tabs_bg {
  5469. background-color: #fff;
  5470. padding: 20rpx 40rpx 0;
  5471. display: flex;
  5472. justify-content: space-between;
  5473. align-items: center;
  5474. .complaint-box {
  5475. width: 120rpx;
  5476. height: 48rpx;
  5477. background: #FFFFFF;
  5478. border-radius: 24rpx 24rpx 24rpx 24rpx;
  5479. border: 2rpx solid #EEEEEE;
  5480. display: flex;
  5481. justify-content: center;
  5482. align-items: center;
  5483. }
  5484. }
  5485. .chat-content-wrapper {
  5486. flex: 1;
  5487. height: 400rpx;
  5488. padding: 20rpx 0;
  5489. transform: translateZ(0);
  5490. will-change: height;
  5491. position: relative;
  5492. &.chat-content-focused {
  5493. height: 100rpx !important;
  5494. }
  5495. .notice-message {
  5496. font-size: 36rpx;
  5497. position: absolute;
  5498. bottom: 388rpx;
  5499. max-width: 80%;
  5500. padding: 24rpx;
  5501. background-color: var(--notice-bg);
  5502. margin: 0 0 20rpx 20rpx;
  5503. border-radius: 20rpx;
  5504. color: #0d0b05;
  5505. font-weight: 500;
  5506. overflow: hidden;
  5507. &.horizontal-notice {
  5508. top: 12rpx;
  5509. height: fit-content;
  5510. z-index: 2;
  5511. }
  5512. &.horizontal-notice2 {
  5513. color: #fff !important;
  5514. top: 12rpx;
  5515. height: fit-content;
  5516. z-index: 2;
  5517. }
  5518. }
  5519. .message-scroll-view {
  5520. width: calc(100% - 20rpx);
  5521. height: 100%;
  5522. padding-left: 20rpx;
  5523. display: flex;
  5524. flex: 1;
  5525. flex-direction: column;
  5526. -webkit-overflow-scrolling: touch;
  5527. &.message-scroll2 {
  5528. height: 300rpx;
  5529. }
  5530. }
  5531. .message-list {
  5532. width: 80%;
  5533. margin-bottom: 16rpx;
  5534. animation: simpleFade 0.2s;
  5535. .message-item {
  5536. display: flex;
  5537. justify-content: flex-start;
  5538. .message-content {
  5539. font-size: 30rpx;
  5540. max-width: 100%;
  5541. border-radius: 30rpx;
  5542. background: var(--normal-bg);
  5543. padding: 10rpx 30rpx;
  5544. line-height: 1.4;
  5545. word-break: break-all;
  5546. word-wrap: break-word;
  5547. overflow-wrap: break-word;
  5548. .user-nickname {
  5549. color: var(--name-color);
  5550. }
  5551. .message-text {
  5552. color: var(--text-color);
  5553. }
  5554. }
  5555. }
  5556. }
  5557. }
  5558. // 输入和操作区域
  5559. .input-actions-container {
  5560. position: relative;
  5561. z-index: 999999;
  5562. background-color: var(--bottom-color);
  5563. display: flex;
  5564. justify-content: space-between;
  5565. align-items: center;
  5566. padding: 24rpx;
  5567. &.input-actions-focused {
  5568. justify-content: flex-start !important;
  5569. align-items: stretch !important;
  5570. width: 100% !important;
  5571. display: flex !important;
  5572. }
  5573. .input-container {
  5574. background: var(--input-bg);
  5575. box-sizing: border-box;
  5576. border-radius: 36rpx;
  5577. display: flex;
  5578. align-items: center;
  5579. transform: translateZ(0);
  5580. will-change: transform;
  5581. backface-visibility: hidden;
  5582. perspective: 1000px;
  5583. &.input-container-normal {
  5584. padding: 18rpx 20rpx;
  5585. width: auto;
  5586. max-width: auto;
  5587. margin-right: 0;
  5588. justify-content: space-between;
  5589. }
  5590. &.input-container-focused {
  5591. padding: 10rpx 10rpx 10rpx 20rpx;
  5592. width: 100%;
  5593. max-width: calc(100% - 20rpx);
  5594. margin-right: 20rpx;
  5595. // flex: 1;
  5596. justify-content: flex-start;
  5597. }
  5598. .chat-input {
  5599. border: none !important;
  5600. font-size: 32rpx !important;
  5601. color: var(--text-color) !important;
  5602. background: transparent !important;
  5603. width: 70% !important;
  5604. transform: translateZ(0);
  5605. will-change: contents, width;
  5606. backface-visibility: hidden;
  5607. margin-left: 20rpx;
  5608. &.input-focused {
  5609. width: 100% !important;
  5610. flex: 1 !important;
  5611. min-width: 0 !important;
  5612. margin-left: 10rpx !important;
  5613. max-width: none !important;
  5614. }
  5615. }
  5616. .send-button {
  5617. background-color: #ffffff;
  5618. border-radius: 28rpx;
  5619. padding: 14rpx 16rpx;
  5620. color: #181818;
  5621. font-weight: 500;
  5622. min-width: 80rpx;
  5623. text-align: center;
  5624. flex-shrink: 0;
  5625. &.send2 {
  5626. background-color: #2fa9f1;
  5627. color: #ffffff;
  5628. }
  5629. }
  5630. }
  5631. .action-buttons {
  5632. display: flex;
  5633. justify-content: space-between;
  5634. align-items: center;
  5635. flex: auto;
  5636. transform: translateZ(0);
  5637. justify-content: flex-end;
  5638. &.action-buttons-hidden {
  5639. opacity: 0;
  5640. visibility: hidden;
  5641. }
  5642. .action-icon {
  5643. width: 48rpx;
  5644. height: 48rpx;
  5645. }
  5646. .cart-icon {
  5647. width: 58rpx;
  5648. height: 58rpx;
  5649. }
  5650. }
  5651. }
  5652. }
  5653. // 商品卡片
  5654. .goods-card {
  5655. position: fixed;
  5656. bottom: 140rpx;
  5657. right: 24rpx;
  5658. z-index: 5;
  5659. background-color: #fff;
  5660. border-radius: 20rpx;
  5661. overflow: hidden;
  5662. width: 224rpx;
  5663. padding: 4rpx;
  5664. .goods-card-header {
  5665. position: absolute;
  5666. top: 4rpx;
  5667. display: flex;
  5668. justify-content: space-between;
  5669. align-items: center;
  5670. color: #fff;
  5671. width: 100%;
  5672. padding-right: 10rpx;
  5673. box-sizing: border-box;
  5674. .goods-status {
  5675. background: rgba(0, 0, 0, 0.4);
  5676. padding: 12rpx;
  5677. font-size: 22rpx;
  5678. display: flex;
  5679. justify-content: space-between;
  5680. align-items: center;
  5681. border-radius: 10rpx;
  5682. .status-icon {
  5683. width: 30rpx;
  5684. height: 30rpx;
  5685. }
  5686. }
  5687. .close-icon {
  5688. width: 24rpx;
  5689. height: 24rpx;
  5690. }
  5691. }
  5692. .goods-image {
  5693. width: 100%;
  5694. height: 224rpx;
  5695. border-radius: 16rpx 16rpx 0 0;
  5696. overflow: hidden;
  5697. }
  5698. .goods-info {
  5699. padding: 4rpx;
  5700. .goods-title {
  5701. font-weight: 500;
  5702. font-size: 24rpx;
  5703. margin: 10rpx 0 12rpx;
  5704. color: #111111;
  5705. }
  5706. .goods-action {
  5707. color: #fff;
  5708. text-align: center;
  5709. font-weight: 500;
  5710. font-size: 30rpx;
  5711. position: relative;
  5712. .goods-price {
  5713. left: 10rpx;
  5714. position: absolute;
  5715. top: 40%;
  5716. transform: translateY(-50%);
  5717. font-size: 30rpx;
  5718. z-index: 999;
  5719. .price-symbol {
  5720. font-weight: 600;
  5721. font-size: 20rpx;
  5722. color: #FFFFFF;
  5723. }
  5724. .price-integer {
  5725. font-size: 36rpx;
  5726. margin-right: 10rpx;
  5727. }
  5728. }
  5729. .shop-icon {
  5730. width: 204rpx;
  5731. height: 52rpx;
  5732. }
  5733. }
  5734. }
  5735. }
  5736. // 动画
  5737. @keyframes simpleFade {
  5738. from {
  5739. opacity: 0;
  5740. }
  5741. to {
  5742. opacity: 1;
  5743. }
  5744. }
  5745. .userlogo {
  5746. padding: 36rpx 40rpx;
  5747. color: #000000;
  5748. .mt42 {
  5749. margin-top: 42rpx;
  5750. }
  5751. .boxweixin {
  5752. width: 44rpx;
  5753. height: 44rpx;
  5754. border-radius: 50%;
  5755. text-align: center;
  5756. line-height: 34rpx;
  5757. color: #0a0;
  5758. }
  5759. .button-container {
  5760. position: relative;
  5761. margin: auto;
  5762. .hidden-input {
  5763. width: calc(100vw - 80rpx);
  5764. text-align: center;
  5765. height: 104rpx;
  5766. background: #07C160;
  5767. padding: 30rpx 0;
  5768. box-sizing: border-box;
  5769. border-radius: 12rpx;
  5770. font-size: 32rpx !important;
  5771. color: #FFFFFF !important;
  5772. /* 输入文字大小 */
  5773. }
  5774. /* 针对 placeholder 的样式 */
  5775. .hidden-input::placeholder {
  5776. font-size: 44rpx !important;
  5777. color: #FFFFFF !important;
  5778. }
  5779. /* 兼容微信小程序的 placeholder 样式 */
  5780. .hidden-input .placeholder {
  5781. font-size: 44rpx !important;
  5782. color: #FFFFFF !important;
  5783. }
  5784. }
  5785. .submitname {
  5786. height: 104rpx;
  5787. line-height: 104rpx;
  5788. background: #F2F2F2;
  5789. border-radius: 12rpx;
  5790. font-size: 32rpx;
  5791. color: #07C160;
  5792. margin-top: 32rpx;
  5793. text-align: center;
  5794. }
  5795. }
  5796. // 抽奖弹窗1
  5797. .lottery-popup {
  5798. position: relative;
  5799. border-radius: 40rpx;
  5800. height: fit-content;
  5801. .lottery-header-image {
  5802. width: 311rpx;
  5803. position: absolute;
  5804. top: -122rpx;
  5805. left: 50%;
  5806. transform: translateX(-50%);
  5807. }
  5808. .lottery-background-image {
  5809. position: absolute;
  5810. top: 0;
  5811. left: 0;
  5812. width: 100%;
  5813. height: 100%;
  5814. z-index: 1;
  5815. }
  5816. .lottery-content {
  5817. position: relative;
  5818. z-index: 2;
  5819. padding: 24rpx 0 68rpx;
  5820. .lottery-close-section {
  5821. display: flex;
  5822. justify-content: flex-end;
  5823. margin-right: 20rpx;
  5824. .close-button-wrapper {
  5825. .close-button {
  5826. width: 80rpx;
  5827. height: 80rpx;
  5828. display: flex;
  5829. justify-content: center;
  5830. align-items: center;
  5831. }
  5832. }
  5833. }
  5834. .lottery-main-content {
  5835. display: flex;
  5836. flex-direction: column;
  5837. align-items: center;
  5838. .lottery-title-image {
  5839. width: 446rpx;
  5840. height: 80rpx;
  5841. }
  5842. .lottery-countdown-section {
  5843. font-size: 24rpx;
  5844. color: #fff;
  5845. display: flex;
  5846. align-items: center;
  5847. margin: 30rpx 0;
  5848. .countdown-label {
  5849. margin-right: 10rpx;
  5850. }
  5851. .countdown-timer {
  5852. display: flex;
  5853. align-items: center;
  5854. .countdown-number {
  5855. width: 40rpx;
  5856. height: 40rpx;
  5857. text-align: center;
  5858. overflow: hidden;
  5859. background: #ffffff;
  5860. box-shadow: inset 0rpx 2rpx 8rpx 0rpx #ffebb2;
  5861. border-radius: 8rpx;
  5862. margin: 4rpx;
  5863. font-weight: 600;
  5864. font-size: 24rpx;
  5865. color: #f85d22;
  5866. line-height: 40rpx;
  5867. }
  5868. }
  5869. }
  5870. .lottery-products-section {
  5871. width: 100%;
  5872. }
  5873. .lottery-indicators {
  5874. margin: 20rpx 0 50rpx;
  5875. display: flex;
  5876. gap: 6rpx;
  5877. .indicator-point {
  5878. width: 20rpx;
  5879. height: 8rpx;
  5880. background: rgba(255, 255, 255, 0.5);
  5881. border-radius: 4rpx;
  5882. &.selected {
  5883. background: #ffeb66;
  5884. }
  5885. }
  5886. }
  5887. .lottery-tip {
  5888. color: #fff;
  5889. font-size: 28rpx;
  5890. }
  5891. .lottery-action-button {
  5892. margin-top: 30rpx;
  5893. width: 520rpx;
  5894. height: 88rpx;
  5895. line-height: 88rpx;
  5896. background: linear-gradient(180deg, #fff4d6 0%, #ffeb66 100%);
  5897. box-shadow: 0rpx 10rpx 8rpx 4rpx rgba(246, 82, 25, 0.5);
  5898. border-radius: 44rpx;
  5899. font-weight: 500;
  5900. font-size: 36rpx;
  5901. color: #f4410b;
  5902. text-align: center;
  5903. }
  5904. }
  5905. }
  5906. }
  5907. // 积分弹窗
  5908. .integral-popup {
  5909. min-width: 400rpx;
  5910. max-width: 600rpx;
  5911. display: flex;
  5912. flex-direction: column;
  5913. align-items: center;
  5914. border-radius: 20rpx;
  5915. overflow: hidden;
  5916. .integral-header {
  5917. width: 100%;
  5918. position: relative;
  5919. .integral-title {
  5920. width: 100%;
  5921. font-weight: 600;
  5922. font-size: 40rpx;
  5923. color: #ffffff;
  5924. text-shadow: 0px 4rpx 8rpx rgba(255, 89, 2, 0.8);
  5925. position: absolute;
  5926. top: 50rpx;
  5927. text-align: center;
  5928. }
  5929. .integral-background-image {
  5930. width: 100%;
  5931. }
  5932. }
  5933. .integral-content {
  5934. padding: 20rpx;
  5935. .integral-message {
  5936. font-weight: 500;
  5937. font-size: 32rpx;
  5938. text-align: center;
  5939. }
  5940. .integral-confirm-button {
  5941. font-size: 32rpx;
  5942. margin-top: 20rpx;
  5943. background: linear-gradient(270deg, #ff4702 0%, #fe6304 100%);
  5944. color: #fff;
  5945. text-align: center;
  5946. padding: 18rpx 60rpx;
  5947. border-radius: 10rpx;
  5948. font-weight: 500;
  5949. }
  5950. }
  5951. }
  5952. // 红包弹窗
  5953. .red-envelope-popup {
  5954. width: 504rpx;
  5955. position: relative;
  5956. text-align: center;
  5957. height: 610rpx;
  5958. display: flex;
  5959. align-items: center;
  5960. .red-envelope-close-button {
  5961. position: absolute;
  5962. right: 0;
  5963. top: -180rpx;
  5964. width: 64rpx;
  5965. height: 64rpx;
  5966. }
  5967. .red-envelope-background {
  5968. width: 100%;
  5969. height: 100%;
  5970. }
  5971. .red-envelope-content {
  5972. position: absolute;
  5973. top: 0;
  5974. left: 50%;
  5975. transform: translateX(-50%);
  5976. display: flex;
  5977. flex-direction: column;
  5978. align-items: center;
  5979. height: auto;
  5980. .red-envelope-header {
  5981. margin-top: 68rpx;
  5982. display: flex;
  5983. align-items: center;
  5984. .header-decoration-left,
  5985. .header-decoration-right {
  5986. width: 44rpx;
  5987. height: 20rpx;
  5988. }
  5989. .header-title {
  5990. margin: 0 12rpx;
  5991. font-size: 28rpx;
  5992. color: #FFF1BC;
  5993. }
  5994. }
  5995. .red-envelope-text {
  5996. font-weight: 600;
  5997. font-size: 80rpx;
  5998. color: #FFF1BC;
  5999. margin-top: 42rpx;
  6000. }
  6001. .red-envelope-open-text {
  6002. font-weight: 800;
  6003. font-size: 110rpx;
  6004. color: #F41420;
  6005. margin-top: 70rpx;
  6006. }
  6007. }
  6008. }
  6009. // 抽奖弹窗2
  6010. .lottery-popup-v2 {
  6011. width: 504rpx;
  6012. position: relative;
  6013. text-align: center;
  6014. height: 556rpx;
  6015. display: flex;
  6016. align-items: center;
  6017. .lottery-close-button-v2 {
  6018. position: absolute;
  6019. right: 0;
  6020. top: -180rpx;
  6021. width: 64rpx;
  6022. height: 64rpx;
  6023. z-index: 9;
  6024. }
  6025. .lottery-top-image {
  6026. position: absolute;
  6027. top: -214rpx;
  6028. width: 100%;
  6029. }
  6030. .lottery-background-v2 {
  6031. width: 100%;
  6032. height: 100%;
  6033. }
  6034. .lottery-content-v2 {
  6035. position: absolute;
  6036. top: 0;
  6037. left: 50%;
  6038. transform: translateX(-50%);
  6039. display: flex;
  6040. flex-direction: column;
  6041. align-items: center;
  6042. height: auto;
  6043. .lottery-header-v2 {
  6044. margin-top: 68rpx;
  6045. display: flex;
  6046. align-items: center;
  6047. .lottery-header-decoration-left,
  6048. .lottery-header-decoration-right {
  6049. width: 44rpx;
  6050. height: 20rpx;
  6051. }
  6052. .lottery-header-title {
  6053. margin: 0 12rpx;
  6054. font-size: 28rpx;
  6055. color: #FFF1BC;
  6056. margin-top: 22rpx;
  6057. }
  6058. }
  6059. .lottery-text-v2 {
  6060. font-weight: 600;
  6061. font-size: 80rpx;
  6062. color: #FFF1BC;
  6063. &:last-of-type {
  6064. margin-bottom: 56rpx;
  6065. }
  6066. }
  6067. .lottery-action-button-v2 {
  6068. width: 424rpx;
  6069. height: 96rpx;
  6070. line-height: 96rpx;
  6071. background: linear-gradient(180deg, #FBF1CA 0%, #FFD684 100%);
  6072. border-radius: 48rpx;
  6073. font-weight: 600;
  6074. font-size: 36rpx;
  6075. color: #F12E2C;
  6076. margin-bottom: 52rpx;
  6077. }
  6078. }
  6079. }
  6080. // 红包卡片弹窗
  6081. .red-card-popup {
  6082. width: 550rpx;
  6083. height: 636rpx;
  6084. position: relative;
  6085. .red-card-image {
  6086. position: absolute;
  6087. width: 100%;
  6088. height: 100%;
  6089. }
  6090. .red-card-content {
  6091. position: relative;
  6092. z-index: 5;
  6093. display: flex;
  6094. flex-direction: column;
  6095. align-items: center;
  6096. .red-card-message {
  6097. font-size: 36rpx;
  6098. color: #ff3a1e;
  6099. margin: 180rpx 0 90rpx;
  6100. }
  6101. .red-card-tip {
  6102. font-size: 28rpx;
  6103. color: #ffecc3;
  6104. margin: 80rpx 0 40rpx;
  6105. }
  6106. .red-card-confirm-button {
  6107. width: 392rpx;
  6108. height: 96rpx;
  6109. line-height: 96rpx;
  6110. background: linear-gradient(180deg, #fff4d5 0%, #ffe5b1 100%);
  6111. border-radius: 48rpx;
  6112. font-weight: 600;
  6113. font-size: 36rpx;
  6114. color: #c32008;
  6115. text-align: center;
  6116. }
  6117. }
  6118. }
  6119. // 消息弹窗
  6120. .message-popup {
  6121. display: flex;
  6122. flex-direction: column;
  6123. position: relative;
  6124. align-items: center;
  6125. padding: 126rpx 48rpx 0;
  6126. width: 554rpx;
  6127. box-sizing: border-box;
  6128. background: linear-gradient(180deg, #E2FCEF 0%, rgba(226, 252, 239, 0) 100%);
  6129. border-radius: 62rpx 62rpx 0rpx 0rpx;
  6130. .message-close-icon {
  6131. position: absolute;
  6132. z-index: 9;
  6133. top: -74rpx;
  6134. width: 176rpx;
  6135. height: 176rpx;
  6136. }
  6137. .message-title {
  6138. font-weight: 600;
  6139. font-size: 36rpx;
  6140. color: #333333;
  6141. margin: 24rpx 0 28rpx;
  6142. }
  6143. .message-content {
  6144. margin-bottom: 52rpx;
  6145. font-weight: 400;
  6146. font-size: 28rpx;
  6147. color: #333333;
  6148. }
  6149. .message-confirm-button {
  6150. width: 358rpx;
  6151. height: 88rpx;
  6152. line-height: 88rpx;
  6153. background: linear-gradient(136deg, #38D97D 0%, #02B176 100%);
  6154. border-radius: 44rpx 44rpx 44rpx 44rpx;
  6155. font-weight: 600;
  6156. font-size: 32rpx;
  6157. text-align: center;
  6158. color: #FFFFFF;
  6159. }
  6160. .close {
  6161. position: absolute;
  6162. z-index: 9;
  6163. bottom: -168rpx;
  6164. width: 64rpx;
  6165. height: 64rpx;
  6166. }
  6167. }
  6168. // 中奖弹窗
  6169. .prize-popup {
  6170. width: 504rpx;
  6171. padding: 40rpx 40rpx 30rpx;
  6172. box-sizing: border-box;
  6173. display: flex;
  6174. flex-direction: column;
  6175. border-radius: 20rpx;
  6176. align-items: center;
  6177. background: linear-gradient(180deg, #f7823f 0%, #ffd4be 27%, #ffffff 100%);
  6178. position: relative;
  6179. .prize-header-image {
  6180. width: 311rpx;
  6181. position: absolute;
  6182. top: -122rpx;
  6183. left: 50%;
  6184. transform: translateX(-50%);
  6185. }
  6186. .prize-title {
  6187. color: #c32008;
  6188. font-size: 34rpx;
  6189. font-weight: 600;
  6190. margin: 20rpx 0 40rpx;
  6191. }
  6192. .prize-info-section {
  6193. width: 100%;
  6194. display: flex;
  6195. justify-content: space-between;
  6196. align-items: center;
  6197. font-size: 28rpx;
  6198. margin: 10rpx 0;
  6199. .prize-level {
  6200. font-weight: 600;
  6201. }
  6202. }
  6203. .prize-tip {
  6204. font-size: 28rpx;
  6205. color: #414141;
  6206. margin: 40rpx 0;
  6207. }
  6208. .prize-action-button {
  6209. width: 200rpx;
  6210. height: 70rpx;
  6211. line-height: 70rpx;
  6212. background: linear-gradient(180deg, #ff7c30 0%, #ff3a1e 100%);
  6213. border-radius: 28rpx;
  6214. font-weight: 500;
  6215. font-size: 32rpx;
  6216. color: #ffffff;
  6217. text-align: center;
  6218. }
  6219. }
  6220. // 未中奖弹窗
  6221. .no-prize-popup {
  6222. width: 504rpx;
  6223. padding: 40rpx 40rpx 30rpx;
  6224. box-sizing: border-box;
  6225. display: flex;
  6226. flex-direction: column;
  6227. border-radius: 20rpx;
  6228. align-items: center;
  6229. position: relative;
  6230. .no-prize-image {
  6231. margin-top: 40rpx;
  6232. width: 300rpx;
  6233. height: 300rpx;
  6234. }
  6235. .no-prize-tip {
  6236. font-size: 36rpx;
  6237. color: #414141;
  6238. margin: 40rpx 0;
  6239. }
  6240. .no-prize-confirm-button {
  6241. width: 220rpx;
  6242. height: 80rpx;
  6243. line-height: 80rpx;
  6244. background: linear-gradient(180deg, #fdfbb8 0%, #b79243 100%);
  6245. border-radius: 28rpx;
  6246. font-weight: 500;
  6247. font-size: 32rpx;
  6248. color: #ffffff;
  6249. text-align: center;
  6250. }
  6251. }
  6252. // 中奖记录弹窗
  6253. .winning-record-popup {
  6254. position: relative;
  6255. height: 800rpx;
  6256. .winning-record-header-bg {
  6257. position: absolute;
  6258. top: -125rpx;
  6259. left: 50%;
  6260. width: 311rpx;
  6261. transform: translateX(-50%);
  6262. }
  6263. .winning-record-background {
  6264. width: 100%;
  6265. height: 100%;
  6266. }
  6267. .winning-record-content {
  6268. position: absolute;
  6269. top: 50rpx;
  6270. color: #fff;
  6271. width: 100%;
  6272. padding: 0 36rpx;
  6273. box-sizing: border-box;
  6274. .winning-record-title {
  6275. text-align: center;
  6276. font-size: 40rpx;
  6277. font-weight: 500;
  6278. margin-bottom: 40rpx;
  6279. }
  6280. .winning-record-header {
  6281. display: flex;
  6282. justify-content: space-between;
  6283. text-align: center;
  6284. font-size: 34rpx;
  6285. margin-bottom: 40rpx;
  6286. .header-column {
  6287. &.time-column {
  6288. width: 45%;
  6289. }
  6290. &.status-column {
  6291. width: 20%;
  6292. }
  6293. &.prize-column {
  6294. width: 35%;
  6295. }
  6296. }
  6297. }
  6298. .winning-record-list {
  6299. height: 500rpx;
  6300. .winning-record-item {
  6301. display: flex;
  6302. justify-content: space-between;
  6303. text-align: center;
  6304. font-size: 34rpx;
  6305. margin-bottom: 20rpx;
  6306. .record-time {
  6307. width: 45%;
  6308. font-size: 30rpx;
  6309. }
  6310. .record-status-button {
  6311. width: 20%;
  6312. background-color: #fff;
  6313. border-radius: 20rpx;
  6314. color: #f4410b;
  6315. }
  6316. .record-status {
  6317. width: 20%;
  6318. }
  6319. .record-prize {
  6320. width: 35%;
  6321. }
  6322. }
  6323. }
  6324. .no-winning-records {
  6325. font-size: 36rpx;
  6326. margin-top: 150rpx;
  6327. text-align: center;
  6328. }
  6329. }
  6330. }
  6331. // 观众列表弹窗
  6332. .viewers-list-popup {
  6333. position: relative;
  6334. height: 40vh;
  6335. padding: 40rpx 0rpx;
  6336. box-sizing: border-box;
  6337. display: flex;
  6338. flex-direction: column;
  6339. .viewers-list-title {
  6340. font-size: 32rpx;
  6341. text-align: center;
  6342. }
  6343. .viewers-scroll-list {
  6344. flex: 1;
  6345. margin-top: 50rpx;
  6346. overflow-y: auto;
  6347. padding: 0 40rpx;
  6348. .viewer-list-item {
  6349. display: flex;
  6350. align-items: center;
  6351. font-size: 28rpx;
  6352. margin: 20rpx 0;
  6353. .rank-number {
  6354. width: 50rpx;
  6355. margin-right: 10rpx;
  6356. font-weight: normal;
  6357. &.rank-1 {
  6358. color: #FF3B30;
  6359. font-weight: bold;
  6360. }
  6361. &.rank-2 {
  6362. color: #FF9500;
  6363. font-weight: bold;
  6364. }
  6365. &.rank-3 {
  6366. color: #FFCC00;
  6367. font-weight: bold;
  6368. }
  6369. &:not(.rank-1):not(.rank-2):not(.rank-3) {
  6370. color: #8E8E93;
  6371. }
  6372. }
  6373. .viewer-avatar-placeholder {
  6374. width: 72rpx;
  6375. height: 72rpx;
  6376. border-radius: 50%;
  6377. display: flex;
  6378. align-items: center;
  6379. justify-content: center;
  6380. background-color: #8978e2;
  6381. .avatar-initial {
  6382. color: #ffffff;
  6383. font-size: 24rpx;
  6384. }
  6385. }
  6386. .viewer-name {
  6387. margin-left: 16rpx;
  6388. font-size: 30rpx;
  6389. }
  6390. }
  6391. }
  6392. }
  6393. // 更多操作弹窗
  6394. .more-actions-popup {
  6395. border-radius: 20rpx 0 0 20rpx;
  6396. padding: 70rpx 30rpx;
  6397. display: flex;
  6398. justify-content: space-between;
  6399. .more-action-item {
  6400. display: flex;
  6401. flex-direction: column;
  6402. align-items: center;
  6403. .action-icon {
  6404. width: 48rpx;
  6405. height: 48rpx;
  6406. }
  6407. .action-label {
  6408. color: #1e1e1e;
  6409. text-align: center;
  6410. margin-top: 10rpx;
  6411. }
  6412. }
  6413. }
  6414. // 商品弹窗
  6415. .shopping-popup {
  6416. padding: 22rpx 16rpx;
  6417. .shopping-header {
  6418. display: flex;
  6419. justify-content: space-between;
  6420. align-items: center;
  6421. padding: 0 16rpx 22rpx;
  6422. .search-input-container {
  6423. width: 414rpx;
  6424. height: 76rpx;
  6425. background: #ffffff;
  6426. border-radius: 36rpx;
  6427. margin-left: 20rpx;
  6428. padding: 0 32rpx;
  6429. box-sizing: border-box;
  6430. font-size: 24rpx;
  6431. margin-right: 24rpx;
  6432. display: flex;
  6433. align-items: center;
  6434. color: #000;
  6435. .search-icon {
  6436. width: 24rpx;
  6437. margin-right: 16rpx;
  6438. }
  6439. }
  6440. .shopping-action-button {
  6441. display: flex;
  6442. flex-direction: column;
  6443. align-items: center;
  6444. font-size: 20rpx;
  6445. color: #222222;
  6446. .action-button-icon {
  6447. width: 48rpx;
  6448. height: 48rpx;
  6449. }
  6450. .action-button-label {
  6451. text-align: center;
  6452. }
  6453. }
  6454. }
  6455. .products-list {
  6456. height: 30vh;
  6457. overflow: hidden;
  6458. .product-item {
  6459. display: flex;
  6460. align-items: center;
  6461. padding: 20rpx 16rpx;
  6462. background: #ffffff;
  6463. border-radius: 16rpx;
  6464. margin-bottom: 16rpx;
  6465. .product-image-container {
  6466. width: 200rpx;
  6467. height: 200rpx;
  6468. border-radius: 16rpx;
  6469. overflow: hidden;
  6470. position: relative;
  6471. margin-right: 24rpx;
  6472. .product-image {
  6473. width: 100%;
  6474. height: 100%;
  6475. }
  6476. .product-index-label {
  6477. position: absolute;
  6478. top: 0;
  6479. width: 64rpx;
  6480. height: 40rpx;
  6481. background: var(--normal-bg);
  6482. border-radius: 16rpx 0rpx 16rpx 0rpx;
  6483. text-align: center;
  6484. font-weight: 500;
  6485. font-size: 28rpx;
  6486. color: var(--text-color);
  6487. }
  6488. }
  6489. .product-info {
  6490. flex: 1;
  6491. .product-name {
  6492. font-weight: 500;
  6493. font-size: 30rpx;
  6494. color: #000000;
  6495. margin-bottom: 10rpx;
  6496. }
  6497. .product-sales {
  6498. font-size: 22rpx;
  6499. color: #e69a22;
  6500. height: 56rpx;
  6501. }
  6502. .product-action-section {
  6503. display: flex;
  6504. justify-content: space-between;
  6505. align-items: center;
  6506. .product-price {
  6507. color: #ff5c03;
  6508. font-size: 22rpx;
  6509. font-weight: 500;
  6510. .price-symbol {
  6511. font-size: 20rpx;
  6512. font-weight: 600;
  6513. }
  6514. .price-integer {
  6515. font-size: 36rpx;
  6516. font-weight: bold;
  6517. }
  6518. }
  6519. .product-action-buttons {
  6520. display: flex;
  6521. align-items: center;
  6522. text-align: center;
  6523. line-height: 56rpx;
  6524. .collect-button {
  6525. width: 72rpx;
  6526. height: 100%;
  6527. background: #f5f7fa;
  6528. border-radius: 8rpx 0rpx 0rpx 8rpx;
  6529. .collect-icon {
  6530. width: 36rpx;
  6531. height: 36rpx;
  6532. vertical-align: middle;
  6533. }
  6534. }
  6535. .buy-button {
  6536. width: 152rpx;
  6537. background: linear-gradient(270deg, #ff5c03 0%, #ffac64 100%);
  6538. border-radius: 0rpx 8rpx 8rpx 0rpx;
  6539. font-weight: 500;
  6540. font-size: 30rpx;
  6541. color: #ffffff;
  6542. &.disabled {
  6543. background: #cccccc;
  6544. }
  6545. }
  6546. }
  6547. }
  6548. }
  6549. }
  6550. }
  6551. }
  6552. // 优惠券弹窗
  6553. .coupon-popup {
  6554. position: fixed;
  6555. bottom: 140rpx;
  6556. right: 100rpx;
  6557. z-index: 5;
  6558. border-radius: 20rpx;
  6559. width: 320rpx;
  6560. .coupon-container {
  6561. width: 100%;
  6562. position: relative;
  6563. .coupon-background {
  6564. height: 452rpx;
  6565. width: 100%;
  6566. }
  6567. .coupon-header {
  6568. position: absolute;
  6569. height: 120rpx;
  6570. top: -120rpx;
  6571. left: 0;
  6572. width: 100%;
  6573. z-index: 6;
  6574. }
  6575. .coupon-close-button {
  6576. position: absolute;
  6577. right: 10rpx;
  6578. top: 10rpx;
  6579. width: 40rpx;
  6580. height: 40rpx;
  6581. z-index: 99;
  6582. }
  6583. .coupon-content {
  6584. width: 100%;
  6585. position: absolute;
  6586. top: 20rpx;
  6587. display: flex;
  6588. flex-direction: column;
  6589. align-items: center;
  6590. color: #fff;
  6591. .coupon-name {
  6592. font-weight: 500;
  6593. font-size: 30rpx;
  6594. margin: 16rpx 0 12rpx;
  6595. }
  6596. .coupon-price {
  6597. font-size: 40rpx;
  6598. .coupon-price-integer {
  6599. font-size: 56rpx;
  6600. font-weight: 600;
  6601. }
  6602. }
  6603. .coupon-condition,
  6604. .coupon-description,
  6605. .coupon-validity {
  6606. font-weight: 500;
  6607. font-size: 30rpx;
  6608. margin: 5rpx 0;
  6609. }
  6610. .coupon-description {
  6611. margin-top: 26rpx;
  6612. }
  6613. .coupon-get-button {
  6614. background: linear-gradient(270deg, #fffce1 0%, #ffeaaf 100%);
  6615. color: #ff0004;
  6616. text-align: center;
  6617. padding: 16rpx 0;
  6618. border-radius: 40rpx;
  6619. font-weight: 500;
  6620. font-size: 30rpx;
  6621. width: 70%;
  6622. margin-top: 26rpx;
  6623. }
  6624. }
  6625. }
  6626. }
  6627. }
  6628. }
  6629. </style>