living.vue 144 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190
  1. <template>
  2. <view class="swiper-wrapper">
  3. <!-- 状态栏占位 -->
  4. <image src="https://fs-1323137866.cos.ap-chongqing.myqcloud.com/live_bg.jpg" class="live-bg"></image>
  5. <view class="container" :class="isFullscreen ? 'fullscreen ' : ''">
  6. <view class="content">
  7. <view class="content-top" style="position:absolute; z-index: 9999;" :style="{top: paddingTop+'px'}">
  8. <view class="x-start justify-between">
  9. <view class="live-x-f">
  10. <image v-if="!scene" @click="goBack" class="return-icon"
  11. src="/static/images/live/return3.png" />
  12. <view class="align-center"
  13. style="padding: 6rpx 8rpx; height: 64rpx; background: rgba(0,0,0,0.5); border-radius: 32rpx">
  14. <u-avatar :src="liveItem.liveImgUrl || '/static/images/live/avatar.png'"
  15. :size="32"></u-avatar>
  16. <view class="user-name">
  17. <view>{{ liveItem.liveName ? truncateString(liveItem.liveName, 8) : '未命名' }}</view>
  18. </view>
  19. </view>
  20. </view>
  21. <!-- 积分倒计时 -->
  22. <view class="progress-countdown" :class="liveItem.showType == 2 ? ' progress-vertical' : ''"
  23. :style="{ 'display': isFocus ? 'none' : '' }"
  24. v-if="countdownPercentage != 100 && liveItem.completionPointsEnabled && liveItem.status == 2 && !isPreview">
  25. <image class="title" src="/static/images/live/points_title.png"></image>
  26. <view class="progress-bar-bg">
  27. <view class="progress-bar-fill" :style="{ width: countdownPercentage + '%' }"></view>
  28. </view>
  29. <view class="progress-text">
  30. 倒计时{{ formattedCountdown.hours || '00' }}:{{ formattedCountdown.minutes || '00' }}:{{
  31. formattedCountdown.seconds ||'00' }}
  32. </view>
  33. </view>
  34. </view>
  35. <view class="end">
  36. <view v-if="Array.isArray(filteredViewers)" class="align-center">
  37. <view v-for="(item, viewerIndex) in filteredViewers" :key="viewerIndex">
  38. <image v-if="item.avatar" class="end-icon" style="border-radius: 26rpx"
  39. :src="item.avatar" />
  40. <view v-else class="end-icon"
  41. :style="{ backgroundColor: getUserRandomColor(item.userId), borderRadius: '50%' }">
  42. <text class="text-white text-xs">{{ getNicknameInitial(item.nickName) }}</text>
  43. </view>
  44. </view>
  45. <view class="sum">{{ formattedWatchCount || 0 }}</view>
  46. </view>
  47. <view class="complaint-box" @click="navgetTo('/pages_live/shopping/complaintList')">
  48. <image class=" complaint-icon" src="/static/images/live/complaint.png" mode="widthFix" />
  49. <view>投诉</view>
  50. </view>
  51. </view>
  52. </view>
  53. <!-- 右边的side -->
  54. <view v-show="!isFocus" class="side-group" :class="{
  55. 'top2': (!isShowRed && !(isShowLottery && countdown)),
  56. 'top3': (isShowRed || (isShowLottery && countdown))
  57. }">
  58. <view class="side-item" @click="onRed()" v-if="isShowRed">
  59. <button class="button button-reset" style="height: 70rpx;">
  60. <image class="image" style="width: 72rpx;" src="/static/images/live/redbag.png"
  61. mode="widthFix" />
  62. </button>
  63. <view class="fs30">领红包</view>
  64. </view>
  65. <view class="side-item" @click="onLottery()" v-if="isShowLottery && countdown">
  66. <button class="button button-reset">
  67. <image class="image" src="/static/images/live/lottery.png" mode="widthFix" />
  68. </button>
  69. <view class="fs30">抽奖</view>
  70. </view>
  71. <view class="side-item" @click="onLike">
  72. <LikeButton :initialCount="100" :heartsPerClick="5" @like="onLike" />
  73. <view class="txt" style="font-size: 30rpx;">{{ formattedLikeCount || 0 }}</view>
  74. </view>
  75. <view class="side-item">
  76. <button open-type="share" class="button button-reset">
  77. <image class="image" src="/static/images/live/weixin1.png" mode="widthFix" />
  78. </button>
  79. <view class="txt" style="font-size: 30rpx;">分享</view>
  80. </view>
  81. </view>
  82. <!-- 直播 -->
  83. <LiveVideo class="live-video" ref="liveVideo" :isAgreement="isAgreement" :liveItem="liveItem"
  84. :hasSubscribed="hasSubscribed" :liveId="liveId" :userData="userData" @liveStart="handleLiveStart"
  85. @videoError="handleVideoError" @liveStateChange="handleLiveStateChange"
  86. @agreementClick="handleAgreement" @fullscreen-change="handleFullscreenChange" />
  87. <!-- 底部聊天区域 -->
  88. <view class="chat-area-container" :class="{ 'chat-area-focused': isFocus }"
  89. :style="{ '--keyboard-height': keyboardHeight + 'rpx' }">
  90. <view class="mt20 chat-content"
  91. :class="{ 'chat-content-focused': isFocus, 'chat-content-preview': liveItem.status == 1 }">
  92. <view v-if="showPurchasePrompt && orderUser && orderUser.count && liveItem.status == 2"
  93. class="shop-prompt ">
  94. <image class="shopping-tip" src="/static/images/live/shopping.png" />
  95. <text>{{ orderUser.count || 0 }}人正在去购买</text>
  96. </view>
  97. <view v-if="showWelcomeMessage" class="welcome-message" style="max-width: 100%">
  98. <view class="list justify-start" v-show="inAndOut.cmd == 'entry' || inAndOut.cmd == 'out'">
  99. <view class="talk-list justify-start">
  100. <view class="fs30">
  101. <text style="color: #ff89d6">{{ inAndOut.nickName || '未命名' }}</text>
  102. <text class="colorf">{{ inAndOut.msg }}直播间</text>
  103. </view>
  104. </view>
  105. </view>
  106. </view>
  107. <view class="notice-message" v-if="!isFocus && isShowNotice">
  108. 公告消息: {{ notice.msg }}
  109. </view>
  110. <scroll-view id="msgScroll" v-if="Array.isArray(talklist)" enable-flex scroll-y="true"
  111. :enhanced="true" :bounces="false" :show-scrollbar="false" :fast-deceleration="false"
  112. :enable-back-to-top="false" class="scrolly flex-1 column"
  113. style="width: calc(100% - 20rpx); -webkit-overflow-scrolling: touch;height: 100%;"
  114. :scroll-top="scrollTop" :scroll-into-view="scrollIntoView" @scroll="onScroll"
  115. ref="scrollView" scroll-with-animation="true" :scroll-animation-duration="300">
  116. <view class="list justify-start" v-for="(item, talkIndex) in talklist || []"
  117. :key="talkIndex" :id="`list_${item.uniqueId || talkIndex}`"
  118. v-show="item.cmd != 'red' && item.cmd != 'out' && item.cmd != 'entry'">
  119. <view class="talk-list justify-start">
  120. <view class="fs30 talk-item" style="max-width: 100%;">
  121. <text class="nickname" style="color: #ffda73;">{{ item.nickName || '未命名'
  122. }}:</text>
  123. <text class="message colorf">{{ item.msg }}</text>
  124. </view>
  125. </view>
  126. </view>
  127. </scroll-view>
  128. </view>
  129. <!-- 一键弹幕 -->
  130. <u-scroll-list :indicator="false" v-if="!isFocus">
  131. <view class="barrage" v-for="(item, index) in barrageList" :key="index"
  132. @click="onBarrage(item)">
  133. {{ item }}
  134. </view>
  135. </u-scroll-list>
  136. <!-- 聊天输入组件 -->
  137. <ChatInput ref="chatInput" :value="value" :focused="isFocus" :placeholderText="placeholderText"
  138. @focus="onInputFocus" @blur="onInputBlur" @send="onSendMessage" @open-cart="openCart"
  139. @show-gift="showGift" @input="onInputChange" @show-more="handleShowMore" />
  140. </view>
  141. </view>
  142. <!-- 商品卡片 -->
  143. <LiveGoods class="shop-card" :isShow="isShowGoods" :goodsData="goodsCard" @goShop="handleGoShop"
  144. @close="handleCloseGoods" />
  145. <!-- 抽奖弹窗 -->
  146. <LotteryPopup :show="isShowLotteryPop && countdown" :countdown="countdown" :products="lotteryProducts"
  147. @close="handleLotteryClose" @claim="handleLotteryClaim" />
  148. <u-popup :show="!!integral.status" round="20rpx" mode="center" bgColor="#ffffff" zIndex="10076">
  149. <view class="integral-box">
  150. <view class="top">
  151. <view class="title">观看视频领芳华币</view>
  152. <image class="photo" src="/static/images/live/integral_bg.png" mode="widthFix" />
  153. </view>
  154. <view class="item">
  155. <view class="title">{{ integral.msg }}</view>
  156. <view class="button" @click="integral.status = false">确认</view>
  157. </view>
  158. </view>
  159. </u-popup>
  160. <u-popup :show="shouldShowIntegralPopup" round="20rpx" mode="center" bgColor="#ffffff" zIndex="10076">
  161. <view class="integral-popup color9">
  162. <view class="integral-header">
  163. <view class="integral-title">观看视频领积分</view>
  164. <image class="integral-background-image"
  165. src="https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/integral_bg.png"
  166. mode="widthFix" />
  167. </view>
  168. <view class="integral-content">
  169. <view class="integral-message">积分发放成功</view>
  170. </view>
  171. </view>
  172. </u-popup>
  173. <u-popup :show="isShowRedCard" round="20rpx" mode="center" bgColor="transparent" zIndex="10076">
  174. <view class="red-card" v-if="redCard">
  175. <image class="img" src="/static/images/live/red_card.png" />
  176. <view class="red-content">
  177. <view class="title">{{ redCard.msg }}</view>
  178. <view class="txt">直播惊喜芳华币</view>
  179. <view class="button" @click="isShowRedCard = false">确认</view>
  180. </view>
  181. </view>
  182. </u-popup>
  183. <!-- 中奖和未中奖 -->
  184. <u-popup :show="isShowPrize && havePrize" round="20rpx" mode="center" bgColor="#fff" zIndex="10076">
  185. <view class="prize-card" v-if="isCurrentUserWon">
  186. <image class="nav-img" src="/static/images/live/red_head.png" mode="widthFix" />
  187. <view class="title">恭喜您 中奖啦!</view>
  188. <view class="prize-content" v-for="(item, index) in prizeInfo || []" :key="index">
  189. <view class="item">{{ item.userName }}</view>
  190. <view class="item">{{ item.userId }}</view>
  191. <view class="txt item">{{ item.prizeLevel }}等奖</view>
  192. </view>
  193. <view class="tip">请填写收货地址,主播将会将奖品发给您</view>
  194. <view class="button"
  195. @click="navgetTo('/pages_live/shopping/confirmCreateOrder?type=win&productId=' + getCurrentUserPrizeProductId + '&liveId=' + liveId + '&recordId=' + getCurrentUserPrizeRecordId), confirm()">
  196. 填写地址
  197. </view>
  198. </view>
  199. <view class="no-prize-card" v-else>
  200. <image class="img" src="/static/images/live/no-prize.png" mode="widthFix" />
  201. <view class="tip">很遗憾 您未中奖</view>
  202. <view class="button" @click="confirm">确认</view>
  203. </view>
  204. </u-popup>
  205. <!-- 中奖记录 -->
  206. <WinningPopup :show="winning" :prizeList="prizeAll" :liveId="liveId" @close="closeWin"
  207. @fill-address="handleFillAddress" />
  208. <!-- 更多弹窗 -->
  209. <u-popup :show="isMore" @close="closeMore" round="20rpx" bgColor="#f3f5f9" zIndex="10076">
  210. <view class="more-block">
  211. <view class="item" @click="navgetTo('/pages/user/integral/integralGoodsList'), (isMore = false)">
  212. <image class="w48 h48" src="/static/image/my/member_icon.png" />
  213. <view style="text-align: center">芳华币</view>
  214. </view>
  215. <view class="item" @click="goOrder(), (isMore = false)">
  216. <image class="w48 h48" src="/static/image/my/my_cforder_icon.png" />
  217. <view style="text-align: center">我的订单</view>
  218. </view>
  219. <view class="item"
  220. @click="navgetTo('/pages_live/shopping/storeOrderRefundList?liveId=' + liveId), (isMore = false)">
  221. <image class="w48 h48" src="/static/image/my/my_cforder_icon.png" />
  222. <view style="text-align: center">售后订单</view>
  223. </view>
  224. <view class="item" @click="getMyLottery(), (isMore = false), (winning = true)">
  225. <image class="w48 h48" src="/static/image/my/my_points_icon.png" />
  226. <view style="text-align: center">中奖记录</view>
  227. </view>
  228. </view>
  229. </u-popup>
  230. <!-- 送礼物 -->
  231. <u-popup :show="isShowGift" @close="closeGift" round="20rpx" bgColor="#242424" zIndex="10076">
  232. <view class="gift">
  233. <view class="gift-top">
  234. <view class="left">
  235. 送花给<text class="ml8 orange">{{ liveItem.liveName }}</text>
  236. </view>
  237. <view class="align-center" @click="navgetTo('/pages_live/shopping/integral')">
  238. <image class="w32 h32" src="/static/images/live/coin.png" />
  239. <text class="ml6 mr6 f24">{{ integralNum }}</text>
  240. <image class="w24 h24" src="/static/images/live/arrow_white.png" />
  241. </view>
  242. </view>
  243. <view class="gift-block">
  244. <view :class="['item', { 'active': activeIndex === index }]" @click="activeIndex = index"
  245. v-for="(item, index) in giftList" :key="index">
  246. <image class="w104 h104" :src="item.img" />
  247. <view class="name">{{ item.giftName }}</view>
  248. <view v-if="activeIndex === index" class="button" @click="sendliveGift(item)">赠送</view>
  249. <view class="number" v-else>{{ item.price }}芳华币</view>
  250. </view>
  251. </view>
  252. </view>
  253. </u-popup>
  254. <!-- 微信昵称授权弹窗 -->
  255. <u-popup :show="userlogo" mode="bottom" round='12'>
  256. <view class="userlogo column">
  257. <view class="bold fs36 mt42">授权你的昵称信息</view>
  258. <view class=" justify-between align-center mt42">
  259. <view class="button-container">
  260. <input type="nickname" class="hidden-input"
  261. placeholder-style="color:#ffffff; font-size:32rpx;" @blur="onNickNameInput"
  262. placeholder="请点击授权微信昵称" @input="onNickNameInput" />
  263. </view>
  264. </view>
  265. <view class="submitname" @click="confimrname">确定</view>
  266. </view>
  267. </u-popup>
  268. <!-- 商品弹窗组件 -->
  269. <ShopPopup class="shop" :show="shopping" :searchKeyword="inputInfo" :products="products"
  270. :loading="loadingProducts" :boxHeight="boxHeight" @close="closeShop" @search="handleSearchInput"
  271. @navigate-to-order="handleNavigateToOrder" @show-more="handleShowMore" @collect="onGoodsCollect"
  272. @go-shop="handleGoShopFromCart" />
  273. <!-- 优惠券弹窗 -->
  274. <view class="coupon-pop" v-if="isShowCoupon">
  275. <view class="coupon-block">
  276. <image class="bg" src="/static/images/live/coupon_bg.png" />
  277. <image class="nav" src="/static/images/live/coupon_top.png" />
  278. <image @click="isShowCoupon = false" class="w40 h40 close" style="z-index: 99"
  279. src="/static/images/live/close1.png" />
  280. <view class="item">
  281. <view class="title">{{ couponInfo.couponName }}</view>
  282. <view class="price">
  283. <text class="bold">{{ couponInfo.couponPrice }}</text>
  284. </view>
  285. <view class="txt">满{{ couponInfo.useMinPrice }}元可用</view>
  286. <view class="txt" style="margin-top: 26rpx">指定商品可用</view>
  287. <view class="txt">自领取起{{ couponInfo.couponTime }}天内有效</view>
  288. <view class="button" @click="onCoupon()">立即领券</view>
  289. </view>
  290. </view>
  291. </view>
  292. <view class="custom-toast" v-if="showToast">
  293. <view class="toast-content">
  294. 观看奖励已到账
  295. </view>
  296. </view>
  297. </view>
  298. </view>
  299. </template>
  300. <script>
  301. import "@/utils/webview.js"
  302. import ChatInput from '@/pages_live/components/chatInput.vue'
  303. import LiveVideo from '@/pages_live/components/liveVideo.vue'
  304. import ShopPopup from '@/pages_live/components/shopPopup.vue'
  305. import LotteryPopup from '@/pages_live/components/lotteryPopup.vue'
  306. import WinningPopup from '@/pages_live/components/winningPopup.vue'
  307. import LiveGoods from '@/pages_live/components/liveGoods.vue'
  308. import LikeButton from '@/pages_live/components/like.vue'
  309. import CryptoJS from 'crypto-js'
  310. import {
  311. myLottery,
  312. coupon,
  313. liveLottery,
  314. claim,
  315. liveRed,
  316. liveDataLike,
  317. collectStore,
  318. collectGoods,
  319. watchUserList,
  320. liveMsg,
  321. liveStore,
  322. liveOrderUser,
  323. getLiveInfo,
  324. getLiveViewData,
  325. currentActivities,
  326. getlive,
  327. subNotifyLive,
  328. internetTraffic,
  329. liveInternetTraffic,
  330. remainingTime,
  331. updateWatchDuration,
  332. receivePoints,
  333. activeList,
  334. sendliveGift,
  335. getUserIntegralInfo, //芳华币
  336. loginByMp,
  337. getUserLiveInfo
  338. } from '@/api/living.js'
  339. import {
  340. getUserInfo,
  341. editUser
  342. } from '@/api/user'
  343. import {
  344. generateRandomString,
  345. checkLiveToken
  346. } from '@/utils/common.js'
  347. import dayjs from 'dayjs'
  348. import {
  349. nextTick
  350. } from 'vue'
  351. var isSocketOpen = false
  352. var socket = null
  353. export default {
  354. components: {
  355. LikeButton,
  356. LiveGoods,
  357. LotteryPopup,
  358. WinningPopup,
  359. ShopPopup,
  360. LiveVideo,
  361. ChatInput
  362. },
  363. data() {
  364. return {fakeAvatar: Array.from({
  365. length: 11
  366. }, (_, i) =>
  367. `https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/userapp/images/avatar${i + 1}.jpg`),
  368. paddingTop: uni.getSystemInfoSync().statusBarHeight + 44,
  369. hasSubscribed: false, // 已成功订阅(永久禁用)
  370. integralNum: null, //礼物芳华币数量
  371. isPreview: true, //是否预告
  372. hasReplayGeneration: false, //是否经历回放生成
  373. diffLiveStartTime: 0, //距离直播开始时间差值
  374. diffLiveEndTime: 0, //距离直播结束时间差值
  375. diffReplayGenerationSeconds: 0, //经历回放生成的秒速
  376. completionTime: 0, //领取积分所需时间
  377. hasLiveEnd: false, //是否经历过首播结束
  378. isPageLoadFirst: true, //是否首次加载直播间
  379. pointsRetryTimer: null, // 积分领取重试定时器
  380. showToast: false,
  381. videoDuration: 0, //要看的时长
  382. pointsRemainingTime: 0,
  383. hasReceived: false,
  384. completionRate: 0,
  385. liveBeginWatchTime: 0,
  386. showPoints: false,
  387. remainingTime: null,
  388. // receiveStatus: false,
  389. watchDuration: null,
  390. isFullscreen: null,
  391. notice: [],
  392. isShowNotice: false,
  393. giftList: [{
  394. img: '/static/images/live/gift1.png',
  395. name: '人气票',
  396. number: 1
  397. },
  398. {
  399. img: '/static/images/live/gift2.png',
  400. name: '小心心',
  401. number: 1
  402. },
  403. {
  404. img: '/static/images/live/gift3.png',
  405. name: '棒棒糖',
  406. number: 9
  407. },
  408. {
  409. img: '/static/images/live/gift4.png',
  410. name: '玫瑰',
  411. number: 50
  412. },
  413. {
  414. img: '/static/images/live/gift5.png',
  415. name: '鲜花',
  416. number: 88
  417. },
  418. {
  419. img: '/static/images/live/gift6.png',
  420. name: '水晶球',
  421. number: 99
  422. },
  423. {
  424. img: '/static/images/live/gift7.png',
  425. name: '啤酒',
  426. number: 128
  427. },
  428. {
  429. img: '/static/images/live/gift8.png',
  430. name: '浪漫热气球',
  431. number: 1000
  432. }
  433. ],
  434. activeIndex: -1,
  435. barrageList: ['支持你!', '谢谢你!', '平台真好!', '666', '欢迎回家!'],
  436. uuId: '',
  437. totalTraffic: 0,
  438. bitrate: 800,
  439. bitrateLive: 1600,
  440. trafficTimer: null,
  441. pingTimeoutTimer: null,
  442. heartBeatTimer: null,
  443. liveViewDataTimer: null,
  444. lookAudsCount: 0, //更多的观众人数
  445. reconnectTimer: null,
  446. scrollTimer: null,
  447. lastScrollTime: 0,
  448. scrollDebounceDelay: 200,
  449. searchTimer: null,
  450. purchasePromptTimer: null,
  451. welcomeTimer: null,
  452. redTimer: null,
  453. liveStartTimer: null,
  454. lotteryTimer: null,
  455. memoryMonitorTimer: null,
  456. networkStatusTimer: null,
  457. networkRetryTimer: null,
  458. lastHeartBeatTime: 0,
  459. connectionStartTime: 0,
  460. connectionLatency: 0,
  461. messageCount: 0,
  462. errorCount: 0,
  463. lastPerformanceCheck: 0,
  464. stayTime: 0,
  465. startTime: 0,
  466. scrollTop: 0,
  467. currentScrollTop: 0,
  468. scrollIntoView: '',
  469. messageIdCounter: 0,
  470. scrollPending: false,
  471. isOnload: false,
  472. isConnecting: false,
  473. hasInitialized: false,
  474. liveViewersData: [],
  475. liveTopViewersData: [],
  476. liveUserCalled: false,
  477. userRandomColors: Object.create(null),
  478. heartBeatRetryCount: 0,
  479. maxHeartBeatRetries: 3,
  480. pingTimeout: 25000,
  481. shownEntryUsers: new Set(),
  482. socket: null,
  483. isSocketOpen: false,
  484. heartBeatInterval: 15000,
  485. adaptiveHeartBeatInterval: 15000,
  486. reconnectCount: 0,
  487. maxReconnectAttempts: 5,
  488. isManualClose: false,
  489. isBackground: false,
  490. networkType: 'unknown',
  491. isNetworkAvailable: true,
  492. templateId: 'oDN_EoYNNb5RKbhTAfVtXevSGRhoyoOQwR9JKqeKMbA',
  493. isAgreement: false,
  494. wsNewUrl: 'wss://api.fhhx.runtzh.com/ws/app/webSocket',
  495. qrFrom: null,
  496. scene: '',
  497. liveCountdown: {},
  498. countdown: {},
  499. liveViewData: {},
  500. keyboardHeight: 0,
  501. videoCurrentTime: 0,
  502. videoProgressKey: '',
  503. inAndOut: {},
  504. winning: false,
  505. isShowPrize: false,
  506. isShowCoupon: false,
  507. isShowGift: false,
  508. prizeInfo: [],
  509. havePrize: uni.getStorageSync('havePrize') || false,
  510. countDownKey: 0,
  511. lotteryProducts: [],
  512. lotteryList: [],
  513. talklist: [],
  514. isShowLotteryPop: false,
  515. liveItem: {},
  516. isSending: false,
  517. isMore: false,
  518. value: '',
  519. placeholderText: '说点什么...',
  520. prizeAll: [],
  521. isShowLottery: false,
  522. isShowRedCard: false,
  523. redCard: null,
  524. integral: {},
  525. lotteryInfo: {},
  526. goodsCard: {},
  527. couponInfo: {},
  528. redInfo: {},
  529. storeId: null,
  530. isFocus: false,
  531. shopping: false,
  532. systemInfo: null,
  533. inputInfo: '',
  534. showWelcomeMessage: false,
  535. isShowGoods: false,
  536. isShowRed: false,
  537. lastClickTime: 0,
  538. videoRetryCounts: Object.create(null),
  539. clickDelay: 300,
  540. liveUserTotal: 0,
  541. showPurchasePrompt: false,
  542. prevOrderCount: 0,
  543. videoUrl: null,
  544. boxHeight: 300,
  545. products: [],
  546. loadingProducts: false,
  547. orderUser: {},
  548. userType: 0,
  549. timestamp: '',
  550. liveId: null,
  551. userinfo: '',
  552. userData: {},
  553. diffTotalTime: '',
  554. virtualTotal: 0,
  555. userlogo: false,
  556. user: {},
  557. userInfo: '',
  558. urlOption: {},
  559. source: '', // app,app内嵌H5页面
  560. }
  561. },
  562. async onLoad(options) {
  563. console.log(options,'options')
  564. // 支持从 index 传入的集合参数 payload
  565. let payload = {}
  566. if (options.payload && options.source === 'app') {
  567. try {
  568. payload = JSON.parse(decodeURIComponent(options.payload))
  569. } catch (e) {
  570. console.warn('解析 payload 失败', e)
  571. }
  572. }
  573. const opts = { ...options, ...payload }
  574. if (opts.liveId) {
  575. this.liveId = opts.liveId
  576. }
  577. this.source = opts.source || ''
  578. if (this.source === 'app') {
  579. // 从 payload 写入 storage(web-view 内与原生隔离,由 index 通过 payload 传入)
  580. //uni.setStorageSync('AppToken', opts.appToken)
  581. uni.setStorageSync('liveToken', opts.liveToken)
  582. uni.setStorageSync('userInfo', opts.userInfo)
  583. uni.setStorageSync('userData', opts.userData)
  584. console.log('++++++++++++++++++++++++Token+', opts.liveToken)
  585. console.log('——————————————————————————liveUser', opts.userData)
  586. //uni.setStorageSync('liveUser', opts.liveUserId)
  587. uni.setStorageSync('subscribe_status_' + this.liveId, opts.hasSubscribed == 'true')
  588. //console.log('++++++++++++subscribe_status_', opts.hasSubscribed)
  589. //console.log('——————————————————————————subscribe_status_', uni.getStorageSync('subscribe_status_' + this.liveId))
  590. }
  591. if (opts.scene) {
  592. this.scene = opts.scene
  593. const decodedScene = decodeURIComponent(this.scene)
  594. const params = {}
  595. decodedScene.split('&').forEach((item) => {
  596. const [key, value] = item.split('=')
  597. params[key] = value
  598. this.liveId = params.a
  599. })
  600. if (params.b && params.c) {
  601. this.qrFrom = `&companyId=${params.b||this.liveItem.companyId||-1}&companyUserId=${params.c}`
  602. }
  603. }
  604. if (opts.companyId && opts.companyUserId) {
  605. this.qrFrom = `&companyId=${opts.companyId||this.liveItem.companyId||-1}&companyUserId=${opts.companyUserId}`
  606. }
  607. // 保存url参数
  608. this.urlOption = opts
  609. this.userinfo = uni.getStorageSync("userInfo")
  610. this.userData = uni.getStorageSync("userData")
  611. console.log('++++++++++++++++++++++++this.userData', this.userData)
  612. this.hasSubscribed = uni.getStorageSync('subscribe_status_' + this.liveId) || false;
  613. // try {
  614. // const isLogin = await checkLiveToken();
  615. // this.hasCheckedLogin = true;
  616. // if (isLogin) {
  617. // // 登录后:优先加载直播间信息,然后加载视频资源
  618. // await this.haveLogin();
  619. // } else {
  620. // // 未登录:先登录,登录成功后再加载数据
  621. // // #ifdef MP-WEIXIN
  622. // this.goLogin();
  623. // // #endif
  624. // }
  625. // } catch (error) {
  626. // console.error('初始化失败:', error);
  627. // this.hasCheckedLogin = true;
  628. // }
  629. this.startMemoryMonitor();
  630. //获取礼物列表
  631. this.getActiveList();
  632. uni.onKeyboardHeightChange((res) => {
  633. console.log('键盘高度变化:', res.height, '平台:', this.systemInfo.platform)
  634. if (this.systemInfo.platform === 'ios') {
  635. if (res.height > 0) {
  636. this.isKeyboardShow = true
  637. let calculatedHeight = res.height * 2
  638. if (this.systemInfo.model) {
  639. if (this.systemInfo.model.includes('iPhone X') ||
  640. this.systemInfo.model.includes('iPhone 11') ||
  641. this.systemInfo.model.includes('iPhone 12') ||
  642. this.systemInfo.model.includes('iPhone 13') ||
  643. this.systemInfo.model.includes('iPhone 14') ||
  644. this.systemInfo.model.includes('iPhone 15') ||
  645. this.systemInfo.model.includes('iPhone 16') ||
  646. this.systemInfo.model.includes('iPhone 17')) {
  647. calculatedHeight = calculatedHeight + 20
  648. }
  649. }
  650. const safeAreaBottom = this.systemInfo.safeAreaInsets ? this.systemInfo.safeAreaInsets
  651. .bottom : 0
  652. if (safeAreaBottom > 0) {
  653. calculatedHeight = calculatedHeight - (safeAreaBottom * 3.0)
  654. }
  655. this.keyboardHeight = Math.max(400, calculatedHeight)
  656. } else {
  657. this.isKeyboardShow = false
  658. this.keyboardHeight = 0
  659. }
  660. } else {
  661. console.log('手机型号是>>>>', this.systemInfo.model)
  662. if (res.height > 0) {
  663. this.isKeyboardShow = true
  664. const safeAreaBottom = this.systemInfo.safeAreaInsets ? this.systemInfo.safeAreaInsets
  665. .bottom : 0
  666. if (this.systemInfo.brand == 'vivo') {
  667. this.keyboardHeight = res.height * 2
  668. } else {
  669. this.keyboardHeight = (res.height * 1.78) + 20
  670. }
  671. console.log('高度是', this.keyboardHeight)
  672. } else {
  673. this.isKeyboardShow = false
  674. this.keyboardHeight = 0
  675. }
  676. }
  677. })
  678. this.initNetworkStatusListener();
  679. },
  680. onPullDownRefresh() {
  681. this.getLiveMsg(this.liveItem);
  682. // this.getliveUserInit();
  683. setTimeout(() => {
  684. uni.stopPullDownRefresh()
  685. }, 1000)
  686. },
  687. async onShow() {
  688. try {
  689. const isLogin = await checkLiveToken()
  690. if (isLogin) {
  691. await this.haveLogin();
  692. //await this.getUserInfo();
  693. //await this.getUserLiveInfo()
  694. } else {
  695. // #ifdef MP-WEIXIN
  696. this.goLogin();
  697. // #endif
  698. }
  699. } catch (error) {
  700. console.error('初始化失败:', error)
  701. }
  702. if (this.liveId) {
  703. await this.getLiveMsg(this.liveItem)
  704. }
  705. if (!this.liveViewData) {
  706. this.getliveViewData()
  707. }
  708. this.uuId = generateRandomString(16)
  709. const isLiveLogin = uni.getStorageSync('isLiveLogin')
  710. this.share = uni.getStorageSync('share')
  711. this.scene = uni.getStorageSync('scene')
  712. if (this.share && this.share.length > 0) {
  713. this.liveId = this.share.liveId
  714. this.qrFrom = `&companyId=${this.share.companyId||this.liveItem.companyId||-1}&companyUserId=${this.share.companyUserId}`
  715. uni.removeStorageSync('share')
  716. }
  717. if (this.scene) {
  718. const decodedScene = decodeURIComponent(this.scene)
  719. const params = {}
  720. decodedScene.split('&').forEach((item) => {
  721. const [key, value] = item.split('=')
  722. params[key] = value
  723. this.liveId = params.a
  724. })
  725. if (params.b && params.c) {
  726. this.qrFrom = `&companyId=${params.b||this.liveItem.companyId||-1}&companyUserId=${params.c}`
  727. }
  728. uni.removeStorageSync('scene')
  729. }
  730. if (isLiveLogin) {
  731. if (this.liveId) {
  732. const userInfo = uni.getStorageSync('userInfo');
  733. if (userInfo) {
  734. await this.getliving(this.liveId)
  735. this.getCurrentActivities()
  736. this.getliveOrder()
  737. this.initSocket()
  738. }
  739. }
  740. this.hasInitialized = true
  741. uni.removeStorageSync('isLiveLogin')
  742. }
  743. await this.resumePageActivity()
  744. this.userinfo = uni.getStorageSync('userInfo')
  745. this.isAgreement = uni.getStorageSync('isAgreement')
  746. this.$nextTick(() => {
  747. // 严格检查 $refs.liveVideo 是否存在
  748. if (this.$refs && this.$refs.liveVideo && typeof this.$refs.liveVideo.setVideoProgress === 'function') {
  749. this.$refs.liveVideo.setVideoProgress()
  750. }
  751. });
  752. if (this.lookTimer) {
  753. clearInterval(this.lookTimer)
  754. this.lookTimer = null
  755. this.stayTime = 0
  756. this.startTime = 0
  757. }
  758. // 清除等待定时器
  759. if (this.$refs && this.$refs.liveVideo && this.$refs.liveVideo.waitingTimer) {
  760. clearTimeout(this.$refs.liveVideo.waitingTimer);
  761. this.$refs.liveVideo.waitingTimer = null;
  762. }
  763. // 同时清理自身的waitingTimer(如果存在)
  764. if (this.waitingTimer) {
  765. clearTimeout(this.waitingTimer);
  766. this.waitingTimer = null;
  767. }
  768. if (this.trafficTimer) {
  769. clearInterval(this.trafficTimer)
  770. this.trafficTimer = null
  771. this.startTime = 0
  772. this.totalTraffic = 0
  773. }
  774. if (this.$refs && this.$refs.liveVideo && typeof this.$refs.liveVideo.startTimer === 'function') {
  775. this.$refs.liveVideo.startTimer()
  776. }
  777. if (this.liveItem.completionPointsEnabled && !this.hasReceived) {
  778. setTimeout(() => {
  779. this.startCountdown();
  780. }, 500);
  781. }
  782. // 页面加载完成后确保聊天内容滚动到底部
  783. this.$nextTick(() => {
  784. setTimeout(() => {
  785. this.forceScrollToBottomOnSend();
  786. }, 500);
  787. });
  788. },
  789. onShareAppMessage() {
  790. return {
  791. title: '邀请你来观看直播:' + this.liveItem.liveName,
  792. path: '/pages_live/living?companyId=-2&companyUserId=' + this.userData.userId + '&liveId=' + this.liveId,
  793. imageUrl: '/static/images/live/logo.png',
  794. success(res) {
  795. console.log('分享成功', res)
  796. },
  797. fail(err) {
  798. console.error('分享失败', err)
  799. }
  800. }
  801. },
  802. onShareTimeline() {
  803. return {
  804. title: '邀请你来观看直播:' + this.liveItem.liveName,
  805. query: 'companyId=-2&companyUserId=' + this.userData.userId + '&liveId=' + this.liveId
  806. }
  807. },
  808. computed: {
  809. appid() {
  810. return this.$store.state.appid
  811. },
  812. shouldShowIntegralPopup() {
  813. //只有当 showPoints 为 true 且 receiveList 有数据时才显示
  814. return this.showPoints;
  815. },
  816. countdownPercentage() {
  817. return this.getCountdownPercentage()
  818. },
  819. formattedCountdown() {
  820. return this.formatCountdown(this.pointsRemainingTime)
  821. },
  822. formattedWatchCount() {
  823. return this.formatNumber(this.liveUserTotal*2+50 || 0)
  824. },
  825. formattedLikeCount() {
  826. return this.formatNumber(this.liveViewData.like || 0)
  827. },
  828. filteredViewers() {
  829. // 从预设的头像数组中随机选择3个
  830. const avatarArray = this.fakeAvatar || [];
  831. if (!Array.isArray(avatarArray) || avatarArray.length === 0) {
  832. return [];
  833. }
  834. const shuffled = [...avatarArray].sort(() => 0.5 - Math.random());
  835. const selected = shuffled.slice(0, 3);
  836. // 转换为包含avatar属性的对象数组
  837. return selected.map((avatar, index) => ({
  838. userId: `virtual_${index}`,
  839. avatar: avatar
  840. }));
  841. },
  842. isCurrentUserWon() {
  843. if (!Array.isArray(this.prizeInfo) || !this.userData?.userId) {
  844. return false
  845. }
  846. return this.prizeInfo.some((item) => {
  847. return String(item.userId) === String(this.userData.userId)
  848. })
  849. },
  850. getCurrentUserPrizeProductId() {
  851. if (!Array.isArray(this.prizeInfo) || !this.userData?.userId) {
  852. return null
  853. }
  854. const userPrize = this.prizeInfo.find(item => {
  855. return String(item.userId) == String(this.userData.userId)
  856. })
  857. return userPrize ? userPrize.productId : null
  858. },
  859. getCurrentUserPrizeRecordId() {
  860. if (!Array.isArray(this.prizeInfo) || !this.userData?.userId) {
  861. return null
  862. }
  863. const userPrize = this.prizeInfo.find(item => {
  864. return String(item.userId) == String(this.userData.userId)
  865. })
  866. return userPrize ? userPrize.recordId : null
  867. }
  868. },
  869. onHide() {
  870. if (this.talklist && this.talklist.length > 20) {
  871. this.talklist = this.talklist.slice(-20)
  872. }
  873. // 保存观看时长
  874. this.updateWatchDuration();
  875. this.stopCountdown();
  876. // 清理流量定时器
  877. if (this.$refs && this.$refs.liveVideo && typeof this.$refs.liveVideo.clearAllTimers === 'function') {
  878. this.$refs.liveVideo.clearAllTimers()
  879. }
  880. },
  881. onUnload() {
  882. if (this.$refs && this.$refs.liveVideo && typeof this.$refs.liveVideo.saveVideoProgress === 'function') {
  883. this.$refs.liveVideo.saveVideoProgress()
  884. }
  885. // 暂停视频
  886. if (this.$refs && this.$refs.liveVideo && typeof this.$refs.liveVideo.pauseVideo === 'function') {
  887. this.$refs.liveVideo.pauseVideo()
  888. }
  889. // 清理流量定时器
  890. if (this.$refs && this.$refs.liveVideo && typeof this.$refs.liveVideo.clearAllTimers === 'function') {
  891. this.$refs.liveVideo.clearAllTimers()
  892. }
  893. // 清理liveItem中的定时器
  894. if (this.liveItem && this.liveItem.timeTimer) {
  895. clearInterval(this.liveItem.timeTimer)
  896. this.liveItem.timeTimer = null
  897. }
  898. // 更新后端观看时长
  899. this.updateWatchDuration();
  900. this.stopCountdown();
  901. this.closeWebSocket(true);
  902. this.clearAllTimersEnhanced();
  903. const videoId = `myVideo_${this.liveId}`
  904. const videoContext = uni.createVideoContext(videoId, this)
  905. if (videoContext) {
  906. videoContext.pause();
  907. }
  908. try {
  909. uni.offNetworkStatusChange();
  910. } catch (err) {
  911. console.warn('移除网络状态监听失败:', err)
  912. }
  913. this.clearBigData();
  914. this.resetAllStates();
  915. },
  916. mounted() {
  917. this.systemInfo = uni.getSystemInfoSync()
  918. console.log('系统信息:', this.systemInfo.platform, this.systemInfo.model)
  919. // this.getCurrentActivities()
  920. this.getliveOrder()
  921. },
  922. watch: {
  923. 'orderUser.count': {
  924. handler(newVal, oldVal) {
  925. if (newVal !== this.prevOrderCount) {
  926. this.prevOrderCount = newVal
  927. this.showPurchaseMessage()
  928. }
  929. },
  930. immediate: true
  931. }
  932. },
  933. methods: {
  934. // getUserLiveInfo(){
  935. // getUserLiveInfo().then((res) => {
  936. // if (res.code == 200) {
  937. // const liveUserId=res.user.userId
  938. // uni.setStorageSync('liveUserId', liveUserId)
  939. // //console.log('liveUser', res.user)
  940. // } else {
  941. // uni.showToast({
  942. // icon: 'none',
  943. // title: '请求失败'
  944. // })
  945. // }
  946. // },
  947. // (rej) => {}
  948. // )
  949. // },
  950. // 芳华币
  951. // 微信昵称授权相关方法
  952. shouquan() {
  953. if (this.user.nickname == '') {
  954. uni.showToast({
  955. icon: 'none',
  956. title: "请先授权微信昵称",
  957. });
  958. }
  959. },
  960. //app发送H5信息
  961. postMessage(data) {
  962. console.log('接收数据:',data)
  963. this.uniReady(() => {
  964. uni.webView.postMessage({
  965. data: data
  966. });
  967. })
  968. },
  969. uniReady(callback) {
  970. if (uni.webView) {
  971. callback()
  972. } else {
  973. document.addEventListener('UniAppJSBridgeReady', callback, { once: true })
  974. }
  975. },
  976. plusReady(callback) {
  977. if (window.plus) {
  978. callback()
  979. } else {
  980. document.addEventListener('plusready', callback, { once: true })
  981. }
  982. },
  983. confimrname() {
  984. if (this.user.nickname == '') {
  985. uni.showToast({
  986. icon: 'none',
  987. title: "请授权微信昵称",
  988. });
  989. return
  990. }
  991. this.editUser();
  992. uni.setStorageSync('userInfo', this.userInfo);
  993. if (this.user.nickname) {
  994. this.userlogo = false;
  995. }
  996. },
  997. onNickNameInput(e) {
  998. this.user.nickname = e.detail.value;
  999. },
  1000. editUser() {
  1001. this.user.nickName = this.user.nickname;
  1002. editUser(this.user).then(
  1003. res => {
  1004. if (res.code == 200) {
  1005. uni.showToast({
  1006. icon: 'success',
  1007. title: "修改成功",
  1008. });
  1009. this.getUserInfo()
  1010. } else {
  1011. uni.showToast({
  1012. icon: 'none',
  1013. title: res.msg,
  1014. });
  1015. }
  1016. },
  1017. rej => {}
  1018. );
  1019. },
  1020. // 静默登录
  1021. goLogin(data) {
  1022. let provider = 'weixin'
  1023. uni.login({
  1024. provider: provider,
  1025. success: async loginRes => {
  1026. uni.getUserInfo({
  1027. provider: provider,
  1028. success: (infoRes) => {
  1029. uni.showToast({
  1030. title: '登录中...',
  1031. icon: 'loading'
  1032. });
  1033. loginByMp({
  1034. code: loginRes.code,
  1035. encryptedData: infoRes.encryptedData,
  1036. iv: infoRes.iv,
  1037. appId: this.appid
  1038. }).then(res => {
  1039. uni.hideLoading();
  1040. if (res.code == 200) {
  1041. uni.setStorageSync('AppToken', res
  1042. .token);
  1043. if(res.liveToken){
  1044. uni.setStorageSync('liveToken', res.liveToken);
  1045. }
  1046. uni.setStorageSync('userInfo', JSON
  1047. .stringify(res
  1048. .user));
  1049. if(!res.user.createTime){
  1050. this.getUserInfo()
  1051. }else{
  1052. uni.setStorageSync('userData', JSON.stringify(res.user));
  1053. }
  1054. this.userInfo = uni.getStorageSync('userInfo');
  1055. this.userData = uni.getStorageSync('userData');
  1056. // uni.setStorageSync('auto_userInfo', JSON.stringify(res.user));
  1057. // this.user = res.user
  1058. this.$store.commit('setCoureLogin', 1);
  1059. this.isLogin = true
  1060. this.haveLogin()
  1061. this.userlogo = true
  1062. // console.log("TOKEN_KEYAuto",TOKEN_KEYAuto)
  1063. if (this.urlOption.qwUserId) {
  1064. this.getIsAddKf() //this.getIsAddKf()
  1065. }
  1066. } else {
  1067. uni.showToast({
  1068. title: res.msg,
  1069. icon: 'none'
  1070. });
  1071. }
  1072. }).catch(err => {
  1073. uni.hideLoading();
  1074. uni.showToast({
  1075. icon: 'none',
  1076. title: "登录失败,请重新登录",
  1077. });
  1078. });
  1079. }
  1080. });
  1081. }
  1082. })
  1083. },
  1084. async haveLogin() {
  1085. // 防止重复执行
  1086. if (this.isOnload) {
  1087. return;
  1088. }
  1089. // this.userInfo = uni.getStorageSync('userInfo');
  1090. // if (this.userInfo) {
  1091. // await this.getUserInfo();
  1092. // }
  1093. if (this.liveId) {
  1094. // 优先加载直播间信息(包含视频资源URL)
  1095. this.isOnload = true;
  1096. const userInfo = uni.getStorageSync('userInfo');
  1097. if (userInfo) {
  1098. await this.getliving(this.liveId);
  1099. }
  1100. Promise.all([
  1101. this.getLiveMsg(this.liveItem),
  1102. this.getliveViewData()
  1103. ]).catch(err => {
  1104. console.error('加载数据失败:', err);
  1105. });
  1106. // 其他非关键数据异步加载
  1107. this.getCurrentActivities();
  1108. this.getliveOrder();
  1109. this.initSocket();
  1110. this.getUserIntegralInfo();
  1111. }
  1112. },
  1113. getUserIntegralInfo() {
  1114. getUserIntegralInfo().then(res => {
  1115. if (res.code == 200) {
  1116. this.integralNum = res.data.integral
  1117. }
  1118. }).catch(error => {
  1119. console.error("获取芳华币数据失败:", error);
  1120. });
  1121. },
  1122. //礼物列表
  1123. getActiveList() {
  1124. activeList().then((res) => {
  1125. if (res.code == 200) {
  1126. this.giftList = res.data
  1127. //console.log('正常状态的礼物列表', res.data)
  1128. }
  1129. }).catch((error) => {})
  1130. },
  1131. // 送礼物
  1132. sendliveGift(item) {
  1133. if (!this.liveId) return
  1134. const data = {
  1135. liveId: this.liveId,
  1136. giftId: item.giftId,
  1137. // giftId: this.giftId,
  1138. // cn: this.cn
  1139. cn: 1
  1140. }
  1141. sendliveGift(data).then((res) => {
  1142. if (res.code == 200) {
  1143. uni.showToast({
  1144. title: res.msg,
  1145. icon: 'none'
  1146. })
  1147. console.log('用户送礼物', res)
  1148. this.getUserIntegralInfo();
  1149. } else {
  1150. uni.showToast({
  1151. title: res.msg,
  1152. icon: 'none'
  1153. })
  1154. }
  1155. }).catch((error) => {})
  1156. },
  1157. // 查询当前用户当前直播间领取积分的剩余时长
  1158. getRemainingTime() {
  1159. if (!this.liveId) return
  1160. const data = {
  1161. liveId: this.liveId
  1162. }
  1163. remainingTime(data).then((res) => {
  1164. if (res.code == 200) {
  1165. this.hasReceived = res.data.hasReceived
  1166. this.videoDuration = res.data.videoDuration
  1167. this.watchDuration = res.data.watchDuration
  1168. this.remainingTime = res.data.remainingTime
  1169. this.completionRate = res.data.completionRate
  1170. console.log('查询当前用户当前直播间领取积分的剩余时长', res)
  1171. }
  1172. }).catch((error) => {})
  1173. },
  1174. // 查询用户积分领取记录
  1175. // completionRecords() {
  1176. // if (!this.liveId) return
  1177. // const data = {
  1178. // liveId: this.liveId
  1179. // }
  1180. // completionRecords(data).then((res) => {
  1181. // if (res.code == 200) {
  1182. // const targetData = res.data.find(item => item.liveId == this.liveId)
  1183. // if (targetData) {
  1184. // const receiveStatus = targetData.receiveStatus
  1185. // this.receiveStatus = receiveStatus
  1186. // } else {
  1187. // console.log('未找到liveId为', this.liveId, '的数据')
  1188. // this.receiveStatus = false
  1189. // }
  1190. // console.log('查询用户积分领取记录', res)
  1191. // }
  1192. // }).catch((error) => {})
  1193. // },
  1194. // 更新用户的看课时长
  1195. updateWatchDuration() {
  1196. if (!this.liveId) return;
  1197. if (this.isPreview) return;
  1198. //const watchDuration = Math.floor((Date.now() - this.liveBeginWatchTime) / 1000)
  1199. const watchDuration = this.totalWatchTime - this.watchDuration;
  1200. updateWatchDuration(this.liveId, watchDuration).then((res) => {
  1201. if (res.code == 200) {
  1202. console.log("更新用户的看课时长", res);
  1203. this.receivePoints();
  1204. }
  1205. }).catch((error) => {
  1206. });
  1207. },
  1208. // 用户领取看课积分 (传入直播间id)
  1209. receivePoints(retryCount = 0) {
  1210. if (!this.liveId) return;
  1211. // const data = {
  1212. // liveId: this.liveId
  1213. // }
  1214. receivePoints(this.liveId).then((res) => {
  1215. if (res.code == 200) {
  1216. console.log("用户领取看课积分", res);
  1217. //清除重试定时器
  1218. if (this.pointsRetryTimer) {
  1219. clearTimeout(this.pointsRetryTimer);
  1220. this.pointsRetryTimer = null;
  1221. }
  1222. }
  1223. }).catch((error) => {
  1224. console.log("用户领取积分失败error", error);
  1225. // 增加重试机制:最多重试3次
  1226. if (retryCount < 3) {
  1227. const delay = (retryCount + 1) * 2000; // 递增延迟: 2s, 4s, 6s
  1228. console.log(`领取积分失败,${delay}ms后进行第${retryCount + 1}次重试`);
  1229. if (this.pointsRetryTimer) {
  1230. clearTimeout(this.pointsRetryTimer);
  1231. }
  1232. this.pointsRetryTimer = setTimeout(() => {
  1233. this.receivePoints(retryCount + 1);
  1234. }, delay);
  1235. }
  1236. });
  1237. },
  1238. // 开始观看时长统计
  1239. // startWatchDurationTracking() {
  1240. // if (this.watchStartTime > 0) {
  1241. // console.log('观看时长统计已启动,跳过')
  1242. // return
  1243. // }
  1244. // this.watchStartTime = Date.now()
  1245. // this.isPageVisible = true
  1246. // console.log('开始观看时长统计', new Date(this.watchStartTime).toLocaleString())
  1247. // this.initPageVisibilityListener()
  1248. // },
  1249. // getCurrentWatchDuration() {
  1250. // if (this.watchStartTime === 0) {
  1251. // return this.accumulatedWatchDuration
  1252. // }
  1253. // if (this.isPageVisible) {
  1254. // const currentDuration = Math.floor((Date.now() - this.watchStartTime) / 1000)
  1255. // return this.accumulatedWatchDuration + currentDuration
  1256. // }
  1257. // return this.accumulatedWatchDuration
  1258. // },
  1259. // pauseWatchDurationTracking() {
  1260. // if (!this.isPageVisible || this.watchStartTime === 0) {
  1261. // return
  1262. // }
  1263. // const currentSessionDuration = Math.floor((Date.now() - this.watchStartTime) / 1000)
  1264. // this.accumulatedWatchDuration += currentSessionDuration
  1265. // this.lastPauseTime = Date.now()
  1266. // this.isPageVisible = false
  1267. // console.log(`暂停观看统计: 本次=${currentSessionDuration}秒, 累计=${this.accumulatedWatchDuration}秒`)
  1268. // },
  1269. // resumeWatchDurationTracking() {
  1270. // if (this.isPageVisible) {
  1271. // return
  1272. // }
  1273. // this.watchStartTime = Date.now()
  1274. // this.isPageVisible = true
  1275. // const pauseDuration = this.lastPauseTime > 0 ? Math.floor((Date.now() - this.lastPauseTime) / 1000) : 0
  1276. // console.log(`恢复观看统计: 暂停了${pauseDuration}秒, 当前累计=${this.accumulatedWatchDuration}秒`)
  1277. // },
  1278. // stopWatchDurationTracking() {
  1279. // if (this.isPageVisible && this.watchStartTime > 0) {
  1280. // const currentSessionDuration = Math.floor((Date.now() - this.watchStartTime) / 1000)
  1281. // this.accumulatedWatchDuration += currentSessionDuration
  1282. // }
  1283. // console.log(`停止观看统计: 总时长=${this.accumulatedWatchDuration}秒`)
  1284. // this.watchStartTime = 0
  1285. // this.isPageVisible = true
  1286. // this.lastPauseTime = 0
  1287. // this.removePageVisibilityListener()
  1288. // },
  1289. // initPageVisibilityListener() {
  1290. // uni.onAppShow(() => {
  1291. // console.log('小程序回到前台')
  1292. // this.resumeWatchDurationTracking()
  1293. // })
  1294. // uni.onAppHide(() => {
  1295. // console.log('小程序切换到后台')
  1296. // this.pauseWatchDurationTracking()
  1297. // })
  1298. // },
  1299. removePageVisibilityListener() {},
  1300. calculateLiveTimeDiff(timeStr, useServerTime = false) {
  1301. // 0. 安全校验:防止 undefined/null 导致 crash
  1302. if (!timeStr) {
  1303. return 0;
  1304. }
  1305. // 1. 获取当前时间戳(毫秒)- 倒计时场景使用服务器时间
  1306. const now = this.getServerTimeNow();
  1307. // 2. 计算差值(毫秒 -> 秒)
  1308. // 结果为:当前时间 - 直播开始时间
  1309. const diffInSeconds = Math.floor((now - timeStr) / 1000);
  1310. return diffInSeconds;
  1311. },
  1312. calculateLiveEndTime(startTime, duration) {
  1313. if (!startTime) return '';
  1314. // 计算结束时间戳
  1315. const endTimeMs = startTime + (Number(duration) * 1000);
  1316. const endDate = new Date(endTimeMs);
  1317. // 格式化
  1318. const year = endDate.getFullYear();
  1319. const month = this.padZero(endDate.getMonth() + 1);
  1320. const day = this.padZero(endDate.getDate());
  1321. const hours = this.padZero(endDate.getHours());
  1322. const minutes = this.padZero(endDate.getMinutes());
  1323. const seconds = this.padZero(endDate.getSeconds());
  1324. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  1325. },
  1326. startCountdown() {
  1327. // 如果已经领取过积分,不启动倒计时
  1328. if (this.hasReceived) {
  1329. return;
  1330. }
  1331. if (!this.liveItem || Array.isArray(this.liveItem) || !this.liveItem.completionPointsEnabled) {
  1332. return;
  1333. }
  1334. // 如果倒计时已经在运行,先清除旧的定时器
  1335. if (this.countdownTimer) {
  1336. clearInterval(this.countdownTimer);
  1337. this.countdownTimer = null;
  1338. }
  1339. // 使用服务器时间重新计算初始状态,确保设备时间不一致时也能正确判断
  1340. const liveStartTime = this.getLiveStartTime();
  1341. if (!liveStartTime) {
  1342. return;
  1343. }
  1344. this.diffLiveStartTime = this.calculateLiveTimeDiff(liveStartTime, true);
  1345. // 如果直播已开始,确保 isPreview 为 false
  1346. if (this.diffLiveStartTime >= 0) {
  1347. this.isPreview = false;
  1348. }
  1349. this.isCountdownActive = true;
  1350. // 立即执行一次更新,确保倒计时显示正确
  1351. this.updateCountdown();
  1352. this.countdownTimer = setInterval(() => {
  1353. this.updateCountdown();
  1354. }, 1000); // 每秒更新一次
  1355. },
  1356. updateCountdown() {
  1357. // 如果已经领取过积分,停止倒计时
  1358. if (this.hasReceived) {
  1359. this.stopCountdown();
  1360. return;
  1361. }
  1362. if (!this.liveItem || Array.isArray(this.liveItem)) {
  1363. return;
  1364. }
  1365. // 使用服务器时间重新计算初始状态,确保设备时间不一致时也能正确判断
  1366. const liveStartTime = this.getLiveStartTime();
  1367. if (!liveStartTime) {
  1368. return;
  1369. }
  1370. this.diffLiveStartTime = this.calculateLiveTimeDiff(liveStartTime, true);
  1371. // 如果直播已开始,确保 isPreview 为 false
  1372. if (this.diffLiveStartTime >= 0) {
  1373. this.isPreview = false;
  1374. }
  1375. this.completionTime = this.liveItem.duration * this.completionRate * 0.01;
  1376. // 已达成观看时间
  1377. if (this.watchDuration > this.completionTime) {
  1378. if (this.countdownTimer) {
  1379. clearInterval(this.countdownTimer);
  1380. this.countdownTimer = null;
  1381. this.isPreview = true;
  1382. return;
  1383. }
  1384. }
  1385. console.log('this.liveItem.duration', this.liveItem.duration);
  1386. console.log('this.completionRate', this.completionRate);
  1387. console.log('this.completionTime', this.completionTime);
  1388. // 倒计时已结束
  1389. if (this.pointsRemainingTime < 0) {
  1390. this.stopCountdown();
  1391. return;
  1392. }
  1393. this.showPoints = false;
  1394. // 使用服务器时间计算差值
  1395. const serverTimeNow = this.getServerTimeNow();
  1396. this.diffLiveStartTime = this.calculateLiveTimeDiff(liveStartTime, true);
  1397. this.diffLiveEndTime = this.calculateLiveTimeDiff(liveStartTime, true) - this.liveItem.duration;
  1398. if (!this.liveItem.finishTime) {
  1399. this.liveItem.finishTime = this.calculateLiveEndTime(liveStartTime, this.liveItem.duration);
  1400. }
  1401. // 预告状态
  1402. if (this.diffLiveStartTime < 0) {
  1403. this.isPreview = true;
  1404. return;
  1405. } else {
  1406. // 直播已开始(基于服务器时间),确保 isPreview 为 false
  1407. this.isPreview = false;
  1408. }
  1409. if (this.diffLiveStartTime >= 0) {
  1410. if (this.diffLiveEndTime >= 0) {
  1411. // 直播回放中
  1412. if (this.hasReplayGeneration) {
  1413. // 经历过回放生成 回放生成后扣减掉经历回放生成的时间
  1414. this.totalWatchTime = this.watchDuration + Math.floor((serverTimeNow - this
  1415. .liveBeginWatchTime) / 1000) - this.diffReplayGenerationSeconds;
  1416. } else {
  1417. // 进入直播间时已过回放生成中
  1418. this.totalWatchTime = this.watchDuration + Math.floor((serverTimeNow - this
  1419. .liveBeginWatchTime) / 1000);
  1420. }
  1421. } else {
  1422. // 直播中状态
  1423. this.totalWatchTime = this.watchDuration + Math.floor((serverTimeNow - this.liveBeginWatchTime) /
  1424. 1000);
  1425. }
  1426. // 积分领取满足条件
  1427. if (this.totalWatchTime >= this.completionTime) {
  1428. if (this.countdownTimer) {
  1429. clearInterval(this.countdownTimer);
  1430. this.countdownTimer = null;
  1431. this.onCountdownComplete();
  1432. return;
  1433. }
  1434. }
  1435. this.isPreview = false;
  1436. // 直播中或回放状态都计算剩余时间
  1437. if (this.diffLiveStartTime >= 0) {
  1438. if (this.diffLiveEndTime < 0) {
  1439. // 直播中状态
  1440. this.pointsRemainingTime = Math.max(0, this.completionTime - this.totalWatchTime);
  1441. }
  1442. }
  1443. }
  1444. // 更新显示
  1445. this.updateCountdownDisplay();
  1446. },
  1447. stopCountdown() {
  1448. if (this.countdownTimer) {
  1449. clearInterval(this.countdownTimer)
  1450. this.countdownTimer = null
  1451. }
  1452. this.isCountdownActive = false
  1453. this.updateWatchDuration()
  1454. },
  1455. onCountdownComplete() {
  1456. console.log('倒计时完成!')
  1457. this.stopCountdown()
  1458. this.pointsRemainingTime = -1
  1459. this.triggerWatchComplete()
  1460. setTimeout(() => {
  1461. this.showToast = true
  1462. setTimeout(() => {
  1463. this.showToast = false
  1464. }, 5000)
  1465. }, 1000)
  1466. },
  1467. async triggerWatchComplete() {
  1468. if (!this.userInfo?.userId || !this.liveId) return
  1469. try {
  1470. console.log('用户已完成观看')
  1471. uni.removeStorageSync(this.watchProgressKey)
  1472. } catch (error) {
  1473. console.error('触发观看完成事件失败:', error)
  1474. }
  1475. },
  1476. padZero(num) {
  1477. return num < 10 ? `0${num}` : num.toString()
  1478. },
  1479. formatCountdown(seconds) {
  1480. const totalSeconds = Math.max(0, Math.floor(Number(seconds) || 0))
  1481. const hours = Math.floor(totalSeconds / 3600)
  1482. const remainingAfterHours = totalSeconds % 3600
  1483. const minutes = Math.floor(remainingAfterHours / 60)
  1484. const secs = remainingAfterHours % 60
  1485. return {
  1486. hours: this.padZero(hours),
  1487. minutes: this.padZero(minutes),
  1488. seconds: this.padZero(secs),
  1489. total: totalSeconds
  1490. }
  1491. },
  1492. getCountdownPercentage() {
  1493. if (!this.liveItem || !this.liveItem.duration) {
  1494. return 0
  1495. }
  1496. if (this.hasReachedTarget || this.pointsRemainingTime <= 0) {
  1497. console.log("这个比例是多少100",this.pointsRemainingTime)
  1498. return 100
  1499. }
  1500. const completionTime = this.liveItem.duration * this.completionRate * 0.01;
  1501. const watchedTime = this.totalWatchTime;
  1502. if (watchedTime <= 0) {
  1503. return 0
  1504. }
  1505. const percentage = (watchedTime / completionTime) * 100
  1506. console.log("这个比例是多少计算",percentage)
  1507. return Math.min(100, Math.max(0, percentage))
  1508. },
  1509. updateCountdownDisplay() {
  1510. this.$forceUpdate()
  1511. },
  1512. initWatchTime() {
  1513. const userId = this.userInfo?.userId || 'anonymous'
  1514. this.watchTimeStorageKey = `watchTime_${userId}_${this.liveId}`
  1515. try {
  1516. const storedTime = uni.getStorageSync(this.watchTimeStorageKey)
  1517. this.totalWatchTime = storedTime ? parseInt(storedTime) : 0
  1518. console.log(`加载累计观看时间: ${this.totalWatchTime}秒`)
  1519. } catch (error) {
  1520. console.error('加载观看时间失败:', error)
  1521. this.totalWatchTime = 0
  1522. }
  1523. },
  1524. saveWatchTime() {
  1525. if (!this.watchTimeStorageKey) return
  1526. try {
  1527. uni.setStorageSync(this.watchTimeStorageKey, this.totalWatchTime)
  1528. console.log(`保存观看时间: ${this.totalWatchTime}秒`)
  1529. } catch (error) {
  1530. console.error('保存观看时间失败:', error)
  1531. }
  1532. },
  1533. formatWatchTime(seconds) {
  1534. const hours = Math.floor(seconds / 3600)
  1535. const minutes = Math.floor((seconds % 3600) / 60)
  1536. const secs = seconds % 60
  1537. return {
  1538. hours: this.padZero(hours),
  1539. minutes: this.padZero(minutes),
  1540. seconds: this.padZero(secs),
  1541. total: seconds
  1542. }
  1543. },
  1544. handleFullscreenChange(isFullscreen) {
  1545. console.log('收到子组件全屏状态:', isFullscreen)
  1546. this.isFullscreen = isFullscreen
  1547. if (isFullscreen) {
  1548. this.shopping = false
  1549. this.isShowGift = false
  1550. this.isMore = false
  1551. this.showViewerList = false
  1552. this.isShowLotteryPop = false
  1553. this.winning = false
  1554. this.isFocus = false
  1555. }
  1556. this.$nextTick(() => {
  1557. this.$forceUpdate()
  1558. })
  1559. },
  1560. onInputFocus(value) {
  1561. this.isFocus = true
  1562. console.log('输入框聚焦:', value)
  1563. },
  1564. onInputBlur(value) {
  1565. this.isFocus = false
  1566. console.log('输入框失焦:', value)
  1567. },
  1568. onSendMessage(message) {
  1569. console.log('发送消息:', message)
  1570. this.sendMsg()
  1571. },
  1572. onInputChange(value) {
  1573. this.value = value
  1574. },
  1575. handleLiveStart() {
  1576. console.log('直播开始')
  1577. const userInfo = uni.getStorageSync('userInfo');
  1578. const isLiveLogin = uni.getStorageSync('isLiveLogin')
  1579. if (userInfo && isLiveLogin) {
  1580. this.getliving(this.liveId)
  1581. }
  1582. },
  1583. handleSearchInput(keyword) {
  1584. this.inputInfo = keyword
  1585. clearTimeout(this.searchTimer)
  1586. this.searchTimer = setTimeout(() => {
  1587. this.queryCollect()
  1588. }, 500)
  1589. },
  1590. handleVideoError(error) {
  1591. console.error('视频播放错误:', error)
  1592. },
  1593. handleLiveStateChange(state) {
  1594. console.log('直播状态变化:', state)
  1595. },
  1596. handleNavigateToOrder() {
  1597. //this.navgetTo('/pages_live/shopping/order')
  1598. this.postMessage({login: 1,pagesUrl:'/pages_live/shopping/order'})
  1599. },
  1600. handleShowMore() {
  1601. this.isMore = true
  1602. this.shopping = false
  1603. },
  1604. handleFillAddress(data) {
  1605. const {
  1606. productId,
  1607. recordId,
  1608. liveId
  1609. } = data
  1610. this.winning = false
  1611. // this.navgetTo(
  1612. // `/pages_live/shopping/confirmCreateOrder?type=win&productId=${productId}&liveId=${liveId}&recordId=${recordId}`
  1613. // )
  1614. this.postMessage({login: 1,pagesUrl:`/pages_live/shopping/confirmCreateOrder?type=win&productId=${productId}&liveId=${liveId}&recordId=${recordId}`})
  1615. },
  1616. handleLotteryClose() {
  1617. this.isShowLotteryPop = false
  1618. },
  1619. handleLotteryClaim() {
  1620. this.onClaim()
  1621. },
  1622. handleGoShop(goodsData) {
  1623. this.goShop(goodsData.productId, goodsData.goodsId)
  1624. },
  1625. handleCloseGoods() {
  1626. this.isShowGoods = false
  1627. },
  1628. formatNumber(num) {
  1629. if (typeof num !== 'number') {
  1630. num = Number(num) || 0
  1631. }
  1632. if (num < 10000) {
  1633. return num.toString()
  1634. }
  1635. const wan = num / 10000
  1636. if (wan < 10) {
  1637. const rounded = Math.round(wan * 100) / 100
  1638. return rounded.toFixed(2).replace(/\.?0+$/, '') + 'w'
  1639. } else if (wan < 10000) {
  1640. const rounded = Math.round(wan * 10) / 10
  1641. return rounded.toFixed(1).replace(/\.0$/, '') + 'w'
  1642. } else {
  1643. const yi = wan / 10000
  1644. const rounded = Math.round(yi * 100) / 100
  1645. return rounded.toFixed(2).replace(/\.?0+$/, '') + '亿'
  1646. }
  1647. },
  1648. resetAllStates() {
  1649. this.liveUserCalled = false
  1650. this.talklist = []
  1651. this.liveViewersData = []
  1652. this.liveViewers = []
  1653. this.products = []
  1654. this.liveItem = null
  1655. this.isSocketOpen = false
  1656. this.isConnecting = false
  1657. this.isManualClose = true
  1658. this.reconnectCount = 0
  1659. this.heartBeatRetryCount = 0
  1660. this.lastHeartBeatTime = 0
  1661. this.adaptiveHeartBeatInterval = this.heartBeatInterval
  1662. this.isNetworkAvailable = true
  1663. this.networkType = 'unknown'
  1664. this.connectionStartTime = 0
  1665. this.connectionLatency = 0
  1666. this.messageCount = 0
  1667. this.errorCount = 0
  1668. this.lastPerformanceCheck = 0
  1669. },
  1670. getWebSocketPerformanceStats() {
  1671. const now = Date.now()
  1672. const uptime = this.connectionStartTime > 0 ? now - this.connectionStartTime : 0
  1673. return {
  1674. connectionLatency: this.connectionLatency,
  1675. uptime: uptime,
  1676. messageCount: this.messageCount,
  1677. errorCount: this.errorCount,
  1678. errorRate: this.messageCount > 0 ? (this.errorCount / this.messageCount * 100).toFixed(2) : 0,
  1679. messagesPerSecond: uptime > 0 ? (this.messageCount / (uptime / 1000)).toFixed(2) : 0,
  1680. networkType: this.networkType,
  1681. isConnected: this.isSocketAvailable(),
  1682. reconnectCount: this.reconnectCount
  1683. }
  1684. },
  1685. performanceCheck() {
  1686. const now = Date.now()
  1687. if (now - this.lastPerformanceCheck > 300000) {
  1688. const stats = this.getWebSocketPerformanceStats()
  1689. console.log('WebSocket性能统计:', stats)
  1690. this.lastPerformanceCheck = now
  1691. if (stats.errorRate > 10) {
  1692. console.warn(`WebSocket错误率过高: ${stats.errorRate}%`)
  1693. }
  1694. if (stats.connectionLatency > 5000) {
  1695. console.warn(`WebSocket连接延迟过高: ${stats.connectionLatency}ms`)
  1696. }
  1697. }
  1698. },
  1699. startVideoCacheCleanup() {
  1700. if (this.videoCleanupTimer) {
  1701. clearInterval(this.videoCleanupTimer)
  1702. }
  1703. this.videoCleanupTimer = setInterval(() => {
  1704. this.cleanupVideoCache()
  1705. }, 30000)
  1706. },
  1707. cleanupVideoCache() {
  1708. if (!this.liveItem || !this.liveId) return
  1709. if (this.liveItem.liveType === 2 && this.liveItem.videoUrl) {
  1710. this.reloadVideoPlayer()
  1711. }
  1712. },
  1713. reloadVideoPlayer() {
  1714. const videoId = `myVideo_${this.liveId}`
  1715. const videoContext = uni.createVideoContext(videoId, this)
  1716. if (videoContext) {
  1717. const currentTime = this.videoCurrentTime
  1718. videoContext.pause()
  1719. setTimeout(() => {
  1720. this.$set(this.liveItem, 'videoUrl', this.liveItem.videoUrl + '&t=' + Date.now())
  1721. setTimeout(() => {
  1722. if (videoContext.seek) {
  1723. videoContext.seek(currentTime)
  1724. }
  1725. videoContext.play()
  1726. }, 500)
  1727. }, 100)
  1728. }
  1729. },
  1730. startMemoryMonitoring() {
  1731. if (this.memoryMonitorTimer) {
  1732. clearInterval(this.memoryMonitorTimer)
  1733. }
  1734. this.memoryMonitorTimer = setInterval(() => {
  1735. this.checkMemoryUsage()
  1736. }, 15000)
  1737. },
  1738. checkMemoryUsage() {
  1739. try {
  1740. const timerCount = this.getActiveTimerCount()
  1741. if (timerCount > 10) {
  1742. console.warn(`检测到过多定时器: ${timerCount}个,可能存在内存泄漏`)
  1743. this.cleanupUnusedTimers()
  1744. }
  1745. if (this.messageQueue && this.messageQueue.length > 100) {
  1746. console.warn(`消息队列过长: ${this.messageQueue.length}条,清理旧消息`)
  1747. this.messageQueue = this.messageQueue.slice(-50)
  1748. }
  1749. if (this.userRandomColors && Object.keys(this.userRandomColors).length > 500) {
  1750. console.warn('用户颜色缓存过大,清理部分缓存')
  1751. const keys = Object.keys(this.userRandomColors)
  1752. const keysToRemove = keys.slice(0, keys.length - 200)
  1753. keysToRemove.forEach(key => delete this.userRandomColors[key])
  1754. }
  1755. if (Date.now() - this.lastPerformanceCheck > 60000) {
  1756. this.triggerGarbageCollection()
  1757. this.lastPerformanceCheck = Date.now()
  1758. }
  1759. } catch (error) {
  1760. console.error('内存检查失败:', error)
  1761. }
  1762. },
  1763. forceMemoryCleanup() {
  1764. console.log('执行强制内存清理')
  1765. this.cleanupVideoPlayer()
  1766. this.clearBigData()
  1767. this.triggerGarbageCollection()
  1768. },
  1769. cleanupVideoPlayer() {
  1770. const videoId = `myVideo_${this.liveId}`
  1771. const videoContext = uni.createVideoContext(videoId, this)
  1772. if (videoContext) {
  1773. videoContext.stop()
  1774. setTimeout(() => {
  1775. this.$set(this.liveItem, 'videoUrl', '')
  1776. setTimeout(() => {
  1777. this.$set(this.liveItem, 'videoUrl', this.getFreshVideoUrl())
  1778. if (this.$refs && this.$refs.liveVideo && typeof this.$refs.liveVideo.playVideo === 'function') {
  1779. this.$refs.liveVideo.playVideo()
  1780. }
  1781. }, 500)
  1782. }, 100)
  1783. }
  1784. },
  1785. getFreshVideoUrl() {
  1786. if (!this.liveItem.originalVideoUrl) {
  1787. this.liveItem.originalVideoUrl = this.liveItem.videoUrl
  1788. }
  1789. const separator = this.liveItem.originalVideoUrl.includes('?') ? '&' : '?'
  1790. return this.liveItem.originalVideoUrl + separator + 't=' + Date.now()
  1791. },
  1792. clearBigData() {
  1793. if (this.talklist && this.talklist.length > 50) {
  1794. this.talklist = this.talklist.slice(-50)
  1795. }
  1796. if (this.liveViewersData && this.liveViewersData.length > 100) {
  1797. this.liveViewersData = this.liveViewersData.slice(-100)
  1798. }
  1799. if (this.liveViewers && this.liveViewers.length > 100) {
  1800. this.liveViewers = this.liveViewers.slice(-100)
  1801. }
  1802. if (this.products && this.products.length > 50) {
  1803. this.products = this.products.slice(0, 50)
  1804. }
  1805. if (typeof gc === 'function') {
  1806. gc()
  1807. }
  1808. },
  1809. triggerGarbageCollection() {
  1810. if (wx && wx.triggerGC) {
  1811. wx.triggerGC()
  1812. }
  1813. },
  1814. forceScrollToBottom() {
  1815. console.log('执行强制滚动到底部')
  1816. this.scrollTop = 999999
  1817. this.$nextTick(() => {
  1818. if (this.talklist && this.talklist.length > 0) {
  1819. const lastMessage = this.talklist[this.talklist.length - 1]
  1820. const targetId = `list_${lastMessage.uniqueId || (this.talklist.length - 1)}`
  1821. console.log(
  1822. `尝试滚动到元素: ${targetId}, 当前消息数量: ${this.talklist.length}, 最后消息ID: ${lastMessage.uniqueId}`
  1823. )
  1824. this.scrollIntoView = targetId
  1825. setTimeout(() => {
  1826. this.scrollIntoView = ''
  1827. }, 200)
  1828. }
  1829. setTimeout(() => {
  1830. this.scrollTop = 999999
  1831. console.log('延迟设置scrollTop为999999')
  1832. }, 100)
  1833. setTimeout(() => {
  1834. this.nativeScrollToBottom()
  1835. }, 300)
  1836. })
  1837. },
  1838. simpleScrollToBottom() {
  1839. const now = Date.now()
  1840. if (now - this.lastScrollTime < this.scrollDebounceDelay) {
  1841. console.log('滚动防抖:忽略频繁调用')
  1842. return
  1843. }
  1844. this.lastScrollTime = now
  1845. console.log('执行可靠滚动到底部')
  1846. if (this.scrollTimer) {
  1847. clearTimeout(this.scrollTimer)
  1848. this.scrollTimer = null
  1849. }
  1850. // H5 环境下的特殊处理
  1851. // #ifdef H5
  1852. console.log('H5环境:simpleScrollToBottom 滚动')
  1853. // 先清空 scroll-into-view
  1854. this.scrollIntoView = ''
  1855. // 使用递增的值确保每次都触发滚动
  1856. this.scrollTop = now
  1857. console.log(`方案1: 设置scrollTop为${now}`)
  1858. // 延迟多次设置,确保 DOM 更新后能滚动到底部
  1859. this.scrollTimer = setTimeout(() => {
  1860. this.scrollTop = now + 1
  1861. console.log(`方案2: 延迟设置scrollTop为${now + 1}`)
  1862. this.scrollTimer = null
  1863. }, 50)
  1864. setTimeout(() => {
  1865. this.scrollTop = 999999999
  1866. console.log('方案3: 设置scrollTop为999999999')
  1867. }, 100)
  1868. // #endif
  1869. // 非 H5 环境
  1870. // #ifndef H5
  1871. this.scrollTop = 999999999
  1872. console.log('方案1: 设置scrollTop为999999999')
  1873. if (this.talklist.length > 0) {
  1874. const lastMessage = this.talklist[this.talklist.length - 1]
  1875. if (lastMessage && lastMessage.uniqueId) {
  1876. this.scrollIntoView = `msg-${lastMessage.uniqueId}`
  1877. console.log(`方案2: 设置scrollIntoView为msg-${lastMessage.uniqueId}`)
  1878. }
  1879. }
  1880. this.scrollTimer = setTimeout(() => {
  1881. this.scrollTop = 999999999
  1882. console.log('方案3: 延迟设置scrollTop为999999999')
  1883. this.scrollTimer = null
  1884. }, 50)
  1885. // #endif
  1886. },
  1887. forceScrollToBottomOnSend() {
  1888. console.log('执行强制滚动到底部(发送消息专用)')
  1889. if (this.scrollTimer) {
  1890. clearTimeout(this.scrollTimer)
  1891. this.scrollTimer = null
  1892. }
  1893. this.lastScrollTime = Date.now()
  1894. // H5 环境下的特殊处理
  1895. // #ifdef H5
  1896. console.log('H5环境:使用 scroll-top 滚动')
  1897. // 清空 scroll-into-view,避免与 scroll-top 冲突
  1898. this.scrollIntoView = ''
  1899. // 使用时间戳作为 scroll-top 值,确保每次都变化以触发滚动
  1900. const now = Date.now()
  1901. this.scrollTop = now
  1902. // 延迟多次滚动,确保 DOM 完全更新后也能滚动到底部
  1903. setTimeout(() => {
  1904. this.scrollTop = now + 1
  1905. }, 50)
  1906. setTimeout(() => {
  1907. this.scrollTop = 999999999
  1908. }, 100)
  1909. setTimeout(() => {
  1910. this.scrollTop = 9999999999
  1911. }, 150)
  1912. // #endif
  1913. // 非 H5 环境下的处理
  1914. // #ifndef H5
  1915. this.scrollIntoView = ''
  1916. if (this.talklist.length > 0) {
  1917. const lastMessage = this.talklist[this.talklist.length - 1]
  1918. if (lastMessage && lastMessage.uniqueId) {
  1919. this.scrollIntoView = `list_${lastMessage.uniqueId}`
  1920. }
  1921. }
  1922. const targetScrollTop = Date.now()
  1923. this.scrollTop = targetScrollTop
  1924. this.nativeScrollToBottom()
  1925. this.$nextTick(() => {
  1926. this.scrollIntoView = ''
  1927. this.scrollTop = targetScrollTop + 1
  1928. setTimeout(() => {
  1929. this.nativeScrollToBottom()
  1930. }, 50)
  1931. })
  1932. this.scrollTimer = setTimeout(() => {
  1933. this.checkAndForceScroll(targetScrollTop)
  1934. this.scrollTimer = null
  1935. }, 200)
  1936. // #endif
  1937. },
  1938. checkAndForceScroll(targetScrollTop) {
  1939. // H5 环境下的特殊处理
  1940. // #ifdef H5
  1941. console.log('H5环境:checkAndForceScroll 滚动')
  1942. this.scrollIntoView = ''
  1943. const now = Date.now()
  1944. this.scrollTop = now + Math.random() * 1000
  1945. this.nativeScrollToBottom()
  1946. setTimeout(() => {
  1947. this.scrollTop = now + Math.random() * 1000 + 1
  1948. this.nativeScrollToBottom()
  1949. }, 100)
  1950. // #endif
  1951. // 非 H5 环境
  1952. // #ifndef H5
  1953. if (this.talklist.length > 0) {
  1954. const lastMessage = this.talklist[this.talklist.length - 1]
  1955. if (lastMessage && lastMessage.uniqueId) {
  1956. this.scrollIntoView = `list_${lastMessage.uniqueId}`
  1957. }
  1958. }
  1959. this.scrollTop = Date.now() + Math.random() * 1000
  1960. this.nativeScrollToBottom()
  1961. setTimeout(() => {
  1962. this.nativeScrollToBottom()
  1963. }, 100)
  1964. // #endif
  1965. },
  1966. nativeScrollToBottom() {
  1967. try {
  1968. // 检测是否为 H5 环境
  1969. // #ifdef H5
  1970. console.log('H5环境使用 scroll-top 属性滚动')
  1971. // H5 环境下通过设置 scroll-top 来滚动,使用足够大的值确保滚动到底部
  1972. this.scrollTop = 999999999
  1973. // #endif
  1974. // #ifndef H5
  1975. console.log('非H5环境使用节点API滚动')
  1976. const query = uni.createSelectorQuery().in(this)
  1977. query.select('#msgScroll').node((res) => {
  1978. if (res && res.node) {
  1979. console.log('找到msgScroll节点,执行原生滚动')
  1980. const scrollHeight = res.node.scrollHeight
  1981. res.node.scrollTop = scrollHeight
  1982. setTimeout(() => {
  1983. res.node.scrollTop = scrollHeight + 100
  1984. }, 50)
  1985. }
  1986. }).exec()
  1987. // #endif
  1988. } catch (error) {
  1989. console.error('原生滚动失败:', error)
  1990. // 失败时降级方案:使用 scroll-top
  1991. this.scrollTop = 999999999
  1992. }
  1993. },
  1994. onScroll(e) {
  1995. this.currentScrollTop = e.detail.scrollTop
  1996. },
  1997. stopMemoryMonitor() {
  1998. if (this.memoryMonitorTimer) {
  1999. clearInterval(this.memoryMonitorTimer)
  2000. this.memoryMonitorTimer = null
  2001. }
  2002. },
  2003. getActiveTimerCount() {
  2004. const timers = [
  2005. 'trafficTimer', 'pingTimeoutTimer', 'heartBeatTimer', 'liveViewDataTimer',
  2006. 'reconnectTimer', 'scrollTimer', 'searchTimer', 'purchasePromptTimer',
  2007. 'welcomeTimer', 'redTimer', 'liveStartTimer', 'lotteryTimer', 'noticeTimer',
  2008. 'memoryMonitorTimer', 'networkStatusTimer', 'networkRetryTimer'
  2009. ];
  2010. return timers.filter(timer => this[timer] !== null).length
  2011. },
  2012. cleanupUnusedTimers() {
  2013. const timers = [
  2014. 'trafficTimer', 'pingTimeoutTimer', 'heartBeatTimer', 'liveViewDataTimer',
  2015. 'reconnectTimer', 'scrollTimer', 'searchTimer', 'purchasePromptTimer',
  2016. 'welcomeTimer', 'redTimer', 'liveStartTimer', 'lotteryTimer',
  2017. 'networkStatusTimer', 'networkRetryTimer'
  2018. ];
  2019. timers.forEach(timerName => {
  2020. if (this[timerName] && typeof this[timerName] === 'number') {
  2021. if (timerName.includes('Interval')) {
  2022. clearInterval(this[timerName])
  2023. } else {
  2024. clearTimeout(this[timerName])
  2025. }
  2026. this[timerName] = null
  2027. console.log(`清理了可能泄漏的定时器: ${timerName}`)
  2028. }
  2029. })
  2030. },
  2031. clearAllTimersEnhanced() {
  2032. this.clearAllTimers()
  2033. if (this.networkRetryTimer) {
  2034. clearTimeout(this.networkRetryTimer)
  2035. this.networkRetryTimer = null
  2036. }
  2037. this.stopMemoryMonitor()
  2038. this.userRandomColors = Object.create(null)
  2039. this.shownEntryUsers.clear()
  2040. this.messageCount = 0
  2041. this.errorCount = 0
  2042. this.connectionLatency = 0
  2043. this.lastPerformanceCheck = 0
  2044. console.log('增强清理完成:所有定时器和缓存已清理')
  2045. },
  2046. goOrder() {
  2047. // uni.navigateTo({
  2048. // url: '/pages_live/shopping/order'
  2049. // })
  2050. this.postMessage({login: 1,pagesUrl:'/pages_live/shopping/order'})
  2051. },
  2052. preventDoubleClick(e) {
  2053. e.preventDefault();
  2054. e.stopPropagation();
  2055. return false
  2056. },
  2057. clearAllTimers() {
  2058. const timers = [
  2059. 'noticeTimer',
  2060. 'scrollTimer',
  2061. 'liveViewDataTimer',
  2062. 'redTimer',
  2063. 'liveStartTimer',
  2064. 'lotteryTimer',
  2065. 'welcomeTimer',
  2066. 'trafficInterval',
  2067. 'lookTimer',
  2068. 'trafficTimer',
  2069. 'intervalId',
  2070. 'reconnectTimer',
  2071. 'searchTimer',
  2072. 'purchasePromptTimer',
  2073. 'heartBeatTimer',
  2074. 'pingTimeoutTimer'
  2075. ]
  2076. timers.forEach((timer) => {
  2077. if (this[timer]) {
  2078. if (timer.includes('Interval') || timer.includes('Timer')) {
  2079. clearInterval(this[timer])
  2080. } else {
  2081. clearTimeout(this[timer])
  2082. }
  2083. this[timer] = null
  2084. }
  2085. })
  2086. this.stayTime = 0
  2087. this.startTime = 0
  2088. this.totalTraffic = 0
  2089. this.scrollPending = false
  2090. this.reconnectCount = 0
  2091. this.heartBeatRetryCount = 0
  2092. },
  2093. scrollToBottom() {
  2094. const now = Date.now()
  2095. if (now - this.lastScrollTime < this.scrollDebounceDelay) {
  2096. console.log('滚动防抖:忽略频繁调用')
  2097. return
  2098. }
  2099. this.lastScrollTime = now
  2100. if (this.scrollTimer) {
  2101. clearTimeout(this.scrollTimer)
  2102. this.scrollTimer = null
  2103. }
  2104. // H5 环境下的特殊处理:只做一次 DOM 滚动,避免多次滚动
  2105. // #ifdef H5
  2106. this.scrollIntoView = ''
  2107. this.scrollTop = now
  2108. this.scrollTimer = setTimeout(() => {
  2109. this.scrollTop = 999999999
  2110. this.scrollTimer = null
  2111. this.h5DomScrollToBottom()
  2112. }, 300)
  2113. // #endif
  2114. // 非 H5 环境
  2115. // #ifndef H5
  2116. this.scrollTop = 999999999
  2117. // #endif
  2118. // 通用方案:使用 scroll-into-view
  2119. this.$nextTick(() => {
  2120. if (this.talklist && this.talklist.length > 0) {
  2121. const lastMessage = this.talklist[this.talklist.length - 1]
  2122. const targetId = `list_${lastMessage.uniqueId || (this.talklist.length - 1)}`
  2123. console.log(
  2124. `尝试滚动到元素: ${targetId}, 当前消息数量: ${this.talklist.length}, 最后消息ID: ${lastMessage.uniqueId}`
  2125. )
  2126. this.scrollIntoView = targetId
  2127. setTimeout(() => {
  2128. this.scrollIntoView = ''
  2129. }, 200)
  2130. }
  2131. })
  2132. },
  2133. // H5 下通过 DOM 直接设置滚动位置,避免 scroll-view 的 scroll-top 不生效
  2134. h5DomScrollToBottom() {
  2135. // #ifdef H5
  2136. const run = () => {
  2137. const ref = this.$refs.scrollView
  2138. let el = (ref && ref.$el) ? ref.$el : null
  2139. if (!el && typeof document !== 'undefined') {
  2140. el = document.querySelector('#msgScroll') || document.querySelector('[id="msgScroll"]')
  2141. }
  2142. if (!el) return
  2143. const setScroll = (node) => {
  2144. if (node && typeof node.scrollTop !== 'undefined' && node.scrollHeight > node.clientHeight) {
  2145. node.scrollTop = node.scrollHeight
  2146. }
  2147. }
  2148. setScroll(el)
  2149. const inner = el.querySelector && el.querySelector('[class*="scroll-view"]')
  2150. if (inner) setScroll(inner)
  2151. const first = el.firstElementChild
  2152. if (first && first.scrollHeight > first.clientHeight) setScroll(first)
  2153. // 兜底:让最后一条消息元素滚动到可视区域
  2154. if (this.talklist && this.talklist.length > 0) {
  2155. const last = this.talklist[this.talklist.length - 1]
  2156. const id = `list_${last.uniqueId || (this.talklist.length - 1)}`
  2157. const lastEl = el.querySelector(`#${id}`) || (typeof document !== 'undefined' ? document.getElementById(id) : null)
  2158. if (lastEl && lastEl.scrollIntoView) {
  2159. lastEl.scrollIntoView({ block: 'end' })
  2160. }
  2161. }
  2162. }
  2163. run()
  2164. // #endif
  2165. },
  2166. startMemoryMonitor() {
  2167. if (this.memoryMonitorTimer) {
  2168. clearInterval(this.memoryMonitorTimer)
  2169. }
  2170. this.memoryMonitorTimer = setInterval(() => {
  2171. this.checkAndCleanMemory()
  2172. }, 5 * 60 * 1000);
  2173. },
  2174. checkAndCleanMemory() {
  2175. try {
  2176. if (this.talklist && this.talklist.length > 25) {
  2177. const keepCount = 20
  2178. this.talklist.splice(0, this.talklist.length - keepCount)
  2179. }
  2180. if (this.userRandomColors && Object.keys(this.userRandomColors).length > 50) {
  2181. const entries = Object.entries(this.userRandomColors)
  2182. const keepEntries = entries.slice(-50)
  2183. this.userRandomColors = Object.fromEntries(keepEntries)
  2184. }
  2185. if (this.shownEntryUsers && this.shownEntryUsers.size > 100) {
  2186. const array = Array.from(this.shownEntryUsers)
  2187. this.shownEntryUsers.clear()
  2188. array.slice(-100).forEach((id) => this.shownEntryUsers.add(id))
  2189. }
  2190. console.log('内存清理完成')
  2191. } catch (error) {
  2192. console.error('内存清理失败:', error)
  2193. }
  2194. },
  2195. async resumePageActivity() {
  2196. if (this.liveItem) {
  2197. const userInfo = uni.getStorageSync('userInfo');
  2198. if (userInfo) {
  2199. await this.getliving(this.liveId)
  2200. }
  2201. this.startTimeTimer(this.liveItem)
  2202. }
  2203. if (!this.isSocketAvailable()) {
  2204. const userInfo = uni.getStorageSync('userInfo');
  2205. if (userInfo) {
  2206. this.initSocket()
  2207. }
  2208. }
  2209. },
  2210. getUserRandomColor(userId) {
  2211. if (!userId) {
  2212. return '#8978e2'
  2213. }
  2214. if (this.userRandomColors[userId]) {
  2215. return this.userRandomColors[userId]
  2216. }
  2217. const color = this.generateStableColor(userId)
  2218. this.userRandomColors[userId] = color
  2219. this.saveUserColorsToStorage()
  2220. return color
  2221. },
  2222. generateStableColor(userId) {
  2223. let seed = 0
  2224. for (let i = 0; i < userId.length; i++) {
  2225. seed = (seed * 31 + userId.charCodeAt(i)) % 1000000
  2226. }
  2227. const colorPool = [
  2228. '#FF6B6B',
  2229. '#4ECDC4',
  2230. '#45B7D1',
  2231. '#96CEB4',
  2232. '#FFEAA7',
  2233. '#DDA0DD',
  2234. '#98D8C8',
  2235. '#F7DC6F',
  2236. '#BB8FCE',
  2237. '#85C1E9',
  2238. '#F8C471',
  2239. '#82E0AA',
  2240. '#F1948A',
  2241. '#85C1E9',
  2242. '#D7BDE2'
  2243. ]
  2244. return colorPool[seed % colorPool.length]
  2245. },
  2246. saveUserColorsToStorage() {
  2247. try {
  2248. uni.setStorageSync('userRandomColors', this.userRandomColors)
  2249. } catch (e) {
  2250. console.warn('保存用户颜色缓存失败:', e)
  2251. }
  2252. },
  2253. loadUserColorsFromStorage() {
  2254. try {
  2255. const cached = uni.getStorageSync('userRandomColors')
  2256. if (cached) {
  2257. this.userRandomColors = cached
  2258. }
  2259. } catch (e) {
  2260. console.warn('加载用户颜色缓存失败:', e)
  2261. }
  2262. },
  2263. getNicknameInitial(nickName) {
  2264. if (!nickName || typeof nickName !== 'string') return '未'
  2265. if (/^[\u4e00-\u9fa5]/.test(nickName[0])) {
  2266. return nickName[0]
  2267. }
  2268. return nickName[0].toUpperCase()
  2269. },
  2270. async getUserInfo() {
  2271. await getUserInfo().then((res) => {
  2272. if (res.code == 200) {
  2273. this.userData = res.user
  2274. uni.setStorageSync('userData', JSON.stringify(res.user))
  2275. } else {
  2276. uni.showToast({
  2277. icon: 'none',
  2278. title: '请求失败'
  2279. })
  2280. }
  2281. },
  2282. (rej) => {}
  2283. )
  2284. },
  2285. /**
  2286. * 获取当前服务器时间戳(毫秒)
  2287. * 如果存在 serviceTime,则基于服务器时间计算;否则使用本地时间
  2288. */
  2289. getServerTimeNow() {
  2290. if (!this.liveItem || !this.liveItem.serviceTime) {
  2291. return Date.now();
  2292. }
  2293. const localTimeWhenReceived = this.liveItem._localTimeWhenReceived;
  2294. if (!localTimeWhenReceived) {
  2295. // 如果没有记录接收时间,使用本地时间
  2296. return Date.now();
  2297. }
  2298. let serverTimeWhenReceived;
  2299. const serviceTime = this.liveItem.serviceTime;
  2300. //如果 serviceTime 是数字类型(毫秒级时间戳)
  2301. if (typeof serviceTime === 'number') {
  2302. serverTimeWhenReceived = serviceTime;
  2303. }
  2304. //如果 serviceTime 是字符串格式的数字时间戳
  2305. else if (typeof serviceTime === 'string' && /^\d+$/.test(serviceTime)) {
  2306. // 纯数字字符串,转换为数字
  2307. const numTime = Number(serviceTime);
  2308. // 判断是秒级还是毫秒级时间戳(13位及以上为毫秒级)
  2309. serverTimeWhenReceived = numTime < 1000000000000 ? numTime * 1000 : numTime;
  2310. }
  2311. //如果 serviceTime 是时间字符串
  2312. else {
  2313. const safeTimeStr = String(serviceTime).replace(/-/g, '/');
  2314. serverTimeWhenReceived = Date.parse(safeTimeStr);
  2315. if (isNaN(serverTimeWhenReceived)) {
  2316. return Date.now();
  2317. }
  2318. }
  2319. //计算服务器时间与本地时间的差值(毫秒)
  2320. const timeDiff = serverTimeWhenReceived - localTimeWhenReceived;
  2321. // 当前服务器时间 = 当前本地时间 + 时间差
  2322. return Date.now() + timeDiff;
  2323. },
  2324. /**
  2325. * 获取直播开始时间,优先使用 serviceStartTime,如果不存在则使用 startTime
  2326. */
  2327. getLiveStartTime() {
  2328. if (!this.liveItem) {
  2329. return null;
  2330. }
  2331. // 优先使用 serviceStartTime(服务器返回的直播开始时间)
  2332. if (this.liveItem.serviceStartTime) {
  2333. return this.liveItem.serviceStartTime;
  2334. }
  2335. // 如果不存在,回退到 startTime
  2336. return this.liveItem.startTime || null;
  2337. },
  2338. handleAgreement() {
  2339. const templateId = this.templateId
  2340. if (this.hasSubscribed) return;
  2341. uni.requestSubscribeMessage({
  2342. tmplIds: [templateId],
  2343. success: (res) => {
  2344. if (res[templateId] === 'accept') {
  2345. uni.showToast({
  2346. title: '订阅成功,开播将提醒您',
  2347. icon: 'success',
  2348. duration: 2500 // 成功提示延长到2.5秒
  2349. })
  2350. this.hasSubscribed = true;
  2351. uni.setStorageSync('subscribe_status_' + this.liveId, true);
  2352. this.callSendMessageApi()
  2353. } else if (res[templateId] === 'reject') {
  2354. uni.showToast({
  2355. title: '您已拒绝订阅,将无法收到提醒',
  2356. icon: 'none',
  2357. duration: 2500 // 成功提示延长到2.5秒
  2358. })
  2359. } else if (res[templateId] === 'ban') {
  2360. uni.showToast({
  2361. title: '您已关闭所有订阅权限,请在设置中开启',
  2362. icon: 'none',
  2363. duration: 2500 // 成功提示延长到2.5秒
  2364. })
  2365. }
  2366. },
  2367. fail: (err) => {
  2368. console.error('订阅消息失败', err)
  2369. uni.showToast({
  2370. title: '订阅失败,请重试',
  2371. icon: 'none',
  2372. duration: 2500 // 成功提示延长到2.5秒
  2373. })
  2374. }
  2375. })
  2376. },
  2377. async callSendMessageApi() {
  2378. if (!this.userData.userId) return
  2379. const templateData = {
  2380. liveId: this.liveId,
  2381. userId: this.userData.userId,
  2382. templateId: this.templateId || '',
  2383. maOpenId: this.userData.maOpenId || '',
  2384. appid: this.appid || '',
  2385. data: {
  2386. thing6: this.liveItem.liveName,
  2387. date7: this.liveItem.startTime
  2388. }
  2389. }
  2390. subNotifyLive(templateData).then(
  2391. (res) => {
  2392. if (res.code == 200) {
  2393. this.isAgreement = true
  2394. uni.setStorageSync('isAgreement', true)
  2395. } else {
  2396. uni.showToast({
  2397. title: res.msg,
  2398. icon: 'none'
  2399. })
  2400. }
  2401. },
  2402. (rej) => {}
  2403. )
  2404. },
  2405. sendHeartBeat() {
  2406. if (!this.isSocketAvailable() || !this.isNetworkAvailable) {
  2407. console.warn('网络不可用或Socket连接异常,跳过心跳发送')
  2408. return
  2409. }
  2410. this.lastHeartBeatTime = Date.now()
  2411. try {
  2412. const heartBeatMsg = JSON.stringify({
  2413. cmd: 'heartbeat',
  2414. msg: 'ping',
  2415. userId: this.userData.userId || '',
  2416. liveId: this.liveId,
  2417. timestamp: this.lastHeartBeatTime,
  2418. networkType: this.networkType
  2419. })
  2420. this.socket.send({
  2421. data: heartBeatMsg,
  2422. success: () => {
  2423. this.heartBeatRetryCount = 0
  2424. this.adjustHeartBeatInterval(true)
  2425. this.startPingTimeout()
  2426. },
  2427. fail: (err) => {
  2428. console.error('心跳包发送失败:', err)
  2429. this.heartBeatRetryCount++
  2430. this.adjustHeartBeatInterval(false)
  2431. const retryDelay = this.getRetryDelay()
  2432. if (this.heartBeatRetryCount < this.maxHeartBeatRetries) {
  2433. setTimeout(() => this.sendHeartBeat(), retryDelay)
  2434. } else {
  2435. this.heartBeatRetryCount = 0
  2436. this.handleReconnect()
  2437. }
  2438. }
  2439. })
  2440. } catch (err) {
  2441. console.error('心跳发送异常:', err)
  2442. this.heartBeatRetryCount++
  2443. const retryDelay = this.getRetryDelay()
  2444. if (this.heartBeatRetryCount < this.maxHeartBeatRetries) {
  2445. setTimeout(() => this.sendHeartBeat(), retryDelay)
  2446. } else {
  2447. this.heartBeatRetryCount = 0
  2448. this.handleReconnect()
  2449. }
  2450. }
  2451. },
  2452. getRetryDelay() {
  2453. const baseDelay = 2000
  2454. const retryMultiplier = Math.pow(1.5, this.heartBeatRetryCount)
  2455. let networkMultiplier = 1
  2456. switch (this.networkType) {
  2457. case '2g':
  2458. networkMultiplier = 3
  2459. break
  2460. case '3g':
  2461. networkMultiplier = 2
  2462. break
  2463. case '4g':
  2464. case '5g':
  2465. networkMultiplier = 1
  2466. break
  2467. case 'wifi':
  2468. networkMultiplier = 0.8
  2469. break
  2470. default:
  2471. networkMultiplier = 1.5
  2472. }
  2473. return Math.min(baseDelay * retryMultiplier * networkMultiplier, 10000)
  2474. },
  2475. adjustHeartBeatInterval(isSuccess) {
  2476. if (isSuccess) {
  2477. this.adaptiveHeartBeatInterval = Math.min(this.adaptiveHeartBeatInterval * 1.1, 30000)
  2478. } else {
  2479. this.adaptiveHeartBeatInterval = Math.max(this.adaptiveHeartBeatInterval * 0.9, 10000)
  2480. }
  2481. },
  2482. startPingTimeout() {
  2483. if (this.pingTimeoutTimer) {
  2484. clearTimeout(this.pingTimeoutTimer)
  2485. this.pingTimeoutTimer = null
  2486. }
  2487. this.pingTimeoutTimer = setTimeout(() => {
  2488. console.warn('心跳超时,触发重连')
  2489. this.pingTimeoutTimer = null
  2490. this.heartBeatRetryCount++
  2491. if (this.heartBeatRetryCount < this.maxHeartBeatRetries) {
  2492. console.log(`心跳超时,尝试重发 (${this.heartBeatRetryCount}/${this.maxHeartBeatRetries})`)
  2493. setTimeout(() => this.sendHeartBeat(), 1000)
  2494. } else {
  2495. console.log('心跳重试次数用尽,触发重连')
  2496. this.heartBeatRetryCount = 0
  2497. this.handleReconnect()
  2498. }
  2499. }, this.pingTimeout)
  2500. },
  2501. stopHeartBeat() {
  2502. if (this.heartBeatTimer) {
  2503. clearInterval(this.heartBeatTimer)
  2504. this.heartBeatTimer = null
  2505. }
  2506. if (this.pingTimeoutTimer) {
  2507. clearTimeout(this.pingTimeoutTimer)
  2508. this.pingTimeoutTimer = null
  2509. }
  2510. },
  2511. isSocketAvailable() {
  2512. // APP-PLUS 下 SocketTask.readyState 可能不可用,以 isSocketOpen 为准
  2513. if (!this.socket || !this.isSocketOpen) return false
  2514. const state = this.socket.readyState
  2515. return state === undefined || state === 1
  2516. },
  2517. handleReconnect() {
  2518. if (this.isManualClose) {
  2519. console.log('手动关闭连接,不进行重连')
  2520. return
  2521. }
  2522. if (this.reconnectTimer) {
  2523. console.log('重连已在进行中,跳过重复重连')
  2524. return
  2525. }
  2526. this.stopHeartBeat()
  2527. if (!this.isNetworkAvailable) {
  2528. console.warn('网络不可用,延迟重连')
  2529. this.reconnectTimer = setTimeout(() => {
  2530. this.reconnectTimer = null
  2531. this.handleReconnect()
  2532. }, 5000)
  2533. return
  2534. }
  2535. if (this.reconnectCount < this.maxReconnectAttempts) {
  2536. this.reconnectCount++
  2537. const baseDelay = 1000
  2538. const exponentialDelay = baseDelay * Math.pow(2, this.reconnectCount - 1)
  2539. const jitter = Math.random() * 1000
  2540. const totalDelay = Math.min(exponentialDelay + jitter, 30000)
  2541. console.log(`第${this.reconnectCount}次重连,延迟${Math.round(totalDelay)}ms,网络类型:${this.networkType}`)
  2542. this.reconnectTimer = setTimeout(() => {
  2543. this.reconnectTimer = null
  2544. if (this.isNetworkAvailable && !this.isManualClose && !this.isSocketAvailable()) {
  2545. console.log('开始执行重连...')
  2546. this.initSocket()
  2547. } else if (this.isSocketAvailable()) {
  2548. console.log('连接已恢复,取消重连')
  2549. this.reconnectCount = 0
  2550. } else {
  2551. console.warn('重连时网络不可用或已手动关闭')
  2552. this.handleReconnect()
  2553. }
  2554. }, totalDelay)
  2555. } else {
  2556. console.log(`已达最大重连次数(${this.maxReconnectAttempts}),停止重连`)
  2557. this.showReconnectFailedMessage()
  2558. }
  2559. },
  2560. showReconnectFailedMessage() {
  2561. uni.showToast({
  2562. title: '网络连接异常,请检查网络后重新进入',
  2563. icon: 'none',
  2564. duration: 3000
  2565. })
  2566. },
  2567. handleConnectionError(errorType, error) {
  2568. console.error(`WebSocket ${errorType}:`, error)
  2569. if (errorType === '连接请求失败') {
  2570. if (!this.isNetworkAvailable) {
  2571. console.warn('网络不可用,等待网络恢复后重连')
  2572. return
  2573. }
  2574. }
  2575. this.handleReconnect()
  2576. },
  2577. resetReconnectState() {
  2578. this.reconnectCount = 0
  2579. this.heartBeatRetryCount = 0
  2580. if (this.reconnectTimer) {
  2581. clearTimeout(this.reconnectTimer)
  2582. this.reconnectTimer = null
  2583. }
  2584. this.adaptiveHeartBeatInterval = this.heartBeatInterval
  2585. },
  2586. initNetworkStatusListener() {
  2587. uni.getNetworkType({
  2588. success: (res) => {
  2589. this.networkType = res.networkType
  2590. this.isNetworkAvailable = res.networkType !== 'none'
  2591. console.log('当前网络类型:', res.networkType)
  2592. }
  2593. })
  2594. uni.onNetworkStatusChange((res) => {
  2595. const oldNetworkType = this.networkType
  2596. const oldNetworkAvailable = this.isNetworkAvailable
  2597. this.networkType = res.networkType
  2598. this.isNetworkAvailable = res.isConnected
  2599. console.log(`网络状态变化: ${oldNetworkType} -> ${res.networkType}, 连接状态: ${res.isConnected}`)
  2600. if (!oldNetworkAvailable && res.isConnected) {
  2601. console.log('网络恢复,尝试重连WebSocket')
  2602. this.resetReconnectState()
  2603. setTimeout(() => {
  2604. if (!this.isSocketAvailable()) {
  2605. this.initSocket()
  2606. }
  2607. }, 1000)
  2608. }
  2609. if (oldNetworkType !== res.networkType && res.isConnected) {
  2610. this.adjustHeartBeatForNetworkType(res.networkType)
  2611. }
  2612. if (!res.isConnected) {
  2613. console.warn('网络断开,停止心跳')
  2614. this.stopHeartBeat()
  2615. }
  2616. })
  2617. },
  2618. adjustHeartBeatForNetworkType(networkType) {
  2619. let newInterval = this.heartBeatInterval
  2620. switch (networkType) {
  2621. case '2g':
  2622. newInterval = 30000
  2623. break
  2624. case '3g':
  2625. newInterval = 20000
  2626. break
  2627. case '4g':
  2628. case '5g':
  2629. newInterval = 15000
  2630. break
  2631. case 'wifi':
  2632. newInterval = 10000
  2633. break
  2634. default:
  2635. newInterval = 15000
  2636. }
  2637. if (newInterval !== this.adaptiveHeartBeatInterval) {
  2638. this.adaptiveHeartBeatInterval = newInterval
  2639. console.log(`根据网络类型(${networkType})调整心跳间隔为${newInterval}ms`)
  2640. if (this.heartBeatTimer && this.isSocketAvailable()) {
  2641. this.startHeartBeat()
  2642. }
  2643. }
  2644. },
  2645. inputFocus() {
  2646. this.isFocus = true
  2647. this.isKeyboardShow = true
  2648. },
  2649. inputBlur() {
  2650. this.isFocus = false
  2651. },
  2652. onRed() {
  2653. if (!this.liveId) return
  2654. if (!this.redInfo?.redId) return
  2655. if (this.redTimer) {
  2656. clearInterval(this.redTimer)
  2657. this.redTimer = null
  2658. }
  2659. let data = {
  2660. liveId: this.liveId,
  2661. userId: this.userData.userId,
  2662. redId: this.redInfo.redId
  2663. }
  2664. liveRed(data).then(
  2665. (res) => {
  2666. this.isShowRed = false
  2667. this.redCard = res
  2668. this.isShowRedCard = true
  2669. },
  2670. (rej) => {}
  2671. )
  2672. },
  2673. onCoupon() {
  2674. if (!this.couponInfo.couponIssueId) return
  2675. let data = {
  2676. goodsId: this.couponInfo.goodsId,
  2677. couponIssueId: this.couponInfo.couponIssueId,
  2678. liveId: this.liveId
  2679. }
  2680. coupon(data).then((res) => {
  2681. this.isShowCoupon = false
  2682. if (res.code == 200) {
  2683. uni.showToast({
  2684. title: res.msg,
  2685. icon: 'none'
  2686. })
  2687. } else {
  2688. uni.showToast({
  2689. title: res.msg,
  2690. icon: 'none'
  2691. })
  2692. }
  2693. }).catch((rej) => {})
  2694. },
  2695. getMyLottery() {
  2696. this.winning = true
  2697. myLottery()
  2698. .then((res) => {
  2699. if (res.code == 200) {
  2700. this.prizeAll = res.data.list || {}
  2701. } else {}
  2702. })
  2703. .catch((rej) => {})
  2704. },
  2705. onLottery() {
  2706. if (!this.lotteryInfo) return
  2707. let data = {
  2708. lotteryId: this.lotteryInfo.lotteryId
  2709. }
  2710. liveLottery(data)
  2711. .then((res) => {
  2712. if (res.code == 200) {
  2713. const resData = res.data || {}
  2714. this.lotteryList = Array.isArray(resData) ? resData : []
  2715. this.lotteryProducts = Array.isArray(resData.products) ? resData.products : []
  2716. if (resData.duration) {
  2717. this.isShowLotteryPop = true
  2718. }
  2719. } else {
  2720. uni.showToast({
  2721. title: res.msg,
  2722. icon: 'none'
  2723. })
  2724. this.lotteryList = []
  2725. this.lotteryProducts = []
  2726. }
  2727. })
  2728. .catch((rej) => {
  2729. uni.showToast({
  2730. title: '获取抽奖信息失败',
  2731. icon: 'none'
  2732. })
  2733. this.lotteryList = []
  2734. this.lotteryProducts = []
  2735. })
  2736. },
  2737. onClaim() {
  2738. let data = {
  2739. liveId: this.liveId,
  2740. lotteryId: this.lotteryInfo.lotteryId
  2741. }
  2742. claim(data).then(
  2743. (res) => {
  2744. if (res.code == 200) {
  2745. uni.showToast({
  2746. title: res.msg,
  2747. icon: 'none'
  2748. })
  2749. this.isShowLotteryPop = false
  2750. this.havePrize = true
  2751. uni.setStorageSync('havePrize', this.havePrize)
  2752. } else {
  2753. uni.showToast({
  2754. title: res.msg,
  2755. icon: 'none'
  2756. })
  2757. }
  2758. },
  2759. (rej) => {}
  2760. )
  2761. },
  2762. confirm() {
  2763. this.isShowPrize = false
  2764. this.havePrize = false
  2765. uni.setStorageSync('havePrize', this.havePrize)
  2766. },
  2767. onGoodsCollect(item) {
  2768. if (!item || item.length === 0 || !item.goodsId) {
  2769. return
  2770. }
  2771. collectGoods(item.goodsId).then(
  2772. (res) => {
  2773. if (res.code == 200) {
  2774. uni.showToast({
  2775. title: res.msg,
  2776. icon: 'none'
  2777. })
  2778. item.isFavorite = !item.isFavorite
  2779. } else {
  2780. uni.showToast({
  2781. title: res.msg,
  2782. icon: 'none'
  2783. })
  2784. }
  2785. },
  2786. (rej) => {}
  2787. )
  2788. },
  2789. handleGoShopFromCart(item) {
  2790. this.goShop(item.productId, item.goodsId)
  2791. },
  2792. getliveOrder(item) {
  2793. if (!this.liveId) {
  2794. return
  2795. }
  2796. liveOrderUser(this.liveId).then(
  2797. (res) => {
  2798. if (res.code == 200) {
  2799. this.orderUser = res
  2800. } else {
  2801. console.log('获取正在购买用户失败')
  2802. }
  2803. },
  2804. (rej) => {}
  2805. )
  2806. },
  2807. onLiveStateChange(e, liveItem) {
  2808. const stateCode = e.detail.code
  2809. if (e.detail.code == -2301 || e.detail.code == -2302) {
  2810. if (this.$refs && this.$refs.liveVideo && typeof this.$refs.liveVideo.playVideo === 'function') {
  2811. this.$refs.liveVideo.playVideo()
  2812. }
  2813. } else if (e.detail.code == 2004) {
  2814. this.calculateTimeDiff(this.liveItem)
  2815. if (this.$refs && this.$refs.liveVideo && typeof this.$refs.liveVideo.startTrafficCalculation === 'function') {
  2816. this.$refs.liveVideo.startTrafficCalculation()
  2817. }
  2818. }
  2819. },
  2820. onLiveError(e, liveItem) {
  2821. this.videoError(e, liveItem)
  2822. console.log('错误')
  2823. },
  2824. getCurrentActivities() {
  2825. if (!this.liveId) return
  2826. currentActivities(this.liveId).then(
  2827. (res) => {
  2828. if (res.code === 200) {
  2829. this.redInfo = (Array.isArray(res.red) ? res.red : [])[0] || {}
  2830. this.lotteryInfo = (Array.isArray(res.lottery) ? res.lottery : [])[0] || {}
  2831. this.goodsCard = res.goods || {}
  2832. this.notice = res.topMsg || {}
  2833. this.isShowGoods = this.goodsCard && this.goodsCard.status == 1
  2834. this.isShowRed = this.redInfo && this.redInfo.redStatus == 1
  2835. this.isShowLottery = this.lotteryInfo && this.lotteryInfo.lotteryStatus == 1
  2836. if (this.isShowRed) {
  2837. this.redTimer = setInterval(() => {
  2838. const redCountdown = this.handleTime(this.redInfo.updateTime, this.redInfo
  2839. .duration)
  2840. if (!redCountdown) {
  2841. this.isShowRed = false
  2842. clearInterval(this.redTimer)
  2843. }
  2844. }, 1000)
  2845. }
  2846. if (this.notice.msg) {
  2847. this.noticeTimer = setInterval(() => {
  2848. const noticeCountdown = this.handleTime(this.notice.endTime, 0)
  2849. console.log('出现1>>>', noticeCountdown)
  2850. if (!noticeCountdown) {
  2851. this.isShowNotice = false
  2852. clearInterval(this.noticeTimer)
  2853. } else {
  2854. this.isShowNotice = true
  2855. }
  2856. }, 1000)
  2857. }
  2858. if (this.isShowLottery) {
  2859. this.lotteryTimer = setInterval(() => {
  2860. this.countdown = this.handleTime(this.lotteryInfo.updateTime, this
  2861. .lotteryInfo.duration)
  2862. }, 1000)
  2863. }
  2864. } else {
  2865. uni.showToast({
  2866. title: res.msg,
  2867. icon: 'none'
  2868. })
  2869. }
  2870. },
  2871. (rej) => {}
  2872. )
  2873. },
  2874. calculateTimeDiff(item) {
  2875. if (!item.startTime) return
  2876. let timeStr = item.startTime
  2877. const time = new Date(timeStr.replace(/-/g, '/'))
  2878. if (isNaN(time.getTime())) {
  2879. return
  2880. }
  2881. const now = new Date()
  2882. let diffMs = Math.max(0, now.getTime() - time.getTime())
  2883. const totalSeconds = Math.floor(diffMs / 1000)
  2884. const hours = this.padZero(Math.floor(totalSeconds / 3600))
  2885. const minutes = this.padZero(Math.floor((totalSeconds % 3600) / 60))
  2886. const seconds = this.padZero(totalSeconds % 60)
  2887. this.diffTotalTime = `${hours}:${minutes}:${seconds}`
  2888. },
  2889. padZero(num) {
  2890. return num < 10 ? `0${num}` : num
  2891. },
  2892. startTimeTimer(item) {
  2893. if (!item) return
  2894. const totalTime = this.calculateTimeDiff(item)
  2895. item.timeTimer = setInterval(() => {
  2896. const totalTime = this.calculateTimeDiff(item)
  2897. }, 1000);
  2898. },
  2899. async getLiveMsg(liveItem) {
  2900. if (!liveItem || !this.liveId) {
  2901. console.error('getLiveMsg 错误:无效的 liveItem')
  2902. return
  2903. }
  2904. try {
  2905. const res = await liveMsg(this.liveId, 30, 1)
  2906. if (res.code == 200) {
  2907. const rows = Array.isArray(res.rows) ? res.rows : []
  2908. const reversedTalkList = [...rows].reverse()
  2909. this.talklist = Array.isArray(reversedTalkList) ? reversedTalkList : []
  2910. this.$nextTick(() => {
  2911. // #ifdef H5
  2912. setTimeout(() => this.h5DomScrollToBottom(), 400)
  2913. // #endif
  2914. // #ifndef H5
  2915. setTimeout(() => this.scrollToBottom(), 300)
  2916. // #endif
  2917. })
  2918. } else {
  2919. this.talklist = []
  2920. }
  2921. } catch (error) {
  2922. this.talklist = []
  2923. console.error('获取聊天记录失败:', error)
  2924. }
  2925. },
  2926. generateRandomChineseName() {
  2927. const prefixes = [
  2928. '幸福', '快乐', '安康', '吉祥', '如意', '平安', '健康', '长寿', '福气', '美满',
  2929. '和谐', '团圆', '富贵', '荣华', '宁静', '淡泊', '知足', '常乐', '悠然', '自在',
  2930. '夕阳', '晚霞', '金秋', '银发', '老顽童', '开心', '笑口常开', '岁月静好', '云淡风轻', '海阔天空'
  2931. ]
  2932. const suffixes = [
  2933. '老人', '大爷', '大妈', '爷爷', '奶奶', '伯伯', '阿姨', '前辈', '先生', '女士',
  2934. '长者', '翁', '婆', '哥', '姐', '友', '客', '迷', '粉丝', '达人',
  2935. '爱好者', '之家', '之人', '之心', '之情', '之乐', '之旅', '之梦', '之歌', '之韵'
  2936. ]
  2937. const specialNames = [
  2938. '老有所乐', '知足常乐', '笑看人生', '云淡风轻', '岁月如歌', '夕阳红', '金色年华', '老当益壮',
  2939. '鹤发童颜', '老骥伏枥', '闲云野鹤', '淡泊明志', '宁静致远', '随遇而安', '顺其自然',
  2940. '老来俏', '开心果', '老宝贝', '不老松', '常青树'
  2941. ]
  2942. if (Math.random() < 0.3) {
  2943. return specialNames[Math.floor(Math.random() * specialNames.length)]
  2944. } else {
  2945. const prefix = prefixes[Math.floor(Math.random() * prefixes.length)]
  2946. const suffix = suffixes[Math.floor(Math.random() * suffixes.length)]
  2947. return prefix + suffix
  2948. }
  2949. },
  2950. // async getliveUser(isLoadMore = false) {
  2951. // if (!isLoadMore) {
  2952. // this.liveViewers = []
  2953. // this.viewPageNum = 1
  2954. // }
  2955. // this.viewLoading = true;
  2956. // try {
  2957. // const res = await watchUserList(this.liveId, this.viewPageSize, this.viewPageNum, false);
  2958. // console.log("qxj watchUserList res", res);
  2959. // if (res.code === 200) {
  2960. // const newRows = Array.isArray(res.rows) ? res.rows : []
  2961. // const newViewers = newRows.map((item) => ({
  2962. // avatar: item.avatar || '',
  2963. // userId: item.userId || '',
  2964. // nickName: item.nickName || '未命名'
  2965. // }));
  2966. // let virtualData = [];
  2967. // // 计算虚拟总数
  2968. // let virtualTotal = res.total * 2 + 50;
  2969. // // 计算需要生成的虚拟数据数量
  2970. // let totalVirtualNeeded = virtualTotal - res.total;
  2971. // totalVirtualNeeded = Math.max(0, Math.min(totalVirtualNeeded, 90)); // 最多生成90条虚拟数据
  2972. // if (!isLoadMore) {
  2973. // // 首次加载时,生成第一批虚拟数据(与真实数据合计不超过10个)
  2974. // let firstBatchVirtualCount = Math.max(0, 10 - newRows.length);
  2975. // for (let i = 0; i < firstBatchVirtualCount; i++) {
  2976. // let data = {
  2977. // avatar: '',
  2978. // userId: '8565' + i,
  2979. // nickName: this.generateRandomChineseName()
  2980. // }
  2981. // virtualData.push(data);
  2982. // }
  2983. // this.liveViewersData = [...newViewers, ...virtualData];
  2984. // } else {
  2985. // // 后续加载时,计算已加载的虚拟数据数量
  2986. // let loadedVirtualCount = this.liveViewers.filter(v => String(v.userId).startsWith('8565'))
  2987. // .length;
  2988. // // 计算还需要加载的虚拟数据数量
  2989. // let remainingVirtualCount = totalVirtualNeeded - loadedVirtualCount;
  2990. // // 本次加载的虚拟数据数量(最多10个)
  2991. // let currentBatchVirtualCount = Math.min(remainingVirtualCount, this.viewPageSize);
  2992. // // 生成本次加载的虚拟数据
  2993. // for (let i = 0; i < currentBatchVirtualCount; i++) {
  2994. // let data = {
  2995. // avatar: '',
  2996. // userId: '8565' + (loadedVirtualCount + i),
  2997. // nickName: this.generateRandomChineseName()
  2998. // }
  2999. // virtualData.push(data);
  3000. // }
  3001. // }
  3002. // // 过滤掉旧列表中的虚拟数据,保留真实用户
  3003. // let currentRealViewers = isLoadMore ? this.liveViewers.filter(v => !String(v.userId)
  3004. // .startsWith('8565')) : [];
  3005. // // 合并真实用户
  3006. // let allRealViewers = [...currentRealViewers, ...newViewers];
  3007. // // 合并虚拟用户
  3008. // let currentVirtualViewers = isLoadMore ? this.liveViewers.filter(v => String(v.userId)
  3009. // .startsWith('8565')) : [];
  3010. // let allVirtualViewers = [...currentVirtualViewers, ...virtualData];
  3011. // // 合并所有用户
  3012. // let mergedViewers = [...allRealViewers, ...allVirtualViewers];
  3013. // // 确保观众列表不超过100个
  3014. // if (mergedViewers.length > 100) {
  3015. // mergedViewers = mergedViewers.slice(0, 100);
  3016. // }
  3017. // this.liveViewers = mergedViewers;
  3018. // // 检查虚拟数据是否还有剩余
  3019. // let totalLoaded = allRealViewers.length + allVirtualViewers.length;
  3020. // let remainingVirtualCount = totalVirtualNeeded - allVirtualViewers.length;
  3021. // // 计算是否还有更多数据
  3022. // // 1. 如果真实用户还有更多
  3023. // // 2. 或者虚拟数据还有剩余
  3024. // // 3. 但不超过100个
  3025. // let hasMore = ((newRows.length >= this.viewPageSize) || (remainingVirtualCount > 0)) && (
  3026. // totalLoaded < 100);
  3027. // // 确保即使真实用户数据加载完毕,只要虚拟数据还有剩余且未超过100个,就继续加载
  3028. // if (remainingVirtualCount > 0 && totalLoaded < 100) {
  3029. // hasMore = true;
  3030. // }
  3031. // if (totalLoaded >= virtualTotal || totalLoaded >= 100) {
  3032. // hasMore = false;
  3033. // this.lookAudsCount = Math.max(0, virtualTotal - 100);
  3034. // } else {
  3035. // this.lookAudsCount = 0;
  3036. // }
  3037. // if (this.$refs.viewer) {
  3038. // this.$refs.viewer.endSuccess(newRows.length + virtualData.length, hasMore);
  3039. // }
  3040. // } else {
  3041. // if (this.$refs.viewer) this.$refs.viewer.endErr();
  3042. // }
  3043. // } catch (error) {
  3044. // console.error('获取观众列表失败:', error)
  3045. // if (this.$refs.viewer) this.$refs.viewer.endErr();
  3046. // } finally {
  3047. // this.viewLoading = false
  3048. // }
  3049. // },
  3050. // async getliveUserInit(isLoadMore = false) {
  3051. // try {
  3052. // const res = await watchUserList(this.liveId, this.viewPageSize, 1, false);
  3053. // console.log("qxj watchUserList res", res);
  3054. // if (res.code === 200) {
  3055. // const userRows = Array.isArray(res.rows) ? res.rows : []
  3056. // let array = userRows.map((item) => ({
  3057. // avatar: item.avatar || '',
  3058. // userId: item.userId || '',
  3059. // nickName: item.nickName || '未命名'
  3060. // }));
  3061. // // let virtualTotal = res.total * 2 + 50;
  3062. // // // 使用虚拟的观众总数
  3063. // // this.liveUserTotal = virtualTotal || 0;
  3064. // this.liveUserTotal = res.total;
  3065. // this.liveTopViewersData = [...array];
  3066. // }
  3067. // } catch (error) {
  3068. // console.error('获取观众列表失败:', error)
  3069. // } finally {}
  3070. // },
  3071. showPurchaseMessage() {
  3072. if (this.purchasePromptTimer) {
  3073. clearTimeout(this.purchasePromptTimer)
  3074. }
  3075. this.showPurchasePrompt = true
  3076. this.purchasePromptTimer = setTimeout(() => {
  3077. this.showPurchasePrompt = false
  3078. }, 2000)
  3079. },
  3080. truncateString(str, maxLength) {
  3081. if (typeof str !== 'string' || str.length <= maxLength) {
  3082. return str
  3083. }
  3084. return str.slice(0, maxLength) + '...'
  3085. },
  3086. navgetTo(url) {
  3087. // uni.navigateTo({
  3088. // url: url
  3089. // })
  3090. this.postMessage({login: 1,pagesUrl:url})
  3091. },
  3092. async getliving(liveId) {
  3093. if (!liveId) return
  3094. const param = {
  3095. id: liveId
  3096. }
  3097. try {
  3098. const res = await getlive(param)
  3099. if (res.code !== 200) {
  3100. uni.showToast({
  3101. title: res.msg,
  3102. icon: 'none'
  3103. })
  3104. return
  3105. }
  3106. this.liveItem = Object.assign({}, this.liveItem, res.data)
  3107. // 保存服务器时间对应的本地时间,用于计算服务器时间差值
  3108. if (res.serviceTime) {
  3109. this.liveItem._localTimeWhenReceived = Date.now();
  3110. this.$set(this.liveItem, 'serviceTime', res.serviceTime);
  3111. }
  3112. if (res.serviceStartTime) {
  3113. this.$set(this.liveItem, 'serviceStartTime', res.serviceStartTime);
  3114. }
  3115. this.startTimeTimer(this.liveItem)
  3116. if (this.liveStartTimer) {
  3117. clearInterval(this.liveStartTimer)
  3118. this.liveStartTimer = null
  3119. }
  3120. if (res.data.status == 1) {
  3121. this.liveStartTimer = setInterval(async () => {
  3122. this.liveCountdown = this.handleTime(res.data.startTime, 0)
  3123. if (!this.liveCountdown) {
  3124. uni.removeStorageSync('isAgreement')
  3125. const userInfo = uni.getStorageSync('userInfo');
  3126. if (userInfo) {
  3127. await this.getliving(this.liveId)
  3128. }
  3129. clearInterval(this.liveStartTimer)
  3130. }
  3131. }, 1000)
  3132. this.$set(this.liveItem, 'previewUrl', res.data.previewUrl)
  3133. this.$set(this.liveItem, 'livingUrl', '')
  3134. this.$set(this.liveItem, 'videoUrl', '')
  3135. } else if (res.data.status == 2) {
  3136. if (res.data.liveType == 1) {
  3137. let cTime = Math.floor(Math.random() * 10000) + 1
  3138. let livingUrl = res.data.flvHlsUrl + '&t=' + cTime
  3139. this.$set(this.liveItem, 'livingUrl', livingUrl)
  3140. this.$set(this.liveItem, 'videoUrl', '')
  3141. } else if (res.data.liveType === 2) {
  3142. this.$set(this.liveItem, 'videoUrl', res.data.videoUrl)
  3143. this.$set(this.liveItem, 'livingUrl', '')
  3144. }
  3145. } else if (res.data.status == 4 && res.data.liveType == 3) {
  3146. this.$set(this.liveItem, 'videoUrl', res.data.videoUrl)
  3147. this.$set(this.liveItem, 'livingUrl', '')
  3148. } else {
  3149. this.$set(this.liveItem, 'livingUrl', '')
  3150. this.$set(this.liveItem, 'videoUrl', '')
  3151. }
  3152. this.$set(this.liveItem, 'autoplay', res.data.liveType !== 0)
  3153. this.$set(this.liveItem, 'showType', res.data.showType)
  3154. this.storeId = res.storeId
  3155. this.diffLiveStartTime = this.calculateLiveTimeDiff(this.liveItem.startTime);
  3156. if (this.diffLiveStartTime < 0) { //预告
  3157. this.isPreview = true;
  3158. this.liveBeginWatchTime = this.getServerTimeNow();
  3159. } else {
  3160. if (!this.hasLiveEnd) { //没经历过直播结束
  3161. this.liveBeginWatchTime = this.getServerTimeNow();
  3162. }
  3163. }
  3164. console.log("是否调用积分列表", this.liveItem.completionPointsEnabled)
  3165. if (this.liveItem.completionPointsEnabled) {
  3166. this.getRemainingTime()
  3167. }
  3168. // 初始化累计观看时间
  3169. this.initWatchTime();
  3170. this.startLiveViewDataTimer()
  3171. if (this.$refs && this.$refs.liveVideo && typeof this.$refs.liveVideo.playVideo === 'function') {
  3172. this.$refs.liveVideo.playVideo()
  3173. }
  3174. } catch (err) {
  3175. console.error('获取直播信息失败:', err)
  3176. uni.showToast({
  3177. title: '获取直播信息失败',
  3178. icon: 'none'
  3179. })
  3180. }
  3181. },
  3182. getPureDecimal(num, precision = 6) {
  3183. const decimalPart = Math.abs(num).toFixed(precision).split('.')[1]
  3184. return decimalPart?.replace(/0+$/, '') || ''
  3185. },
  3186. goBack() {
  3187. if (this.liveItem) {
  3188. if (this.$refs && this.$refs.liveVideo && typeof this.$refs.liveVideo.pauseVideo === 'function') {
  3189. this.$refs.liveVideo.pauseVideo()
  3190. }
  3191. }
  3192. this.closeWebSocket(true)
  3193. this.postMessage({login: 1,isBack:true,pagesUrl:'/pages_live/livingList'})
  3194. // const pages = getCurrentPages()
  3195. // if (pages.length > 1) {
  3196. // uni.navigateBack()
  3197. // } else {
  3198. // uni.reLaunch({
  3199. // url: '/pages_live/livingList'
  3200. // })
  3201. // }
  3202. },
  3203. async onLike() {
  3204. if (!this.liveId) return
  3205. try {
  3206. const res = await liveDataLike(this.liveId)
  3207. if (res?.like) {
  3208. this.liveViewData.like++
  3209. } else {
  3210. uni.showToast({
  3211. title: res.msg,
  3212. icon: 'none'
  3213. })
  3214. }
  3215. } catch (error) {
  3216. console.error('点赞失败:', error)
  3217. }
  3218. },
  3219. getliveViewData() {
  3220. if (!this.liveId) return
  3221. getLiveViewData(this.liveId).then((res) => {
  3222. if (res.code == 200) {
  3223. this.liveViewData = res
  3224. }
  3225. }).catch((error) => {
  3226. console.error('获取直播间数据失败:', error)
  3227. this.liveViewData = {
  3228. like: 0,
  3229. watchCount: 0
  3230. }
  3231. })
  3232. },
  3233. startLiveViewDataTimer() {
  3234. if (this.liveViewDataTimer) {
  3235. clearInterval(this.liveViewDataTimer)
  3236. this.liveViewDataTimer = null
  3237. }
  3238. this.getliveViewData()
  3239. this.liveViewDataTimer = setInterval(() => {
  3240. if (this.liveId) {
  3241. this.getliveViewData()
  3242. }
  3243. }, 20000)
  3244. },
  3245. goShop(productId, goodsId) {
  3246. if (!this.liveId) return
  3247. // uni.navigateTo({
  3248. // url: '/pages_live/shopping/goods?productId=' + productId + '&liveId=' + this.liveId +
  3249. // '&goodsId=' + goodsId + '&storeId=' + this.storeId
  3250. // })
  3251. this.postMessage({login: 1,pagesUrl:'/pages_live/shopping/goods?productId=' + productId + '&liveId=' + this.liveId +'&goodsId=' + goodsId + '&storeId=' + this.storeId})
  3252. },
  3253. async queryCollect() {
  3254. this.loadingProducts = true
  3255. if (!this.liveId) return
  3256. if (this.inputInfo == null) this.inputInfo = ''
  3257. uni.showLoading({
  3258. title: '加载中'
  3259. })
  3260. try {
  3261. const res = await liveStore(this.liveId, this.inputInfo)
  3262. uni.hideLoading()
  3263. this.shopping = true
  3264. if (res.code === 200) {
  3265. this.products = Array.isArray(res.data) ? res.data : []
  3266. }
  3267. } catch (error) {
  3268. console.error('获取小黄车商品失败:', error)
  3269. } finally {
  3270. this.loadingProducts = false
  3271. }
  3272. },
  3273. initTime() {
  3274. const now = new Date()
  3275. this.timestamp = now.getTime()
  3276. },
  3277. showGift() {
  3278. this.isShowGift = true
  3279. },
  3280. clearChatInput() {
  3281. this.$refs.chatInput.clearInput()
  3282. },
  3283. openCart() {
  3284. this.queryCollect()
  3285. },
  3286. closeGift() {
  3287. this.isShowGift = false
  3288. },
  3289. closeShop() {
  3290. this.shopping = false
  3291. },
  3292. closeMore() {
  3293. this.isMore = false
  3294. },
  3295. closeWin() {
  3296. this.winning = false
  3297. },
  3298. closeWebSocket(isManual = true) {
  3299. if (!this.socket || !this.isSocketOpen) {
  3300. console.warn('WebSocket 任务不存在或未打开,无需关闭')
  3301. return
  3302. }
  3303. console.log(`WebSocket连接关闭 - ${isManual ? '手动' : '自动'}`)
  3304. this.isManualClose = isManual
  3305. this.cleanupAllResources()
  3306. try {
  3307. const socketToClose = this.socket
  3308. this.socket = null
  3309. this.isSocketOpen = false
  3310. this.isConnecting = false
  3311. // APP-PLUS 部分版本 close 不支持传入参数,先带参关闭,失败则无参关闭
  3312. try {
  3313. socketToClose.close({
  3314. code: 1000,
  3315. reason: isManual ? '主动关闭' : '异常关闭'
  3316. })
  3317. } catch (closeErr) {
  3318. if (typeof socketToClose.close === 'function') {
  3319. socketToClose.close()
  3320. }
  3321. }
  3322. console.log('WebSocket连接已发起关闭')
  3323. } catch (err) {
  3324. console.error('关闭WebSocket失败:', err)
  3325. this.socket = null
  3326. this.isSocketOpen = false
  3327. this.isConnecting = false
  3328. }
  3329. },
  3330. cleanupAllResources() {
  3331. this.stopHeartBeat()
  3332. if (this.pingTimeoutTimer) {
  3333. clearTimeout(this.pingTimeoutTimer)
  3334. this.pingTimeoutTimer = null
  3335. }
  3336. if (this.networkStatusTimer) {
  3337. clearTimeout(this.networkStatusTimer)
  3338. this.networkStatusTimer = null
  3339. }
  3340. this.resetReconnectState()
  3341. this.heartBeatRetryCount = 0
  3342. this.lastHeartBeatTime = 0
  3343. this.adaptiveHeartBeatInterval = this.heartBeatInterval
  3344. },
  3345. startHeartBeat() {
  3346. this.stopHeartBeat()
  3347. this.heartBeatTimer = setInterval(() => {
  3348. this.sendHeartBeat()
  3349. }, this.adaptiveHeartBeatInterval)
  3350. },
  3351. initSocket() {
  3352. if (this.isConnecting) {
  3353. console.log('WebSocket正在连接中,跳过重复连接')
  3354. return
  3355. }
  3356. if (!this.isNetworkAvailable) {
  3357. console.warn('网络不可用,延迟WebSocket连接')
  3358. if (!this.networkRetryTimer) {
  3359. this.networkRetryTimer = setTimeout(() => {
  3360. this.networkRetryTimer = null
  3361. this.initSocket()
  3362. }, 3000)
  3363. }
  3364. return
  3365. }
  3366. if (this.socket && this.isSocketOpen) {
  3367. console.log('WebSocket连接已存在且正常,无需重新连接')
  3368. this.reconnectCount = 0
  3369. return
  3370. }
  3371. // APP-PLUS 下 readyState 可能不可用,用 isConnecting/isSocketOpen 判断
  3372. if (this.socket && (this.isConnecting || this.isSocketOpen)) {
  3373. console.log('关闭现有WebSocket连接,创建新连接')
  3374. this.closeWebSocket(false)
  3375. setTimeout(() => {
  3376. this.createWebSocketConnection()
  3377. }, 100)
  3378. return
  3379. }
  3380. this.createWebSocketConnection()
  3381. },
  3382. createWebSocketConnection() {
  3383. if (!this.liveId) {
  3384. console.error('缺失直播间ID,无法初始化WebSocket')
  3385. return
  3386. }
  3387. if (!this.userData || !this.userData.userId) {
  3388. console.error('用户信息缺失,无法初始化WebSocket')
  3389. return
  3390. }
  3391. this.isConnecting = true
  3392. this.isSocketOpen = false
  3393. this.connectionStartTime = Date.now()
  3394. this.resetReconnectState()
  3395. // setTimeout(() => {
  3396. // this.getliveUserInit(false)
  3397. // }, 200)
  3398. const now = new Date()
  3399. this.timestamp = now.getTime()
  3400. const signature = CryptoJS.HmacSHA256(
  3401. `${this.liveId}${this.userData.userId}${this.userType}${this.timestamp}`, this.timestamp.toString()
  3402. ).toString(CryptoJS.enc.Hex)
  3403. try {
  3404. const baseWsUrl = 'wss://im.fhhx.runtzh.com/ws/app/webSocket'
  3405. let wsUrl =
  3406. `${baseWsUrl}?userId=${this.userData.userId}&liveId=${this.liveId}&userType=${this.userType}&timestamp=${this.timestamp}&signature=${signature}`
  3407. if (this.qrFrom) {
  3408. wsUrl += this.qrFrom
  3409. }
  3410. console.log('qxj wsUrl', wsUrl)
  3411. const socketTask = uni.connectSocket({
  3412. url: wsUrl,
  3413. multiple: true, // APP-PLUS 同页面多连接需声明
  3414. success: () => {
  3415. console.log('WebSocket连接请求发送成功')
  3416. },
  3417. fail: (err) => {
  3418. console.error('WebSocket连接请求失败:', err)
  3419. this.isConnecting = false
  3420. this.handleConnectionError('连接请求失败', err)
  3421. }
  3422. })
  3423. // APP-PLUS 下极少数情况可能未返回 SocketTask
  3424. if (!socketTask || typeof socketTask.onOpen !== 'function') {
  3425. console.error('WebSocket connectSocket 未返回有效 SocketTask')
  3426. this.isConnecting = false
  3427. this.handleConnectionError('连接请求失败', new Error('无效的 SocketTask'))
  3428. return
  3429. }
  3430. socketTask.onOpen((res) => {
  3431. console.log('WebSocket连接已打开')
  3432. this.socket = socketTask
  3433. this.isConnecting = false
  3434. this.isSocketOpen = true
  3435. if (this.connectionStartTime > 0) {
  3436. this.connectionLatency = Date.now() - this.connectionStartTime
  3437. console.log(`WebSocket连接延迟: ${this.connectionLatency}ms`)
  3438. }
  3439. this.reconnectCount = 0
  3440. this.resetReconnectState()
  3441. this.heartBeatRetryCount = 0
  3442. this.shownEntryUsers.clear()
  3443. if (this.reconnectCount === 0) {
  3444. console.log('WebSocket连接建立成功')
  3445. } else {
  3446. console.log(`WebSocket重连成功(第${this.reconnectCount}次尝试)`)
  3447. }
  3448. this.startHeartBeat()
  3449. })
  3450. socketTask.onMessage((res) => {
  3451. this.messageCount++
  3452. try {
  3453. // APP-PLUS 下 res.data 可能为字符串或 ArrayBuffer,统一转为字符串
  3454. let raw = res.data
  3455. if (typeof raw !== 'string') {
  3456. if (raw instanceof ArrayBuffer) {
  3457. raw = String.fromCharCode.apply(null, new Uint8Array(raw))
  3458. } else if (raw != null) {
  3459. raw = String(raw)
  3460. } else {
  3461. raw = ''
  3462. }
  3463. }
  3464. const data = JSON.parse(raw)
  3465. if (!!data.data && data.data.cmd === 'heartbeat') {
  3466. if (this.pingTimeoutTimer) {
  3467. clearTimeout(this.pingTimeoutTimer)
  3468. this.pingTimeoutTimer = null
  3469. }
  3470. this.heartBeatRetryCount = 0
  3471. this.adjustHeartBeatInterval(true)
  3472. return
  3473. }
  3474. this.handleSocketMessage({ data: raw })
  3475. } catch (err) {
  3476. console.error('消息解析异常:', err)
  3477. this.errorCount++
  3478. }
  3479. })
  3480. socketTask.onError((err) => {
  3481. console.error('WebSocket连接错误:', err)
  3482. this.errorCount++
  3483. this.isSocketOpen = false
  3484. this.isConnecting = false
  3485. this.stopHeartBeat()
  3486. this.handleConnectionError('连接错误', err)
  3487. })
  3488. socketTask.onClose((res) => {
  3489. console.log('WebSocket连接关闭:', res)
  3490. this.isSocketOpen = false
  3491. this.isConnecting = false
  3492. this.stopHeartBeat()
  3493. if (!this.isManualClose) {
  3494. // APP-PLUS 下 res 或 res.code 可能缺失,按异常关闭处理并重连
  3495. const code = res && res.code
  3496. if (code === 1000) {
  3497. console.log('WebSocket正常关闭,不进行重连')
  3498. } else {
  3499. console.warn(`WebSocket异常关闭 (code: ${code}, reason: ${res && res.reason})`)
  3500. this.handleReconnect()
  3501. }
  3502. } else {
  3503. console.log('WebSocket手动关闭,不进行重连')
  3504. }
  3505. })
  3506. } catch (e) {
  3507. console.error('创建WebSocket异常:', e)
  3508. this.isConnecting = false
  3509. this.handleReconnect()
  3510. }
  3511. },
  3512. handleTime(time, duration) {
  3513. let timeStamp
  3514. if (typeof time === 'number' && time > 0 && time < 9999999999999) {
  3515. timeStamp = time
  3516. } else if (typeof time === 'string' && time.trim() !== '') {
  3517. const match = time.match(/^(\w{3}) (\w{3}) (\d{1,2}) (\d{1,2}):(\d{2}):(\d{2}) CST (\d{4})$/)
  3518. if (match) {
  3519. const [, day, month, date, hours, minutes, seconds, year] = match
  3520. const monthMap = {
  3521. 'Jan': 0,
  3522. 'Feb': 1,
  3523. 'Mar': 2,
  3524. 'Apr': 3,
  3525. 'May': 4,
  3526. 'Jun': 5,
  3527. 'Jul': 6,
  3528. 'Aug': 7,
  3529. 'Sep': 8,
  3530. 'Oct': 9,
  3531. 'Nov': 10,
  3532. 'Dec': 11
  3533. }
  3534. const jsDate = new Date(
  3535. parseInt(year),
  3536. monthMap[month],
  3537. parseInt(date),
  3538. parseInt(hours),
  3539. parseInt(minutes),
  3540. parseInt(seconds)
  3541. )
  3542. timeStamp = jsDate.getTime()
  3543. } else {
  3544. let normalizedTime = time
  3545. if (normalizedTime.includes(' ')) {
  3546. normalizedTime = normalizedTime.replace(' ', 'T')
  3547. }
  3548. const date = new Date(normalizedTime)
  3549. if (!isNaN(date.getTime())) {
  3550. timeStamp = date.getTime()
  3551. } else {
  3552. console.error('无效的日期格式:', time)
  3553. return false
  3554. }
  3555. }
  3556. } else {
  3557. console.error('time参数必须是有效的时间戳(数字)或日期字符串')
  3558. return false
  3559. }
  3560. const targetTimestamp = timeStamp + duration * 60 * 1000
  3561. const currentTimestamp = Date.now()
  3562. const timeDiffMs = targetTimestamp - currentTimestamp
  3563. if (timeDiffMs <= 0) {
  3564. return false
  3565. }
  3566. const hours = Math.floor(timeDiffMs / (1000 * 60 * 60))
  3567. const minutes = Math.floor((timeDiffMs % (1000 * 60 * 60)) / (1000 * 60))
  3568. const seconds = Math.floor((timeDiffMs % (1000 * 60)) / 1000)
  3569. const formatNum = (num) => num.toString().padStart(2, '0')
  3570. return {
  3571. hours: formatNum(hours),
  3572. minutes: formatNum(minutes),
  3573. seconds: formatNum(seconds)
  3574. }
  3575. },
  3576. addToTalkList(message) {
  3577. const MAX_TALK_ITEMS = 30
  3578. if (!Array.isArray(this.talklist)) {
  3579. this.talklist = []
  3580. }
  3581. const wasAtLimit = this.talklist.length >= MAX_TALK_ITEMS
  3582. const isMyMessage = message.userId === this.userData.userId
  3583. try {
  3584. message.uniqueId = ++this.messageIdCounter
  3585. let msgdata = {}
  3586. try {
  3587. msgdata = JSON.parse(message.data)
  3588. } catch (e) {
  3589. console.warn('JSON解析失败:', e)
  3590. msgdata = {}
  3591. }
  3592. message.msgId = msgdata.msgId || Date.now()
  3593. this.talklist.push(message)
  3594. } catch (error) {
  3595. console.error('处理消息时出错:', error)
  3596. }
  3597. if (this.talklist.length > MAX_TALK_ITEMS) {
  3598. const removeCount = this.talklist.length - MAX_TALK_ITEMS
  3599. this.talklist.splice(0, removeCount)
  3600. }
  3601. this.$forceUpdate()
  3602. // 如果是自己的消息,确保滚动到底部
  3603. if (isMyMessage) {
  3604. this.$nextTick(() => {
  3605. // 重置滚动时间戳,确保不会因为防抖而忽略滚动
  3606. this.lastScrollTime = 0
  3607. this.forceScrollToBottomOnSend()
  3608. })
  3609. } else {
  3610. // 其他消息也滚动到底部
  3611. this.$nextTick(() => {
  3612. this.scrollToBottom()
  3613. })
  3614. }
  3615. },
  3616. async handleSocketMessage(message) {
  3617. try {
  3618. let data = JSON.parse(message.data)
  3619. const socketMessage = data.data
  3620. if (data.code == 200) {
  3621. const messageData = {
  3622. ...socketMessage,
  3623. cmd: socketMessage.cmd || '',
  3624. ts: Date.now()
  3625. }
  3626. if (socketMessage.cmd == 'sendMsg') {
  3627. if (!this.isSocketAvailable()) {
  3628. uni.showToast({
  3629. title: '连接已断开,正在重试...',
  3630. icon: 'none'
  3631. })
  3632. this.handleReconnect()
  3633. return
  3634. }
  3635. this.addToTalkList(messageData)
  3636. } else if (socketMessage.cmd == 'red') {
  3637. const redData = socketMessage.data ? JSON.parse(socketMessage.data) : {}
  3638. this.redInfo = redData || {}
  3639. this.isShowRed = socketMessage.status === 1
  3640. if (this.isShowRed) {
  3641. this.redTimer = setInterval(() => {
  3642. const redCountdown = this.handleTime(this.redInfo.updateTime, this.redInfo
  3643. .duration)
  3644. if (!redCountdown) {
  3645. this.isShowRed = false
  3646. clearInterval(this.redTimer)
  3647. }
  3648. }, 1000)
  3649. }
  3650. } else if (socketMessage.cmd == 'goods') {
  3651. try {
  3652. const goodsData = socketMessage.data ? JSON.parse(socketMessage.data) : {}
  3653. this.goodsCard = goodsData || {}
  3654. this.isShowGoods = Number(socketMessage.status) === 1
  3655. console.log('商品卡片数据:', {
  3656. goodsData,
  3657. status: socketMessage.status,
  3658. isShowGoods: this.isShowGoods
  3659. });
  3660. } catch (err) {
  3661. console.error('解析商品卡片数据失败:', err);
  3662. this.goodsCard = {};
  3663. this.isShowGoods = false;
  3664. }
  3665. // 确保在所有平台上都能正确显示商品卡片
  3666. this.$nextTick(() => {
  3667. if (this.isShowGoods) {
  3668. console.log('商品卡片显示状态已更新:', this.isShowGoods);
  3669. }
  3670. });
  3671. } else if (socketMessage.cmd == 'coupon') {
  3672. const couponData = socketMessage.data ? JSON.parse(socketMessage.data) : {}
  3673. this.couponInfo = couponData || {}
  3674. this.isShowCoupon = socketMessage.status === 1
  3675. } else if (socketMessage.cmd == 'likeDetail') {
  3676. this.liveViewData.like = socketMessage.data
  3677. } else if (socketMessage.cmd == 'lottery') {
  3678. const lotteryData = socketMessage.data ? JSON.parse(socketMessage.data) : {}
  3679. this.lotteryInfo = lotteryData || {}
  3680. this.isShowLottery = socketMessage.status === 1
  3681. if (socketMessage.status != 1) {
  3682. this.isShowLotteryPop = false
  3683. }
  3684. clearTimeout(this.lotteryTimer)
  3685. if (this.isShowLottery) {
  3686. this.lotteryTimer = setInterval(() => {
  3687. this.countdown = this.handleTime(this.lotteryInfo.updateTime, this
  3688. .lotteryInfo.duration)
  3689. if (!this.countdown) {
  3690. console.log('倒计时', this.countdown)
  3691. this.isShowLottery = false
  3692. this.isShowLotteryPop = false
  3693. clearInterval(this.lotteryTimer)
  3694. }
  3695. }, 1000)
  3696. } else {
  3697. this.isShowLottery = false
  3698. }
  3699. } else if (socketMessage.cmd == 'deleteMsg') {
  3700. const index = this.talklist.findIndex(item => item.msgId == socketMessage.msg)
  3701. if (index !== -1) {
  3702. this.talklist.splice(index, 1)
  3703. }
  3704. } else if (socketMessage.cmd == 'globalVisible') {} else if (socketMessage.cmd ==
  3705. 'singleVisible') {} else if (socketMessage.cmd == 'entry') {
  3706. try {
  3707. //if (!this.liveUserCalled) {
  3708. // await this.getliveUserInit(false)
  3709. this.liveUserCalled = true;
  3710. //}
  3711. const userIdToEntry = socketMessage.userId
  3712. const existingIndex = this.liveViewersData.findIndex((item) => item.userId ===
  3713. userIdToEntry);
  3714. if (existingIndex === -1) {
  3715. const liveViewers = {
  3716. userId: socketMessage.userId,
  3717. nickName: socketMessage.nickName,
  3718. avatar: socketMessage.avatar
  3719. }
  3720. this.liveViewersData.push(liveViewers)
  3721. this.liveUserTotal++
  3722. }
  3723. const userData = JSON.parse(socketMessage.data || '{}')
  3724. const userId = userData.userId || socketMessage.userId
  3725. if (!userId) return
  3726. if (!this.shownEntryUsers.has(userId)) {
  3727. this.inAndOut = socketMessage;
  3728. this.showWelcomeMessage = true;
  3729. this.shownEntryUsers.add(userId);
  3730. if (this.welcomeTimer) clearTimeout(this.welcomeTimer)
  3731. this.welcomeTimer = setTimeout(() => {
  3732. this.showWelcomeMessage = false
  3733. }, 3000)
  3734. }
  3735. } catch (err) {
  3736. console.error('解析entry用户数据失败:', err)
  3737. }
  3738. } else if (socketMessage.cmd == 'out') {
  3739. if (this.liveUserTotal > 0) {
  3740. const userIdToRemove = socketMessage.userId
  3741. const index = this.liveViewersData.findIndex((item) => item.userId === userIdToRemove)
  3742. if (index !== -1) {
  3743. this.liveViewersData.splice(index, 1)
  3744. this.liveUserTotal--
  3745. }
  3746. }
  3747. this.inAndOut = socketMessage
  3748. this.showWelcomeMessage = true
  3749. if (this.welcomeTimer) clearTimeout(this.welcomeTimer)
  3750. this.welcomeTimer = setTimeout(() => {
  3751. this.showWelcomeMessage = false
  3752. }, 3000)
  3753. } else if (socketMessage.cmd == 'sendTopMsg') {
  3754. clearInterval(this.noticeTimer)
  3755. console.log('公告消息>>>>', socketMessage)
  3756. const noticeData = socketMessage.data ? JSON.parse(socketMessage.data) : {}
  3757. this.notice = noticeData || {}
  3758. this.isShowNotice = true
  3759. if (this.isShowNotice) {
  3760. this.noticeTimer = setInterval(() => {
  3761. const noticeCountdown = this.handleTime(this.notice.endTime, 0)
  3762. if (!noticeCountdown) {
  3763. this.isShowNotice = false
  3764. clearInterval(this.noticeTimer)
  3765. }
  3766. }, 1000)
  3767. }
  3768. } else if (socketMessage.cmd == 'live_start' || socketMessage.cmd == 'live_end') {
  3769. if (this.liveStartTimer) {
  3770. clearInterval(this.liveStartTimer)
  3771. this.liveStartTimer = null
  3772. }
  3773. if (this.redTimer) {
  3774. clearInterval(this.redTimer)
  3775. this.redTimer = null
  3776. }
  3777. const userInfo = uni.getStorageSync('userInfo');
  3778. if (userInfo) {
  3779. await this.getliving(this.liveId)
  3780. }
  3781. } else if (socketMessage.cmd == 'Integral') {
  3782. this.integral = {
  3783. msg: socketMessage.msg,
  3784. status: true
  3785. }
  3786. } else if (socketMessage.cmd == 'userCount') {
  3787. this.liveUserTotal = socketMessage.data
  3788. }else if (socketMessage.cmd == 'LotteryDetail') {
  3789. try {
  3790. this.prizeInfo = Array.isArray(JSON.parse(socketMessage.data || '[]')) ? JSON.parse(
  3791. socketMessage.data || '[]') : []
  3792. } catch (err) {
  3793. console.error('解析抽奖结果失败:', err)
  3794. this.prizeInfo = []
  3795. }
  3796. this.isShowPrize = true
  3797. this.isShowLottery = false
  3798. this.isShowLotteryPop = false
  3799. } else if (socketMessage.cmd == 'blockUser') {
  3800. uni.removeStorage({
  3801. key: 'AppToken',
  3802. success: () => {
  3803. uni.reLaunch({
  3804. url: '/pages/auth/login'
  3805. })
  3806. }
  3807. })
  3808. }
  3809. } else {
  3810. uni.showToast({
  3811. title: data.msg,
  3812. icon: 'none'
  3813. })
  3814. }
  3815. } catch (error) {
  3816. console.error('Socket消息处理失败:', error)
  3817. }
  3818. },
  3819. onBarrage(item) {
  3820. if (this.isSending) return
  3821. this.isSending = true
  3822. setTimeout(() => {
  3823. this.isSending = false
  3824. }, 800)
  3825. const text = (item || '').trim()
  3826. if (!text) {
  3827. uni.showToast({
  3828. title: '不能发送空消息',
  3829. icon: 'none'
  3830. })
  3831. return
  3832. }
  3833. if (!this.isSocketAvailable()) {
  3834. if (retries > 0) {
  3835. uni.showToast({
  3836. title: `连接不稳定,正在重试(${retries}次)...`,
  3837. icon: 'none'
  3838. })
  3839. setTimeout(() => this.sendMsg(retries - 1), 500)
  3840. } else {
  3841. uni.showToast({
  3842. title: '连接已断开,发送失败',
  3843. icon: 'none'
  3844. })
  3845. }
  3846. return
  3847. }
  3848. const liveId = this.liveId
  3849. const data = {
  3850. liveId,
  3851. userId: this.userData.userId,
  3852. userType: 0,
  3853. cmd: 'sendMsg',
  3854. msg: text,
  3855. nickName: this.userData.nickname || '未命名',
  3856. avatar: this.userData.avatar || '/static/images/live/avatar.png'
  3857. }
  3858. try {
  3859. this.socket.send({
  3860. data: JSON.stringify(data),
  3861. success: () => {
  3862. this.forceScrollToBottomOnSend()
  3863. },
  3864. fail: (err) => {
  3865. console.error('消息发送失败:', err)
  3866. if (retries > 0) {
  3867. uni.showToast({
  3868. title: `发送失败,正在重试(${retries}次)`,
  3869. icon: 'none'
  3870. })
  3871. setTimeout(() => this.sendMsg(retries - 1), 500)
  3872. } else {
  3873. uni.showToast({
  3874. title: '发送失败,请稍后再试',
  3875. icon: 'none'
  3876. })
  3877. }
  3878. }
  3879. })
  3880. } catch (err) {
  3881. console.error('发送消息异常:', err)
  3882. if (retries > 0) {
  3883. setTimeout(() => this.sendMsg(retries - 1), 500)
  3884. } else {
  3885. uni.showToast({
  3886. title: '发送失败,请稍后再试',
  3887. icon: 'none'
  3888. })
  3889. }
  3890. }
  3891. },
  3892. sendMsg(retries = 1) {
  3893. if (this.isSending) return
  3894. this.isSending = true
  3895. setTimeout(() => {
  3896. this.isSending = false
  3897. }, 800)
  3898. const text = (this.value || '').trim()
  3899. if (!text) {
  3900. uni.showToast({
  3901. title: '不能发送空消息',
  3902. icon: 'none'
  3903. })
  3904. return
  3905. }
  3906. if (!this.isSocketAvailable()) {
  3907. if (retries > 0) {
  3908. uni.showToast({
  3909. title: `连接不稳定,正在重试(${retries}次)...`,
  3910. icon: 'none'
  3911. })
  3912. setTimeout(() => this.sendMsg(retries - 1), 500)
  3913. } else {
  3914. uni.showToast({
  3915. title: '连接已断开,发送失败',
  3916. icon: 'none'
  3917. })
  3918. this.value = text
  3919. }
  3920. return
  3921. }
  3922. const liveId = this.liveId
  3923. this.value = ''
  3924. const data = {
  3925. liveId,
  3926. userId: this.userData.userId,
  3927. userType: 0,
  3928. cmd: 'sendMsg',
  3929. msg: text,
  3930. nickName: this.userData.nickname || '未命名',
  3931. avatar: this.userData.avatar || '/static/images/live/avatar.png'
  3932. }
  3933. try {
  3934. this.socket.send({
  3935. data: JSON.stringify(data),
  3936. success: () => {
  3937. this.value = ''
  3938. // 滚动会在 addToTalkList 中处理,这里不需要重复滚动
  3939. },
  3940. fail: (err) => {
  3941. console.error('消息发送失败:', err)
  3942. if (retries > 0) {
  3943. uni.showToast({
  3944. title: `发送失败,正在重试(${retries}次)`,
  3945. icon: 'none'
  3946. })
  3947. setTimeout(() => this.sendMsg(retries - 1), 500)
  3948. } else {
  3949. uni.showToast({
  3950. title: '发送失败,请稍后再试',
  3951. icon: 'none'
  3952. })
  3953. this.value = text
  3954. }
  3955. }
  3956. })
  3957. } catch (err) {
  3958. console.error('发送消息异常:', err)
  3959. if (retries > 0) {
  3960. setTimeout(() => this.sendMsg(retries - 1), 500)
  3961. } else {
  3962. uni.showToast({
  3963. title: '发送失败,请稍后再试',
  3964. icon: 'none'
  3965. })
  3966. this.value = text
  3967. }
  3968. }
  3969. }
  3970. }
  3971. }
  3972. </script>
  3973. <style scoped lang="scss">
  3974. .w48{
  3975. width: 48rpx;
  3976. }
  3977. .h48{
  3978. height: 48rpx;
  3979. }
  3980. // 弹窗
  3981. .custom-toast {
  3982. position: fixed;
  3983. top: 50%;
  3984. left: 50%;
  3985. transform: translate(-50%, -50%);
  3986. z-index: 999999;
  3987. }
  3988. .toast-content {
  3989. background-color: rgba(0, 0, 0, 0.7);
  3990. color: white;
  3991. padding: 20rpx 40rpx;
  3992. border-radius: 16rpx;
  3993. max-width: 80vw;
  3994. text-align: center;
  3995. line-height: 1.5;
  3996. font-weight: 500;
  3997. font-size: 32rpx;
  3998. }
  3999. .swiper-wrapper {
  4000. position: relative;
  4001. width: 100%;
  4002. height: 100vh;
  4003. overflow: hidden;
  4004. background-color: #000000;
  4005. .live-bg {
  4006. position: absolute;
  4007. top: 0;
  4008. width: 100%;
  4009. height: 100%;
  4010. }
  4011. .container {
  4012. width: 100%;
  4013. height: 100%;
  4014. position: relative;
  4015. transition: opacity 0.3s ease;
  4016. transform: translateZ(0);
  4017. will-change: opacity;
  4018. &.fullscreen {
  4019. .content-top,
  4020. .side-group,
  4021. .chat-area-container,
  4022. .shop-popup,
  4023. .lottery-popup,
  4024. .more-popup,
  4025. .shop-card,
  4026. .gift-popup,
  4027. .coupon-pop,
  4028. .prize-popup,
  4029. .winning-popup,
  4030. .integral-popup,
  4031. .red-card-popup,
  4032. .viewer-popup {
  4033. display: none !important;
  4034. visibility: hidden !important;
  4035. opacity: 0 !important;
  4036. pointer-events: none !important;
  4037. }
  4038. .live-video {
  4039. position: fixed !important;
  4040. top: 0 !important;
  4041. left: 0 !important;
  4042. width: 100vw !important;
  4043. height: 100vh !important;
  4044. z-index: 99999 !important;
  4045. background: #000 !important;
  4046. }
  4047. }
  4048. }
  4049. }
  4050. .content {
  4051. position: relative;
  4052. z-index: 2;
  4053. height: 100%;
  4054. width: 100%;
  4055. top: 0;
  4056. left: 0;
  4057. display: flex;
  4058. flex-direction: column;
  4059. justify-content: space-between;
  4060. .progress-countdown {
  4061. margin-top: 24rpx;
  4062. z-index: 9000;
  4063. width: 180rpx;
  4064. display: flex;
  4065. flex-direction: column;
  4066. align-items: center;
  4067. background: rgba(0, 0, 0, 0.3);
  4068. border-radius: 16rpx;
  4069. padding: 16rpx;
  4070. flex-direction: column;
  4071. &.progress-vertical {
  4072. top: 15%;
  4073. left: 24rpx;
  4074. }
  4075. .title {
  4076. width: 148rpx;
  4077. height: 28rpx;
  4078. }
  4079. .progress-bar-bg {
  4080. width: 148rpx;
  4081. height: 8rpx;
  4082. margin-top: 16rpx;
  4083. background: #ffffff;
  4084. border-radius: 6rpx;
  4085. overflow: hidden;
  4086. margin-bottom: 16rpx;
  4087. .progress-bar-fill {
  4088. height: 100%;
  4089. background: #face15;
  4090. border-radius: 6rpx;
  4091. transition: width 1s linear;
  4092. }
  4093. }
  4094. .progress-text {
  4095. color: #fff;
  4096. font-size: 20rpx;
  4097. }
  4098. }
  4099. }
  4100. .content-top {
  4101. width: 100%;
  4102. display: flex;
  4103. justify-content: space-between;
  4104. padding: 0 24rpx;
  4105. box-sizing: border-box;
  4106. position: fixed;
  4107. // margin-top: 88rpx;
  4108. top: 0;
  4109. z-index: 5;
  4110. will-change: transform;
  4111. color: #FFFFFF;
  4112. .live-x-f {
  4113. display: flex;
  4114. align-items: center;
  4115. .return-icon{
  4116. width: 64rpx;
  4117. height: 64rpx;
  4118. margin-right: 6rpx;
  4119. }
  4120. .user-name{
  4121. color: #fff;
  4122. margin: 0 6rpx 0 10rpx;
  4123. }
  4124. .avatar-container {
  4125. padding: 6rpx 8rpx;
  4126. height: 64rpx;
  4127. background: rgba(0, 0, 0, 0.5);
  4128. border-radius: 32rpx;
  4129. display: flex;
  4130. align-items: center;
  4131. }
  4132. }
  4133. .end {
  4134. display: flex;
  4135. flex-direction: column;
  4136. align-items: flex-end;
  4137. .end-icon {
  4138. width: 52rpx;
  4139. height: 52rpx;
  4140. margin-right: 4rpx;
  4141. }
  4142. .viewer-preview {
  4143. margin-top: 120rpx;
  4144. display: flex;
  4145. align-items: center;
  4146. .sum {
  4147. width: 80rpx;
  4148. height: 52rpx;
  4149. background: rgba(0, 0, 0, 0.5);
  4150. border-radius: 26rpx;
  4151. font-size: 30rpx;
  4152. color: #ffffff;
  4153. text-align: center;
  4154. line-height: 52rpx;
  4155. }
  4156. }
  4157. .complaint-box {
  4158. width: 140rpx;
  4159. margin-top: 20rpx;
  4160. display: flex;
  4161. align-items: center;
  4162. justify-content: center;
  4163. background: rgba(0, 0, 0, 0.3);
  4164. padding: 16rpx 0;
  4165. color: #fff;
  4166. border-radius: 28rpx;
  4167. z-index: 999;
  4168. font-size: 26rpx;
  4169. .complaint-icon{
  4170. width: 32rpx;
  4171. height: 32rpx;
  4172. margin-right: 10rpx;
  4173. }
  4174. }
  4175. }
  4176. }
  4177. .side-group {
  4178. position: absolute;
  4179. top: 52%;
  4180. right: 24rpx;
  4181. z-index: 1000;
  4182. display: flex;
  4183. flex-direction: column;
  4184. align-items: center;
  4185. will-change: transform;
  4186. &.top2 {
  4187. top: 65%;
  4188. }
  4189. &.top3 {
  4190. top: 55%;
  4191. }
  4192. .side-item {
  4193. font-weight: 500;
  4194. font-size: 22rpx;
  4195. color: #ffffff;
  4196. margin-bottom: 26rpx;
  4197. text-align: center;
  4198. .button-reset {
  4199. background-color: transparent !important;
  4200. padding: 0 !important;
  4201. line-height: 1 !important;
  4202. margin: 0 !important;
  4203. width: auto !important;
  4204. font-weight: 500 !important;
  4205. border-radius: none !important;
  4206. &::after {
  4207. border: none !important;
  4208. padding: 0 !important;
  4209. margin: 0 !important;
  4210. }
  4211. }
  4212. .image {
  4213. width: 72rpx;
  4214. height: auto;
  4215. }
  4216. .txt {
  4217. font-size: 30rpx;
  4218. margin-top: 4rpx;
  4219. }
  4220. }
  4221. }
  4222. .chat-area-container {
  4223. position: fixed;
  4224. width: 100%;
  4225. bottom: 0;
  4226. z-index:100;
  4227. transform: translateZ(0);
  4228. will-change: transform;
  4229. backface-visibility: hidden;
  4230. &.chat-area-focused {
  4231. transform: translateY(calc(-1 * var(--keyboard-height, 0rpx))) translateZ(0);
  4232. // z-index: 1000;
  4233. }
  4234. }
  4235. .chat-content {
  4236. height: 30vh;
  4237. //overflow: hidden;
  4238. overflow-y: scroll;
  4239. padding-left: 20rpx;
  4240. transform: translateZ(0);
  4241. will-change: height;
  4242. display: flex;
  4243. flex-direction: column;
  4244. &.chat-content-preview {
  4245. height: 20vh;
  4246. }
  4247. &.chat-content-focused {
  4248. height: 64rpx;
  4249. }
  4250. }
  4251. .shop-prompt {
  4252. width: auto;
  4253. display: inline-flex !important;
  4254. max-width: max-content;
  4255. padding: 6rpx 20rpx;
  4256. background: rgba(230, 154, 34, 0.7);
  4257. border-radius: 24rpx;
  4258. z-index: 9;
  4259. font-weight: 500;
  4260. color: #ffffff;
  4261. transition: opacity 0.3s ease;
  4262. margin-bottom: 20rpx;
  4263. font-size: 30rpx;
  4264. .shopping-tip {
  4265. width: 32rpx;
  4266. height: 32rpx;
  4267. margin-right: 8rpx;
  4268. }
  4269. }
  4270. .welcome-message {
  4271. width: 100%;
  4272. color: white;
  4273. border-radius: 20rpx;
  4274. z-index: 10;
  4275. transition: opacity 0.3s ease;
  4276. }
  4277. .notice-message {
  4278. max-width: 80%;
  4279. padding: 24rpx;
  4280. background: linear-gradient(135deg,
  4281. rgba(255, 220, 41, 0.8) 0%,
  4282. rgba(218, 187, 30, 0.4) 100%);
  4283. margin-bottom: 10rpx;
  4284. border-radius: 20rpx;
  4285. color: #ffffff;
  4286. }
  4287. .scrolly {
  4288. width: calc(100% - 20rpx);
  4289. -webkit-overflow-scrolling: touch;
  4290. }
  4291. .list {
  4292. width: 80%;
  4293. margin-bottom: 20rpx;
  4294. animation: simpleFade 0.2s;
  4295. .talk-list {
  4296. will-change: transform;
  4297. contain: layout style paint;
  4298. backface-visibility: hidden;
  4299. transform: translateZ(0);
  4300. max-width: 100%;
  4301. border-radius: 30rpx;
  4302. background: rgba(0, 0, 0, 0.3);
  4303. padding: 10rpx 30rpx;
  4304. .talk-item {
  4305. line-height: 1.4;
  4306. word-break: break-all;
  4307. word-wrap: break-word;
  4308. overflow-wrap: break-word;
  4309. .nickname {
  4310. color: #ffda73;
  4311. }
  4312. .message {
  4313. color: #ffffff;
  4314. }
  4315. }
  4316. }
  4317. }
  4318. .barrage {
  4319. background: rgba(0, 0, 0, 0.3);
  4320. padding: 12rpx 20rpx;
  4321. color: #fff;
  4322. margin: 30rpx 20rpx 0 20rpx;
  4323. border-radius: 30rpx;
  4324. white-space: nowrap;
  4325. }
  4326. .prize-card {
  4327. width: 504rpx;
  4328. padding: 40rpx 40rpx 30rpx;
  4329. box-sizing: border-box;
  4330. display: flex;
  4331. flex-direction: column;
  4332. border-radius: 20rpx;
  4333. align-items: center;
  4334. background: linear-gradient(180deg, #f7823f 0%, #ffd4be 27%, #ffffff 100%);
  4335. position: relative;
  4336. .nav-img {
  4337. width: 311rpx;
  4338. position: absolute;
  4339. top: -122rpx;
  4340. left: 50%;
  4341. transform: translateX(-50%);
  4342. }
  4343. .title {
  4344. color: #c32008;
  4345. font-size: 34rpx;
  4346. font-weight: 600;
  4347. margin: 20rpx 0 40rpx;
  4348. }
  4349. .prize-content {
  4350. width: 100%;
  4351. display: flex;
  4352. justify-content: space-between;
  4353. align-items: center;
  4354. font-size: 28rpx;
  4355. margin: 10rpx 0;
  4356. .txt {
  4357. font-weight: 600;
  4358. }
  4359. }
  4360. .tip {
  4361. font-size: 28rpx;
  4362. color: #414141;
  4363. margin: 40rpx 0;
  4364. }
  4365. .button {
  4366. width: 200rpx;
  4367. height: 70rpx;
  4368. line-height: 70rpx;
  4369. background: linear-gradient(180deg, #ff7c30 0%, #ff3a1e 100%);
  4370. border-radius: 28rpx;
  4371. font-weight: 500;
  4372. font-size: 32rpx;
  4373. color: #ffffff;
  4374. text-align: center;
  4375. }
  4376. }
  4377. .no-prize-card {
  4378. width: 504rpx;
  4379. padding: 40rpx 40rpx 30rpx;
  4380. box-sizing: border-box;
  4381. display: flex;
  4382. flex-direction: column;
  4383. border-radius: 20rpx;
  4384. align-items: center;
  4385. position: relative;
  4386. .img {
  4387. margin-top: 40rpx;
  4388. width: 300rpx;
  4389. height: 300rpx;
  4390. }
  4391. .tip {
  4392. font-size: 36rpx;
  4393. color: #414141;
  4394. margin: 40rpx 0;
  4395. }
  4396. .button {
  4397. width: 220rpx;
  4398. height: 80rpx;
  4399. line-height: 80rpx;
  4400. background: linear-gradient(180deg, #fdfbb8 0%, #b79243 100%);
  4401. border-radius: 28rpx;
  4402. font-weight: 500;
  4403. font-size: 32rpx;
  4404. color: #ffffff;
  4405. text-align: center;
  4406. }
  4407. }
  4408. .red-card {
  4409. width: 550rpx;
  4410. height: 636rpx;
  4411. position: relative;
  4412. .img {
  4413. position: absolute;
  4414. width: 100%;
  4415. height: 100%;
  4416. }
  4417. .red-content {
  4418. position: relative;
  4419. z-index: 5;
  4420. display: flex;
  4421. flex-direction: column;
  4422. align-items: center;
  4423. .title {
  4424. font-size: 36rpx;
  4425. color: #ff3a1e;
  4426. margin: 180rpx 0 90rpx;
  4427. }
  4428. .txt {
  4429. font-size: 28rpx;
  4430. color: #ffecc3;
  4431. margin: 80rpx 0 40rpx;
  4432. }
  4433. .button {
  4434. width: 392rpx;
  4435. height: 96rpx;
  4436. line-height: 96rpx;
  4437. background: linear-gradient(180deg, #fff4d5 0%, #ffe5b1 100%);
  4438. border-radius: 48rpx;
  4439. font-weight: 600;
  4440. font-size: 36rpx;
  4441. color: #c32008;
  4442. text-align: center;
  4443. }
  4444. }
  4445. }
  4446. .integral-box {
  4447. min-width: 400rpx;
  4448. max-width: 600rpx;
  4449. display: flex;
  4450. flex-direction: column;
  4451. align-items: center;
  4452. border-radius: 20rpx;
  4453. overflow: hidden;
  4454. .top {
  4455. width: 100%;
  4456. position: relative;
  4457. .title {
  4458. width: 100%;
  4459. font-weight: 600;
  4460. font-size: 40rpx;
  4461. color: #ffffff;
  4462. text-shadow: 0px 4rpx 8rpx rgba(255, 89, 2, 0.8);
  4463. position: absolute;
  4464. top: 50rpx;
  4465. text-align: center;
  4466. }
  4467. .photo {
  4468. width: 100%;
  4469. }
  4470. }
  4471. .item {
  4472. padding: 20rpx;
  4473. .title {
  4474. font-weight: 500;
  4475. font-size: 32rpx;
  4476. text-align: center;
  4477. }
  4478. .button {
  4479. font-size: 32rpx;
  4480. margin-top: 20rpx;
  4481. background: linear-gradient(270deg, #ff4702 0%, #fe6304 100%);
  4482. color: #fff;
  4483. text-align: center;
  4484. padding: 18rpx 60rpx;
  4485. border-radius: 10rpx;
  4486. font-weight: 500;
  4487. }
  4488. }
  4489. }
  4490. .coupon-pop {
  4491. position: fixed;
  4492. bottom: 140rpx;
  4493. right: 100rpx;
  4494. z-index: 5;
  4495. border-radius: 20rpx;
  4496. width: 320rpx;
  4497. .coupon-block {
  4498. width: 100%;
  4499. position: relative;
  4500. .bg {
  4501. height: 452rpx;
  4502. width: 100%;
  4503. }
  4504. .nav {
  4505. position: absolute;
  4506. height: 120rpx;
  4507. top: -120rpx;
  4508. left: 0;
  4509. width: 100%;
  4510. z-index: 6;
  4511. }
  4512. .close {
  4513. position: absolute;
  4514. right: 10rpx;
  4515. top: 10rpx;
  4516. z-index: 99;
  4517. }
  4518. .item {
  4519. width: 100%;
  4520. position: absolute;
  4521. top: 20rpx;
  4522. display: flex;
  4523. flex-direction: column;
  4524. align-items: center;
  4525. color: #fff;
  4526. .title {
  4527. font-weight: 500;
  4528. font-size: 30rpx;
  4529. margin: 16rpx 0 12rpx;
  4530. }
  4531. .price {
  4532. font-size: 40rpx;
  4533. .bold {
  4534. font-size: 56rpx;
  4535. font-weight: 600;
  4536. }
  4537. }
  4538. .txt {
  4539. font-weight: 500;
  4540. font-size: 30rpx;
  4541. margin: 5rpx 0;
  4542. }
  4543. .button {
  4544. background: linear-gradient(270deg, #fffce1 0%, #ffeaaf 100%);
  4545. color: #ff0004;
  4546. text-align: center;
  4547. padding: 16rpx 0;
  4548. border-radius: 40rpx;
  4549. font-weight: 500;
  4550. font-size: 30rpx;
  4551. width: 70%;
  4552. margin-top: 26rpx;
  4553. }
  4554. }
  4555. }
  4556. }
  4557. .more-block {
  4558. border-radius: 20rpx 0 0 20rpx;
  4559. padding: 70rpx 30rpx;
  4560. display: flex;
  4561. justify-content: space-between;
  4562. .item {
  4563. text-align: center;
  4564. }
  4565. }
  4566. .integral-popup {
  4567. min-width: 400rpx;
  4568. max-width: 600rpx;
  4569. display: flex;
  4570. flex-direction: column;
  4571. align-items: center;
  4572. border-radius: 20rpx;
  4573. overflow: hidden;
  4574. .integral-header {
  4575. width: 100%;
  4576. position: relative;
  4577. .integral-title {
  4578. width: 100%;
  4579. font-weight: 600;
  4580. font-size: 40rpx;
  4581. color: #ffffff;
  4582. text-shadow: 0px 4rpx 8rpx rgba(255, 89, 2, 0.8);
  4583. position: absolute;
  4584. top: 50rpx;
  4585. text-align: center;
  4586. }
  4587. .integral-background-image {
  4588. width: 100%;
  4589. }
  4590. }
  4591. .integral-content {
  4592. padding: 20rpx;
  4593. .integral-message {
  4594. font-weight: 500;
  4595. font-size: 32rpx;
  4596. text-align: center;
  4597. }
  4598. .integral-confirm-button {
  4599. font-size: 32rpx;
  4600. margin-top: 20rpx;
  4601. background: linear-gradient(270deg, #ff4702 0%, #fe6304 100%);
  4602. color: #fff;
  4603. text-align: center;
  4604. padding: 18rpx 60rpx;
  4605. border-radius: 10rpx;
  4606. font-weight: 500;
  4607. }
  4608. }
  4609. }
  4610. .gift {
  4611. padding: 32rpx 24rpx;
  4612. color: #ffffff;
  4613. .gift-top {
  4614. margin-bottom: 32rpx;
  4615. display: flex;
  4616. justify-content: space-between;
  4617. align-items: center;
  4618. .left {
  4619. font-weight: 600;
  4620. font-size: 28rpx;
  4621. .orange {
  4622. color: #f4a007;
  4623. }
  4624. }
  4625. }
  4626. .gift-block {
  4627. display: flex;
  4628. justify-content: space-between;
  4629. flex-wrap: wrap;
  4630. .item {
  4631. width: 162rpx;
  4632. height: 212rpx;
  4633. text-align: center;
  4634. margin-bottom: 20rpx;
  4635. border: 1rpx solid transparent;
  4636. .name {
  4637. font-size: 24rpx;
  4638. color: rgba(255, 255, 255, 0.8);
  4639. margin: 16rpx 0 12rpx;
  4640. }
  4641. .number {
  4642. font-size: 18rpx;
  4643. color: rgba(255, 255, 255, 0.4);
  4644. }
  4645. .button {
  4646. font-size: 24rpx;
  4647. width: 100%;
  4648. height: 56rpx;
  4649. line-height: 56rpx;
  4650. background: linear-gradient(270deg, #ff5701 0%, #ffb501 100%);
  4651. }
  4652. }
  4653. .active {
  4654. overflow: hidden;
  4655. background: #333333;
  4656. border-radius: 16rpx;
  4657. border: 1rpx solid rgba(255, 255, 255, 0.1);
  4658. .name {
  4659. margin: 4rpx 0 14rpx;
  4660. }
  4661. }
  4662. }
  4663. }
  4664. .view-box {
  4665. position: relative;
  4666. height: 40vh;
  4667. padding: 40rpx 0rpx;
  4668. box-sizing: border-box;
  4669. display: flex;
  4670. flex-direction: column;
  4671. .scroll-content {
  4672. flex: 1;
  4673. margin-top: 50rpx;
  4674. overflow-y: auto;
  4675. padding: 0 40rpx;
  4676. }
  4677. .bottom {
  4678. padding: 20rpx 40rpx;
  4679. position: absolute;
  4680. bottom: 0;
  4681. width: 100%;
  4682. box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(195, 195, 195, 0.3);
  4683. background: #fff;
  4684. }
  4685. }
  4686. @keyframes simpleFade {
  4687. from {
  4688. opacity: 0;
  4689. }
  4690. to {
  4691. opacity: 1;
  4692. }
  4693. }
  4694. .skeleton-item {
  4695. display: flex;
  4696. padding: 20rpx;
  4697. background: #fff;
  4698. margin-bottom: 16rpx;
  4699. border-radius: 16rpx;
  4700. }
  4701. .skeleton-img {
  4702. width: 200rpx;
  4703. height: 200rpx;
  4704. background: #f0f0f0;
  4705. border-radius: 8rpx;
  4706. margin-right: 24rpx;
  4707. animation: pulse 1.5s ease-in-out infinite;
  4708. }
  4709. .skeleton-content {
  4710. flex: 1;
  4711. }
  4712. .skeleton-line {
  4713. height: 20rpx;
  4714. background: #f0f0f0;
  4715. margin-bottom: 16rpx;
  4716. border-radius: 4rpx;
  4717. animation: pulse 1.5s ease-in-out infinite;
  4718. &.short {
  4719. width: 60%;
  4720. }
  4721. &.medium {
  4722. width: 80%;
  4723. }
  4724. &.long {
  4725. width: 95%;
  4726. }
  4727. }
  4728. @keyframes pulse {
  4729. 0% {
  4730. opacity: 1;
  4731. }
  4732. 50% {
  4733. opacity: 0.5;
  4734. }
  4735. 100% {
  4736. opacity: 1;
  4737. }
  4738. }
  4739. .text-white {
  4740. color: #ffffff;
  4741. }
  4742. .text-xs {
  4743. font-size: 18rpx;
  4744. }
  4745. .text-sm {
  4746. font-size: 24rpx;
  4747. }
  4748. .w52,
  4749. .w72 {
  4750. display: flex;
  4751. align-items: center;
  4752. justify-content: center;
  4753. }
  4754. .w52.h52.mr4 {
  4755. border-radius: 26rpx;
  4756. }
  4757. .align-center {
  4758. display: flex;
  4759. align-items: center;
  4760. }
  4761. .justify-start {
  4762. display: flex;
  4763. justify-content: flex-start;
  4764. }
  4765. .flex-1 {
  4766. flex: 1;
  4767. }
  4768. .column {
  4769. flex-direction: column;
  4770. }
  4771. .colorf {
  4772. color: #ffffff;
  4773. }
  4774. .orange {
  4775. color: #f4a007;
  4776. }
  4777. ::v-deep .u-icon--right {
  4778. justify-content: flex-end !important;
  4779. }
  4780. /* 微信昵称授权弹窗样式 */
  4781. .userlogo {
  4782. padding: 36rpx 40rpx;
  4783. color: #000000;
  4784. .mt42 {
  4785. margin-top: 42rpx;
  4786. }
  4787. .boxweixin {
  4788. width: 44rpx;
  4789. height: 44rpx;
  4790. border-radius: 50%;
  4791. text-align: center;
  4792. line-height: 34rpx;
  4793. color: #0a0;
  4794. }
  4795. .button-container {
  4796. position: relative;
  4797. margin: auto;
  4798. .hidden-input {
  4799. width: calc(100vw - 80rpx);
  4800. text-align: center;
  4801. height: 104rpx;
  4802. background: #07C160;
  4803. padding: 30rpx 0;
  4804. box-sizing: border-box;
  4805. border-radius: 12rpx;
  4806. font-size: 32rpx !important;
  4807. color: #FFFFFF !important;
  4808. /* 输入文字大小 */
  4809. }
  4810. /* 针对 placeholder 的样式 */
  4811. .hidden-input::placeholder {
  4812. font-size: 44rpx !important;
  4813. color: #FFFFFF !important;
  4814. }
  4815. /* 兼容微信小程序的 placeholder 样式 */
  4816. .hidden-input .placeholder {
  4817. font-size: 44rpx !important;
  4818. color: #FFFFFF !important;
  4819. }
  4820. }
  4821. .submitname {
  4822. height: 104rpx;
  4823. line-height: 104rpx;
  4824. background: #F2F2F2;
  4825. border-radius: 12rpx;
  4826. font-size: 32rpx;
  4827. color: #07C160;
  4828. margin-top: 32rpx;
  4829. text-align: center;
  4830. }
  4831. }
  4832. </style>