living.vue 69 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733
  1. <template>
  2. <view class="swiper-wrapper">
  3. <view class="container" :class="{active: true}">
  4. <view class="blackbg"></view>
  5. <view class="content">
  6. <!-- 页面内容 -->
  7. <view style="position: fixed;top: 0;z-index: 5;" class="content-top">
  8. <view class="u-flex-y-center">
  9. <image @click="goBack" class="w60 h64 mr26" src="/static/images/live/return.png">
  10. </image>
  11. <view class="align-center"
  12. style="padding: 6rpx 8rpx;height: 64rpx;background: rgba(0,0,0,0.5);border-radius: 32rpx;">
  13. <u-avatar :src="liveItem.liveImgUrl||$img.logo" size="32"></u-avatar>
  14. <view class="colorf ml10 mr6">
  15. <view>{{liveItem.liveName?truncateString(liveItem.liveName,8):"未命名"}}</view>
  16. </view>
  17. </view>
  18. </view>
  19. <view class="u-flex-end align-center" @click="toggleViewerList" style="margin-top: 120rpx;">
  20. <image v-if="Array.isArray(filteredViewers)" class="w52 h52 mr4"
  21. v-for="(item,viewerIndex) in filteredViewers" :key="viewerIndex"
  22. style="border-radius: 26rpx;" :src="item.avatar||$img.logo">
  23. </image>
  24. <view class="sum">{{liveUserTotal||0}}</view>
  25. </view>
  26. </view>
  27. <!-- 右边的 -->
  28. <view :class=" liveItem.showType==1 ? 'siderow-group' : 'side-group'">
  29. <view class="side-item">
  30. <image class="image" @click="onLike(liveItem)" src="/static/images/like.png">
  31. </image>
  32. <view>{{liveItem.liveViewData?.like||0}}</view>
  33. </view>
  34. <!-- -->
  35. <view class="side-item">
  36. <button open-type="share" class="button">
  37. <image class="image" src="/static/images/share.png" mode="widthFix"></image>
  38. </button>
  39. <view>分享</view>
  40. </view>
  41. </view>
  42. <!-- <view class="hongbao-box" v-if="redInfo?.redStatus==1&&isShowRed">
  43. <view class="u-flex-y-center">
  44. <view class="tip">领红包</view>
  45. <view class="item">
  46. <image @click="onRed()" src="/static/images/redbag.png" mode="widthFix">
  47. </image>
  48. </view>
  49. </view>
  50. </view> -->
  51. <view class="activity-box">
  52. <!-- v-if="redInfo?.redStatus==1&&isShowRed" -->
  53. <view class="item-box" @click="onRed()" v-if="isShowRed">
  54. <view class="u-flex-y-center">
  55. <view class="tip">领红包</view>
  56. <view class="item">
  57. <image class="w70" src="/static/images/redbag.png" mode="widthFix">
  58. </image>
  59. </view>
  60. </view>
  61. </view>
  62. <view class="item-box" @click="onLottery()" v-if="isShowLottery">
  63. <view class="u-flex-y-center">
  64. <view class="tip">抽奖</view>
  65. <view class="item">
  66. <image class="w60" src="/static/images/lottery.png" mode="widthFix">
  67. </image>
  68. </view>
  69. </view>
  70. </view>
  71. </view>
  72. <view class="shop-prompt f30 u-flex-y-center" v-if="showPurchasePrompt&&orderUser?.count">
  73. <image class="w32 h32 mr8" src="/static/images/live/shopping.png"></image>
  74. <text> {{orderUser.userName ? maskString(orderUser.userName) : ''}} 等
  75. {{orderUser.count || 0}}
  76. 人正在去购买</text>
  77. </view>
  78. <!-- style="margin: auto 0" -->
  79. <view class="videolist">
  80. <view class="video" :class="liveItem.showType == 1 ? 'video_row' : ''">
  81. <!-- 视频组件 -->
  82. <!-- <live-player v-if=" liveItem.livingUrl && liveItem.liveType === 1"
  83. :id="'myLivePlayer_' + liveId" :src="liveItem.livingUrl" autoplay mode="live"
  84. object-fit="contain" :waiting="false" :muted="false"
  85. :orientation="liveItem.showType == 1 ? 'horizontal' : 'vertical'"
  86. @statechange="onLiveStateChange($event, liveItem)" @error="onLiveError($event, liveItem)"
  87. class=" item"></live-player> -->
  88. <live-player v-if="liveItem.livingUrl && liveItem.liveType === 1" :id="'myLivePlayer_' + liveId"
  89. :src="liveItem.livingUrl" autoplay mode="live" object-fit="contain" :muted="false"
  90. orientation="vertical" :enable-play-gesture="true"
  91. @statechange="onLiveStateChange($event, liveItem)" @error="onLiveError($event, liveItem)"
  92. class="item"></live-player>
  93. <!-- <video v-if="currentSwiperIndex === index && liveItem.livingUrl"
  94. :id="'myVideo_' + liveItem.liveId"
  95. :class="liveItem.showType == 1 ? 'video_row' : 'videotop'" :src="liveItem.livingUrl"
  96. :controls='false' object-fit='contain' :custom-cache="false"
  97. :enable-progress-gesture="false" vslide-gesture-in-fullscreen='true'
  98. :show-center-play-btn="false" :http-cache="false" @error="videoError">
  99. </video> -->
  100. <!-- <video v-if=" liveItem.videoUrl" :class="liveItem.showType == 1 ? 'video_row' : 'videotop'"
  101. :src="liveItem.videoUrl" :autoplay="true" :controls='false' object-fit='contain'
  102. :custom-cache="false" :enable-progress-gesture="false" vslide-gesture-in-fullscreen='true'
  103. :show-center-play-btn="false" :http-cache="false" loop @error="videoError">
  104. </video> -->
  105. <!-- 录播 -->
  106. <video v-if="liveItem.videoUrl&&liveItem.liveType==2" :id="`myVideo_${liveId}`" class="item"
  107. :src="liveItem.videoUrl" :autoplay="true" :controls="false" object-fit="contain"
  108. :custom-cache="false" :enable-progress-gesture="false" vslide-gesture-in-fullscreen="true"
  109. :show-center-play-btn="false" :http-cache="false" loop @error="videoError"
  110. @timeupdate="onVideoTimeUpdate" @loadeddata="onVideoLoaded"
  111. @loadedmetadata="onVideoMetaLoaded" @pause="onVideoPause" @play="onVideoPlay">
  112. </video>
  113. <!-- 直播回放 -->
  114. <!-- :enable-progress-gesture="true" -->
  115. <video v-if="liveItem.videoUrl&&liveItem.liveType==3" :id="`myVideo_${liveId}`" class="item"
  116. :src="liveItem.videoUrl" :autoplay="true" :controls="true" object-fit="contain"
  117. :custom-cache="false" :enable-progress-gesture="liveItem.isSpeedAllowed"
  118. vslide-gesture-in-fullscreen="true" :show-center-play-btn="true" :http-cache="false" loop
  119. @error="videoError" @timeupdate="onVideoTimeUpdate" @loadeddata="onVideoLoaded"
  120. @loadedmetadata="onVideoMetaLoaded" @pause="onVideoPause" @play="onVideoPlay"
  121. @seek="onVideoSeek">
  122. </video>
  123. <!-- v-if="liveItem.liveType" -->
  124. <view v-if="liveItem.videoUrl&&liveItem.liveType==2" class="time">{{liveItem.totalTime}}</view>
  125. <view v-if="liveItem.videoUrl&&liveItem.liveType==3" class="lable">直播回放</view>
  126. </view>
  127. </view>
  128. <!-- 底部聊天区域 -->
  129. <view class="pb40 " style="position: fixed;bottom: 120rpx;width: 100%;">
  130. <view class="w100 mt20" style="height: 35vh;">
  131. <scroll-view enable-flex scroll-y="true" class=" p20 scrolly flex-1 column"
  132. style="width: calc(100% - 40rpx);height: calc(100% - 40rpx);"
  133. :scroll-into-view="scrollIntoView">
  134. <!-- <view>
  135. <view class="list justify-start" v-for="(item,talkIndex) in inAndOut" :key="talkIndex"
  136. v-show="item.cmd=='announcement'">
  137. <view class="talk-list ml16 justify-start">
  138. <view class="fs30">
  139. <text class='colorf'>
  140. {{item.msg}}直播间{{liveItem.messageContent}}</text>
  141. </view>
  142. </view>
  143. </view>
  144. </view> -->
  145. <view class="list justify-start" v-for="(item,talkIndex) in talklist" :key="talkIndex"
  146. :id="`list_${talkIndex}`" v-show="item.cmd!='red'&&item.cmd!='out'&&item.cmd!='entry'">
  147. <view class="talk-list ml16 justify-start">
  148. <view class="fs30">
  149. <text style="color: #FFDA73;">{{item.nickName}}:</text>
  150. <text class='colorf'>{{item.msg}}</text>
  151. </view>
  152. </view>
  153. </view>
  154. <view v-if="showWelcomeMessage" class="welcome-message">
  155. <view class="list justify-start" v-show="inAndOut.cmd=='entry'||inAndOut.cmd=='out'">
  156. <view class="talk-list ml16 justify-start">
  157. <view class="fs30">
  158. <text style="color: #ff89d6;">{{inAndOut.nickName}} </text>
  159. <text class='colorf'>
  160. {{inAndOut.msg}}直播间{{liveItem.messageContent}}</text>
  161. </view>
  162. </view>
  163. </view>
  164. </view>
  165. </scroll-view>
  166. </view>
  167. <!-- 底部输入框和操作按钮 -->
  168. <view class="justify-between p24 input-box">
  169. <view class="u-flex-y-center w580"
  170. style="background:rgba(57, 57, 57, 1);padding:10rpx 14rpx 10rpx 32rpx;box-sizing:border-box;border-radius:36rpx;">
  171. <u-input :placeholder="placeholderText" border="none" customStyle='font-size:24rpx;'
  172. v-model="value" shape='circle' color='#fff' placeholderStyle='color:#e7e7e7'
  173. class="ml20" @focus="inputFocus" @blur="inputBlur">
  174. </u-input>
  175. <view class="send" @click="sendMsg()">发送</view>
  176. </view>
  177. <view class="justify-between mr15 align-center">
  178. <view class="icon-bg ml20" @click="openCart()">
  179. <image src="/static/images/shopping.png" class="w58 h58"></image>
  180. </view>
  181. </view>
  182. </view>
  183. </view>
  184. </view>
  185. <view class="goods" v-if="isShowGoods">
  186. <view class="top">
  187. <view class="left">
  188. <image class="w30 h30 mr8" src="/static/images/signal.png"></image>讲解中
  189. </view>
  190. <image class="w44 h44 mr10" src="/static/images/close.png" @click="isShowGoods=false">
  191. </image>
  192. </view>
  193. <image class="photo" :src="goodsCard?.imgUrl"></image>
  194. <view class="item">
  195. <view class="price"><text class="red">¥{{goodsCard?.price}} </text><text
  196. class="del">¥{{goodsCard?.otPrice}}</text></view>
  197. <view class="title oneline-hide">{{goodsCard?.productName}}</view>
  198. <view class="button" @click="goShop(goodsCard?.productId,goodsCard?.goodsId)">
  199. 立即抢购
  200. </view>
  201. </view>
  202. </view>
  203. <!-- <u-popup :show="isShowGoods" @close="!isShowGoods" round='20rpx' mode="center" zIndex='10099'
  204. bgColor='#ffffff'>
  205. <view class="goods">
  206. <view class="top">
  207. <view class="left">
  208. <image class="w30 h30 mr8" src="/static/images/signal.png"></image>讲解中
  209. </view>
  210. <image class="w44 h44 mr10" src="/static/images/close.png" @click="isShowGoods=false">
  211. </image>
  212. </view>
  213. <image class="photo" :src="goodsCard?.imgUrl"></image>
  214. <view class="item">
  215. <view class="price"><text class="red">¥{{goodsCard?.price}} </text><text
  216. class="del">¥{{goodsCard?.otPrice}}</text></view>
  217. <view class="title oneline-hide">{{goodsCard?.productName}}</view>
  218. <view class="button" @click="goShop(goodsCard?.productId,goodsCard?.goodsId)">
  219. 立即抢购
  220. </view>
  221. </view>
  222. </view>
  223. </u-popup> -->
  224. <u-popup :show="isShowLotteryPop" @close="!isShowLotteryPop" round='40rpx'>
  225. <view class="prize-box" style="border-radius: 40rpx;height: fit-content;">
  226. <image class="nav-img " src="/static/images/red_head.png" mode="widthFix"></image>
  227. <image class="bg-img " src="/static/images/red_bg.png"></image>
  228. <view class="prize-content">
  229. <view class="u-flex-row-reverse u-flex mr20">
  230. <u-icon name="close" color="#fff" size="20" @click="isShowLotteryPop=false"></u-icon>
  231. </view>
  232. <view class="column align-center ">
  233. <image class="w446 h80" src="/static/images/red_title.png"></image>
  234. <view class="fs24 colorf u-flex-y-center mt30 mb30">
  235. 52人已参与,开奖倒计时
  236. <!-- <view class="white-item">{{lotteryTime.hours}}</view>:
  237. <view class="white-item">{{lotteryTime.minutes}}</view>:
  238. <view class="white-item">{{lotteryTime.seconds}}</view> -->
  239. <u-count-down :time="lotteryTime||0" format="DD:HH:mm:ss" autoStart millisecond
  240. @change="onChange">
  241. <view class="u-flex-y-center">
  242. <view class="white-item">
  243. {{ timeData.hours>10?timeData.hours:'0'+timeData.hours}}
  244. </view>
  245. <view class="white-item">{{ timeData.minutes }}</view>
  246. <view class="white-item">{{ timeData.seconds }}</view>
  247. </view>
  248. </u-count-down>
  249. </view>
  250. <!-- <view class="item-group">
  251. <three-d-swiper :items="swiperItems" :swiperHeight="348" :autoPlay="4000"
  252. :indicatorTop="20"></three-d-swiper>
  253. </view> -->
  254. <view class="item-group">
  255. <ThreeItemSwiper :products="lotteryProducts"></ThreeItemSwiper>
  256. </view>
  257. <!-- <view class="item-group">
  258. <view class="item center">
  259. <image class="w280 h280" src="/static/images/img.png"></image>
  260. <view v-show="true" class="title">二等奖</view>
  261. <view v-show="true" class="txt">新用户免费领礼品弹窗</view>
  262. </view>
  263. </view> -->
  264. <view class="point-group" v-for="(item,index) in lotteryProducts" :key="index">
  265. <!-- <view class="item" v-if="item.length"
  266. :class="{ selected: activePointIndex === 0 }" @click="activePointIndex = 0">
  267. </view> -->
  268. <!-- <view class="item" :class="{ selected: activePointIndex === 1 }"
  269. @click="() => { activePointIndex = 1; scrollToCenterItem() }"></view>
  270. <view class="item" :class="{ selected: activePointIndex === 2 }"
  271. @click="activePointIndex = 2"></view> -->
  272. </view>
  273. <view class="colorf fs28">
  274. 观看直播参与抽奖
  275. </view>
  276. <view class="button" @click="onClaim">参与抽奖</view>
  277. </view>
  278. </view>
  279. </view>
  280. </u-popup>
  281. <u-popup :show="integral?.status" @close="!integral?.status" round='20rpx' mode="center" bgColor='#ffffff'
  282. zIndex='10076'>
  283. <view class="integral-box">
  284. <view class="top">
  285. <view class="title">观看视频领芳华币</view>
  286. <image class="photo" src="/static/images/integral.png" mode="widthFix"></image>
  287. </view>
  288. <view class="item">
  289. <view class="title ">{{integral?.msg}}</view>
  290. <view class="button" @click="integral.status=flase">确认</view>
  291. </view>
  292. </view>
  293. </u-popup>
  294. <u-popup :show="isShowRedCard" @close="!isShowRedCard" round='20rpx' mode="center" bgColor='transparent'
  295. zIndex='10076'>
  296. <view class="red-card">
  297. <image src="/static/images/red_card.png"></image>
  298. <view class="red-content">
  299. <view class="title">{{redCard?.msg}}</view>
  300. <view class="txt ">直播惊喜芳华币</view>
  301. <view class="button" @click="isShowRedCard=flase">确认</view>
  302. </view>
  303. </view>
  304. </u-popup>
  305. <!-- :show="isShowPrize" -->
  306. <u-popup :show="isShowPrize" @close="!isShowPrize" round='20rpx' mode="center" bgColor='#fff'
  307. zIndex='10076'>
  308. <view class="prize-card">
  309. <view class="title">{{prizeInfo.length}}位幸运观众</view>
  310. <view class="prize-content" v-for="(item,index) in prizeInfo" :key="index">
  311. <view>{{item.userName}}</view>
  312. <view class="txt">{{item.prizeLevel}}等奖</view>
  313. </view>
  314. <view class="tip">请联系管理员领取</view>
  315. <view class="button" @click="isShowPrize=flase">确认</view>
  316. </view>
  317. </u-popup>
  318. <!-- 观众列表弹窗 -->
  319. <u-popup :show="showadd" @close="close" @open="openViews" round='20rpx' bgColor='#ffffff' zIndex='10077'>
  320. <view class="view-box">
  321. <view class="t-c fs32">在线观众</view>
  322. <scroll-view scroll-y class="scroll-content" :style="{height: scrollHeight + 'px'}"
  323. @scrolltolower="handleScrollToLower">
  324. <view class="fs28 u-flex-y-center mb20 mt20" v-for="(item,index) in liveViewers" :key="index">
  325. <view
  326. :style="{color: index === 0 ? '#FF3B30' : index === 1 ? '#FF9500' : index === 2 ? '#FFCC00' : '#8E8E93',fontWeight: index < 3 ? 'bold' : 'normal',width: '50rpx'}"
  327. class="mr10">{{index+1}}</view>
  328. <u-avatar :src="item.avatar||$img.logo" size="36"></u-avatar>
  329. <text class="ml16 f30">{{item.nickName||"未命名"}}</text>
  330. </view>
  331. <!-- <view class="no-more" v-if="viewNoMoreData && liveViewers.length > 0">
  332. <text>没有更多了</text>
  333. </view> -->
  334. </scroll-view>
  335. </view>
  336. </u-popup>
  337. <!-- 商品弹窗 -->
  338. <u-popup :show="shopping" @close="closeShop" round='20rpx' bgColor='f3f5f9' zIndex='10075'>
  339. <view class="shoppop">
  340. <view class="shoppop-top">
  341. <!-- <u-avatar :src="store.logoUrl" size="36" class="ml16"></u-avatar> -->
  342. <view class="search-input u-flex-y-center">
  343. <image class="w24 mr16" src="/static/images/search.png" mode="widthFix">
  344. </image>
  345. <input placeholder="请搜索商品" v-model="inputInfo" @input="handleSearchInput" />
  346. </view>
  347. <view class="t-c search-top" @click="goOrderList(liveItem)">
  348. <image class="w48 h48" src="/static/images/order.png"></image>
  349. <view>订单</view>
  350. </view>
  351. </view>
  352. <scroll-view enable-flex scroll-y class="shop-list" :style="{ height: boxHeight + 'px' }">
  353. <view class="list-item" v-for="(item,index) in products" :key="index">
  354. <view class="goods-img">
  355. <image :src="item.imgUrl" mode="widthFix"></image>
  356. <view class="goods-label">{{index+1}}</view>
  357. </view>
  358. <view class="goods-right">
  359. <view class="goods-title">{{item.productName}}</view>
  360. <view class="goods-people">{{item.sales}} 人已购</view>
  361. <view class="goods-shop">
  362. <text class="nummber"><text style="font-size: 20rpx;font-weight: 600;">¥</text><text
  363. style="font-size: 36rpx;font-weight: bold;">{{Math.trunc(item.price)}}</text>.{{getPureDecimal(item.price)?getPureDecimal(item.price):'00'}}/日</text>
  364. <view class="btn-group u-flex-y-center">
  365. <view class="collect-btn">
  366. <image v-if="item.isFavorite" @click="onGoodsCollect(item)" class="w36 h36"
  367. src="/static/images/collect_select.png">
  368. </image>
  369. <image v-else @click="onGoodsCollect(item)" class="w36 h36"
  370. src="/static/images/collect.png">
  371. </image>
  372. </view>
  373. <view v-if="item.status==1" class="shop-btn"
  374. @click="goShop(item.productId,item.goodsId)">去购买 </view>
  375. <view v-else-if="item.status==0" class="shop-btn">
  376. 已下架</view>
  377. </view>
  378. </view>
  379. </view>
  380. </view>
  381. </scroll-view>
  382. </view>
  383. </u-popup>
  384. </view>
  385. </view>
  386. </template>
  387. <script>
  388. import ThreeItemSwiper from '@/components/ThreeItemSwiper.vue'
  389. import Hls from 'hls.js';
  390. import CryptoJS from 'crypto-js'
  391. import {
  392. liveLottery, // 抽奖查询
  393. claim, //抽奖
  394. liveRed, // 点击领红包
  395. liveDataLike, // 点赞
  396. collectStore, // 店铺收藏/取消收藏
  397. collectGoods, // 商品收藏/取消收藏
  398. // store, // 查询店铺
  399. // follow, // 关注/取消关注
  400. watchUserList, //获取直播间用户(展示在线用户)
  401. liveMsg, //获取最近聊天记录
  402. // 小黄车
  403. liveStore, //店铺展示,
  404. liveOrderUser, //正在购买
  405. getLiveInfo, //获取直播间信息接口
  406. getLiveViewData, //直播间点赞、关注、在线人数数据
  407. currentActivities //红包 卡片 抽奖
  408. } from '@/api/live'
  409. import {
  410. liveOrderList, // 订单列表
  411. } from '@/api/order'
  412. import parse from '/uni_modules/uview-plus/libs/config/props/parse';
  413. import {
  414. LiveWS
  415. } from '@/utils/liveWS.js'
  416. import {
  417. getlive,
  418. } from '@/api/home'
  419. var wsUrl = "wss://live.test.ylrztop.com/ws/live-api/app/webSocket"; //余红奇
  420. // var wsUrl = "ws://192.168.10.166:7114/app/webSocket"; //余红奇
  421. var pingpangTimes = null;
  422. var initTimes = null;
  423. var isSocketOpen = false;
  424. var socket = null;
  425. export default {
  426. components: {
  427. ThreeItemSwiper
  428. },
  429. data() {
  430. return {
  431. videoCurrentTime: 0, // 当前视频播放时间
  432. isVideoPlaying: false, // 视频是否正在播放
  433. videoProgressKey: '', // 存储进度的key
  434. inAndOut: {},
  435. isShowPrize: false,
  436. prizeInfo: [],
  437. timeData: {},
  438. lotteryTime: 0,
  439. lotteryProducts: [],
  440. lotteryList: [],
  441. talklist: {},
  442. isShowLotteryPop: false,
  443. liveItem: {},
  444. isSending: false,
  445. welcomeTimer: null,
  446. redTimer: null,
  447. lotteryTimer: null,
  448. isShowLottery: false,
  449. isShowRedCard: false,
  450. redCard: null, //点击红包出现弹窗
  451. integral: {},
  452. lotteryInfo: {},
  453. goodsCard: {},
  454. redInfo: {},
  455. storeId: null,
  456. isFocus: false,
  457. shopping: false,
  458. inputInfo: '',
  459. showWelcomeMessage: false,
  460. scrollIntoView: '',
  461. isShowGoods: false,
  462. isShowRed: false,
  463. lastClickTime: 0,
  464. clickDelay: 300, // 300ms内只响应一次点击
  465. videoRetryCounts: {}, // 记录每个直播间的视频重试次数,格式: { liveId: 次数 }
  466. placeholderText: '说点什么...',
  467. liveUserTotal: null,
  468. viewPageSize: 10, // 每页数量
  469. viewPageNum: 1, // 当前页码
  470. viewLoading: false, // 是否正在加载
  471. viewNoMoreData: false, // 是否没有更多数据
  472. scrollHeight: 0,
  473. scrollTimer: null, // 滚动防抖定时器
  474. socketInstance: null, // 统一管理 socket 实例
  475. reconnectCount: 0,
  476. maxReconnectAttempts: 3,
  477. isManualClose: false, // 标记是否手动关闭
  478. showRed: true,
  479. searchTimer: null,
  480. reconnectTimer: null,
  481. isCollect: false,
  482. showPurchasePrompt: false,
  483. purchasePromptTimer: null,
  484. prevOrderCount: 0, // 用于记录上一次的购买人数
  485. videoUrl: null,
  486. showType: 1, //横屏1 竖屏2
  487. boxHeight: 300, //小黄车高度
  488. liveViewers: [], //观众
  489. likeName: 0,
  490. hlsPlayer: null, // HLS播放器实例,
  491. livingUrl: "",
  492. products: {},
  493. store: {},
  494. orderUser: {}, //正在购买
  495. userType: 0,
  496. timestamp: '',
  497. liveId: null,
  498. userinfo: '', //用户信息
  499. // path: 'http://192.168.10.166/dev-api', //余红奇
  500. // path: 'http://v56c9b8e.natappfree.cc', //余红奇
  501. // path: 'live.test.ylrztop.com/prod-api', //余红奇
  502. value: '',
  503. talkdisabled: false, //输入框是否禁用
  504. autoplay: false, //视频自动播放
  505. showadd: false,
  506. videoContext: '',
  507. livedata: {}, //直播间点赞、关注、在线人数数据
  508. };
  509. },
  510. onLoad(options) {
  511. this.initTime();
  512. console.log(options)
  513. if (options.liveId) {
  514. this.liveId = options.liveId; // 仅当 liveId 变化时更新
  515. }
  516. this.userinfo = JSON.parse(uni.getStorageSync("userInfo"))
  517. if (this.liveId == null) {
  518. uni.showToast({
  519. title: "未知错误,请联系管理员!",
  520. icon: 'none'
  521. });
  522. return;
  523. }
  524. console.log(this.liveId)
  525. // 初始化直播间列表
  526. this.getliving(this.liveId);
  527. this.initSocket();
  528. // this.getliveUser(false); // 调用获取在线用户接口
  529. const platform = uni.getWindowInfo().platform;
  530. // 初始化直播间列表
  531. if (['mp-weixin', 'mp-alipay', 'mp-baidu', 'mp-toutiao'].includes(platform)) {
  532. // 确保 API 存在再调用
  533. if (uni.showShareMenu) {
  534. uni.showShareMenu({
  535. withShareTicket: true, // 可选参数,根据需求配置
  536. success: () => {
  537. console.log('分享菜单显示成功');
  538. },
  539. fail: (err) => {
  540. console.error('分享菜单显示失败:', err);
  541. }
  542. });
  543. } else {
  544. console.warn('当前平台不支持 uni.showShareMenu');
  545. }
  546. }
  547. },
  548. onReady() {},
  549. onShareAppMessage() {
  550. return {
  551. title: this.livedata.liveName,
  552. path: '/pages/home/living',
  553. imageUrl: this.products.image,
  554. success(res) {
  555. console.log("分享成功", res);
  556. },
  557. fail(err) {
  558. console.error("分享失败", err);
  559. }
  560. };
  561. },
  562. computed: {
  563. filteredViewers() {
  564. // 先判断是否为数组,再判断长度,避免 null/undefined 报错
  565. if (!Array.isArray(this.liveViewers) || this.liveViewers.length === 0) {
  566. return [];
  567. }
  568. return this.liveViewers.slice(0, 3);
  569. }
  570. // payLimitTime() {
  571. // return this.order?.updateTime ?
  572. // dayjs(this.order.updateTime).add(2, 'day').format('YYYY-MM-DD HH:mm:ss') :
  573. // '';
  574. // }
  575. },
  576. onHide() {
  577. // 页面隐藏时保存视频进度
  578. this.saveVideoProgress();
  579. const currentLive = this.liveItem;
  580. if (currentLive) {
  581. this.pauseVideo(currentLive);
  582. this.clearTimeTimer(currentLive); // 隐藏时清除当前直播间定时器
  583. } // 隐藏时关闭所有连接
  584. this.closeAllWebSockets();
  585. if (this.lotteryList) {
  586. this.clearTimeTimer(this.lotteryList)
  587. }
  588. },
  589. mounted() {
  590. this.getCurrentActivities();
  591. this.getliveOrder()
  592. },
  593. onUnload() {
  594. // 页面卸载时保存视频进度
  595. this.saveVideoProgress();
  596. // 清除定时器
  597. if (this.redTimer) {
  598. clearInterval(this.redTimer);
  599. this.redTimer = null;
  600. }
  601. if (this.lotteryTimer) {
  602. clearInterval(this.lotteryTimer);
  603. this.lotteryTimer = null;
  604. }
  605. if (this.welcomeTimer) {
  606. clearInterval(this.welcomeTimer);
  607. this.welcomeTimer = null;
  608. }
  609. // 关闭所有WebSocket连接
  610. // this.closeAllWebSockets();
  611. this.closeWebSocket();
  612. // this.socketInstances = {}; // 强制清空实例对象
  613. // 移除所有全局事件监听
  614. this.removeAllEventListeners();
  615. // 清理定时器
  616. this.clearAllTimers();
  617. // 销毁HLS播放器(如果使用了hls.js)
  618. if (this.hlsPlayer) {
  619. this.hlsPlayer.destroy();
  620. this.hlsPlayer = null;
  621. }
  622. // 移除所有全局事件监听
  623. this.removeAllEventListeners();
  624. // 清理定时器
  625. this.clearAllTimers();
  626. // 销毁HLS播放器(如果使用了hls.js)
  627. if (this.hlsPlayer) {
  628. this.hlsPlayer.destroy();
  629. this.hlsPlayer = null;
  630. }
  631. // 暂停视频
  632. const videoId = `myVideo_${this.liveId}`;
  633. const videoContext = uni.createVideoContext(videoId, this);
  634. if (videoContext) {
  635. videoContext.pause();
  636. }
  637. },
  638. watch: {
  639. // 监听orderUser.count的变化
  640. 'orderUser.count': {
  641. handler(newVal, oldVal) {
  642. if (newVal !== this.prevOrderCount) {
  643. this.prevOrderCount = newVal;
  644. this.showPurchaseMessage();
  645. }
  646. },
  647. immediate: true
  648. }
  649. },
  650. methods: {
  651. // 录播时间点
  652. onVideoMetaLoaded(e) {
  653. // 设置存储key
  654. this.videoProgressKey = `videoProgress_${this.liveId}`;
  655. // 尝试从存储中获取保存的播放进度
  656. uni.getStorage({
  657. key: this.videoProgressKey,
  658. success: (res) => {
  659. this.videoCurrentTime = res.data;
  660. // 设置视频播放位置
  661. this.setVideoCurrentTime(this.videoCurrentTime);
  662. },
  663. fail: (err) => {
  664. console.log('没有找到保存的播放进度或获取失败', err);
  665. this.videoCurrentTime = 0;
  666. }
  667. });
  668. },
  669. // 视频数据加载完成
  670. onVideoLoaded(e) {
  671. console.log('视频数据加载完成');
  672. },
  673. // 视频时间更新
  674. onVideoTimeUpdate(e) {
  675. // 获取当前播放时间
  676. this.videoCurrentTime = e.detail.currentTime;
  677. // 每隔5秒保存一次进度(避免频繁存储)
  678. if (Math.floor(this.videoCurrentTime) % 5 === 0) {
  679. this.saveVideoProgress();
  680. }
  681. },
  682. // 视频暂停
  683. onVideoPause(e) {
  684. this.isVideoPlaying = false;
  685. // 暂停时保存进度
  686. this.saveVideoProgress();
  687. },
  688. // 视频播放
  689. onVideoPlay(e) {
  690. this.isVideoPlaying = true;
  691. },
  692. // 设置视频当前时间
  693. setVideoCurrentTime(time) {
  694. const videoId = `myVideo_${this.liveId}`;
  695. const videoContext = uni.createVideoContext(videoId, this);
  696. if (videoContext) {
  697. videoContext.seek(time);
  698. // console.log(`设置视频播放位置: ${time}秒`);
  699. }
  700. },
  701. // 保存视频进度
  702. saveVideoProgress() {
  703. if (this.videoProgressKey) {
  704. uni.setStorage({
  705. key: this.videoProgressKey,
  706. data: this.videoCurrentTime,
  707. success: () => {},
  708. fail: (err) => {
  709. console.error('保存视频进度失败:', err);
  710. }
  711. });
  712. }
  713. },
  714. onChange(e) {
  715. this.timeData = e
  716. },
  717. // 参与抽奖
  718. onClaim() {
  719. let data = {
  720. liveId: this.liveId,
  721. // integral: this.integral,
  722. lotteryId: this.lotteryInfo.lotteryId,
  723. // userId: this.userinfo.userId
  724. }
  725. claim(data).then(res => {
  726. if (res.code == 200) {
  727. uni.showToast({
  728. title: res.msg,
  729. icon: 'none'
  730. });
  731. this.isShowLotteryPop = false
  732. } else {
  733. uni.showToast({
  734. title: res.msg,
  735. icon: 'none'
  736. });
  737. }
  738. },
  739. rej => {}
  740. );
  741. },
  742. // 点击抽奖图标
  743. onLottery() {
  744. if (this.lotteryTimer) {
  745. clearInterval(this.lotteryTimer);
  746. this.lotteryTimer = null;
  747. }
  748. if (!this.lotteryInfo) return;
  749. let data = {
  750. lotteryId: this.lotteryInfo.lotteryId
  751. }
  752. // 抽奖查询
  753. liveLottery(data).then(res => {
  754. // this.isShowLottery = false
  755. this.isShowLotteryPop = true
  756. if (res.code == 200) {
  757. this.lotteryList = res.data
  758. this.lotteryProducts = res.data.products
  759. // this.startTimeTimer(this.lotteryList, "lottery");
  760. } else {
  761. uni.showToast({
  762. title: res.msg,
  763. icon: 'none'
  764. });
  765. }
  766. },
  767. rej => {}
  768. );
  769. },
  770. // 商品收藏
  771. onGoodsCollect(item) {
  772. console.log("商品收藏", item)
  773. if (!item || item.length === 0 || !item.goodsId) {
  774. return;
  775. }
  776. collectGoods(item.goodsId).then(res => {
  777. if (res.code == 200) {
  778. uni.showToast({
  779. title: res.msg,
  780. icon: 'none'
  781. });
  782. item.isFavorite = !item.isFavorite
  783. } else {
  784. uni.showToast({
  785. title: res.msg,
  786. icon: 'none'
  787. });
  788. }
  789. },
  790. rej => {}
  791. );
  792. },
  793. inputFocus() {
  794. this.isFocus = true
  795. },
  796. inputBlur() {
  797. this.isFocus = false
  798. },
  799. //正在购买
  800. getliveOrder(item) {
  801. if (!this.liveId) {
  802. return;
  803. }
  804. liveOrderUser(this.liveId).then(res => {
  805. console.log("正在购买", res)
  806. if (res.code == 200) {
  807. this.orderUser = res; // 同步全局变量
  808. } else {
  809. console.error('获取正在购买用户失败:', error);
  810. }
  811. },
  812. rej => {}
  813. );
  814. },
  815. onLiveStateChange(e, liveItem) {
  816. // console.log('直播状态变化:', e.detail.code, e.detail);
  817. // 可以根据状态码处理不同的直播状态
  818. const stateCode = e.detail.code;
  819. // 2001: 已经连接服务器
  820. // 2002: 已经连接服务器,开始拉流
  821. // 2003: 网络接收到首个视频数据包(IDR)
  822. // 2004: 视频播放开始
  823. // 2005: 视频播放进度
  824. // 2006: 视频播放结束
  825. // 2007: 视频播放Loading
  826. // 2008: 解码器启动
  827. // -2301: 网络断连,且经多次重连抢救无效,更多重试请自行重启播放
  828. // -2302: 获取加速拉流地址失败
  829. }, // 直播错误事件
  830. onLiveError(e, liveItem) {
  831. this.videoError(e, liveItem);
  832. },
  833. // 红包 卡片 抽奖
  834. getCurrentActivities() {
  835. if (!this.liveId) return;
  836. currentActivities(this.liveId).then(res => {
  837. if (res.code == 200) {
  838. // 商品卡片
  839. // 红包
  840. if (res.red) {
  841. this.redInfo = res.red[0];
  842. }
  843. if (res.lottery) {
  844. this.lotteryInfo = res.lottery[0];
  845. }
  846. this.goodsCard = res.goods;
  847. // 抽奖
  848. this.lotteryTime = this.lotteryInfo?.duration * 60 * 1000
  849. if (this.goodsCard && this.goodsCard.status == 1) {
  850. this.isShowGoods = true
  851. } else {
  852. this.isShowGoods = false
  853. }
  854. if (this.redInfo && this.redInfo.redStatus == 1) {
  855. this.isShowRed = true
  856. } else {
  857. this.isShowRed = false
  858. }
  859. if (this.lotteryInfo && this.lotteryInfo.lotteryStatus == 1) {
  860. this.isShowLottery = true
  861. } else {
  862. this.isShowLottery = false
  863. }
  864. } else {
  865. uni.showToast({
  866. title: res.msg,
  867. icon: 'none'
  868. });
  869. }
  870. },
  871. rej => {}
  872. );
  873. },
  874. // 计算当前时间与 liveItem.startTime 的差值,并更新 totalTime
  875. calculateTimeDiff(item, type = "") {
  876. // 防御1:item 不存在直接返回
  877. if (!item) return;
  878. // 步骤1:提取对应场景的时间字符串(直播用startTime,抽奖用updateTime)
  879. let timeStr = "";
  880. if (type === "lottery") {
  881. timeStr = item || ""; // 抽奖场景:从item取updateTime
  882. } else {
  883. timeStr = item || ""; // 直播场景:从item取startTime
  884. }
  885. // 防御2:时间字符串为空,兜底显示
  886. if (!timeStr) {
  887. if (type === "lottery") {
  888. // this.lotteryTime = {
  889. // hours: "00",
  890. // minutes: "00",
  891. // seconds: "00"
  892. // };
  893. } else {
  894. this.$set(this.liveItem, "totalTime", "00:00:00");
  895. }
  896. return;
  897. }
  898. // 步骤2:转换时间格式(适配iOS,将 "-" 替换为 "/")
  899. const iosCompatibleTime = timeStr.replace(/-/g, "/");
  900. const time = new Date(iosCompatibleTime);
  901. // 防御3:时间解析失败(Invalid Date),兜底显示
  902. if (isNaN(time.getTime())) {
  903. if (type === "lottery") {
  904. this.lotteryTime = {
  905. hours: "00",
  906. minutes: "00",
  907. seconds: "00"
  908. };
  909. } else {
  910. this.$set(this.liveItem, "totalTime", "00:00:00");
  911. }
  912. console.warn("时间解析失败,原始格式:", timeStr);
  913. return;
  914. }
  915. // 步骤3:计算时间差(抽奖是「结束时间-当前时间」,直播是「当前时间-开始时间」)
  916. const now = new Date();
  917. let diffMs = 0;
  918. if (type === "lottery") {
  919. diffMs = Math.max(0, time - now); // 抽奖:倒计时(结束时间-现在)
  920. } else {
  921. diffMs = Math.max(0, now - time); // 直播:已播放时长(现在-开始时间)
  922. }
  923. // 步骤4:转换为 时:分:秒(补零处理)
  924. const totalSeconds = Math.floor(diffMs / 1000);
  925. const hours = this.padZero(Math.floor(totalSeconds / 3600));
  926. const minutes = this.padZero(Math.floor((totalSeconds % 3600) / 60));
  927. const seconds = this.padZero(totalSeconds % 60);
  928. // 步骤5:更新对应数据(响应式)
  929. if (type === "lottery") {
  930. // this.lotteryTime = {
  931. // hours,
  932. // minutes,
  933. // seconds
  934. // };
  935. } else {
  936. this.$set(this.liveItem, "totalTime", `${hours}:${minutes}:${seconds}`);
  937. }
  938. },
  939. padZero(num) {
  940. return num < 10 ? `0${num}` : num;
  941. },
  942. // 启动当前直播间的时间差值定时器
  943. startTimeTimer(item, type = '') {
  944. // 防御:若 item 不存在,直接返回
  945. if (!item) return;
  946. // 立即计算一次(避免等待1秒才显示)
  947. if (type === "lottery") {
  948. this.calculateTimeDiff(item.updateTime, "lottery");
  949. item.timeTimer = setInterval(() => {
  950. this.calculateTimeDiff(item.updateTime, "lottery");
  951. }, 1000);
  952. } else {
  953. this.calculateTimeDiff(item.startTime);
  954. item.timeTimer = setInterval(() => {
  955. this.calculateTimeDiff(item.startTime);
  956. }, 1000);
  957. }
  958. },
  959. // 清除指定直播间的时间定时器
  960. clearTimeTimer(item) {
  961. if (item.timeTimer) {
  962. clearInterval(item.timeTimer);
  963. item.timeTimer = null; // 清空定时器标识
  964. }
  965. },
  966. //抽奖时间
  967. // convertMinutesToHoursMinutes(totalMinutes) {
  968. // const hours = Math.floor(totalMinutes / 60);
  969. // const minutes = totalMinutes % 60;
  970. // const seconds = totalMinutes % 3600;
  971. // this.lotteryTime = {
  972. // hours,
  973. // minutes,
  974. // seconds
  975. // };
  976. // },
  977. toggleViewerList() {
  978. const now = Date.now()
  979. if (now - this.lastClickTime > this.clickDelay) {
  980. this.showadd = !this.showadd
  981. this.lastClickTime = now
  982. }
  983. },
  984. closeAllWebSockets() {
  985. this.isManualClose = true;
  986. // this.socketInstances = {};
  987. isSocketOpen = false;
  988. }, // 移除所有全局事件监听器
  989. removeAllEventListeners() {
  990. uni.$off('initSocket');
  991. uni.$off('sendMsg');
  992. uni.$off('closeWebSocket');
  993. uni.$off('refreshOrder');
  994. // 可以根据实际情况添加其他需要移除的事件
  995. }, // 清理所有定时器
  996. clearAllTimers() {
  997. if (this.intervalId) {
  998. clearInterval(this.intervalId);
  999. this.intervalId = null;
  1000. }
  1001. if (this.pingpangTimes) {
  1002. clearInterval(this.pingpangTimes);
  1003. this.pingpangTimes = null;
  1004. }
  1005. if (this.reconnectTimer) {
  1006. clearInterval(this.reconnectTimer);
  1007. this.reconnectTimer = null;
  1008. }
  1009. if (this.scrollTimer) {
  1010. clearTimeout(this.scrollTimer);
  1011. this.scrollTimer = null;
  1012. }
  1013. if (this.searchTimer) {
  1014. clearTimeout(this.searchTimer);
  1015. this.searchTimer = null;
  1016. }
  1017. if (this.purchasePromptTimer) {
  1018. clearTimeout(this.purchasePromptTimer);
  1019. this.purchasePromptTimer = null;
  1020. }
  1021. },
  1022. // 播放视频
  1023. playVideo(liveItem) {
  1024. if (!liveItem) return;
  1025. try {
  1026. // 直播流使用live-player
  1027. if (liveItem.liveType === 1 && liveItem.livingUrl) {
  1028. const livePlayerId = `myLivePlayer_${this.liveId}`;
  1029. const livePlayerContext = uni.createLivePlayerContext(livePlayerId, this);
  1030. if (livePlayerContext) {
  1031. livePlayerContext.play();
  1032. }
  1033. }
  1034. // 回放视频使用video
  1035. else if (liveItem.liveType === 2 && liveItem.videoUrl) {
  1036. const videoId = `myVideo_${this.liveId}`;
  1037. const videoContext = uni.createVideoContext(videoId, this);
  1038. if (videoContext) {
  1039. videoContext.pause(() => {
  1040. videoContext.play();
  1041. });
  1042. }
  1043. } else if (liveItem.liveType === 3 && liveItem.videoUrl) {
  1044. const videoId = `myVideo_${this.liveId}`;
  1045. const videoContext = uni.createVideoContext(videoId, this);
  1046. if (videoContext) {
  1047. videoContext.pause(() => {
  1048. videoContext.play();
  1049. });
  1050. }
  1051. }
  1052. } catch (error) {
  1053. console.error("播放视频失败:", error);
  1054. }
  1055. },
  1056. pauseVideo(liveItem) {
  1057. if (!liveItem) return;
  1058. try {
  1059. // 直播流使用live-player
  1060. if (liveItem.liveType === 1) {
  1061. const livePlayerId = `myLivePlayer_${this.liveId}`;
  1062. const livePlayerContext = uni.createLivePlayerContext(livePlayerId, this);
  1063. if (livePlayerContext) {
  1064. livePlayerContext.pause();
  1065. }
  1066. }
  1067. // 回放视频使用video
  1068. else if (liveItem.liveType === 2) {
  1069. const videoId = `myVideo_${this.liveId}`;
  1070. const videoContext = uni.createVideoContext(videoId, this);
  1071. if (videoContext) {
  1072. videoContext.pause();
  1073. }
  1074. }
  1075. } catch (error) {
  1076. console.error("暂停视频失败:", error);
  1077. }
  1078. },
  1079. openViews() {
  1080. // 计算scroll-view高度
  1081. this.$nextTick(() => {
  1082. const query = uni.createSelectorQuery().in(this);
  1083. query.select('.view-box').boundingClientRect(data => {
  1084. if (data) {
  1085. // 减去标题和底部固定区域的高度
  1086. this.scrollHeight = data.height - 80 - 120; // 80是标题高度,120是底部高度
  1087. }
  1088. }).exec();
  1089. });
  1090. },
  1091. // 获取直播间用户
  1092. async getliveUser(isLoadMore = false) {
  1093. this.viewLoading = true;
  1094. try {
  1095. const res = await watchUserList(this.liveId, this.viewPageSize, this.viewPageNum, false);
  1096. if (res.code === 200) {
  1097. this.liveUserTotal = res.total || 0;
  1098. // 强制将 rows 转为数组(防止接口返回 null/undefined)
  1099. const newRows = Array.isArray(res.rows) ? res.rows : [];
  1100. // 加载更多时合并数组,首次加载直接赋值(确保始终是数组)
  1101. this.liveViewers = isLoadMore ? [...this.liveViewers, ...newRows] : newRows;
  1102. this.viewPageNum++;
  1103. }
  1104. } catch (error) {
  1105. console.error('获取观众列表失败:', error);
  1106. // 异常时重置为数组,避免后续报错
  1107. this.liveViewers = [];
  1108. } finally {
  1109. this.viewLoading = false;
  1110. }
  1111. },
  1112. // 滚动到底部触发
  1113. handleScrollToLower() {
  1114. // 清除上一次未执行的定时器,避免重复请求
  1115. if (this.scrollTimer) {
  1116. clearTimeout(this.scrollTimer);
  1117. }
  1118. // 延迟0.5秒(500毫秒)执行接口请求
  1119. this.scrollTimer = setTimeout(() => {
  1120. // this.getliveUser(true); // 加载更多数据
  1121. }, 1000); // 延迟时间:500毫秒
  1122. },
  1123. async getLiveMsg(liveItem) {
  1124. // 强化校验:确保 liveItem 存在且包含 liveId
  1125. if (!liveItem || !this.liveId) {
  1126. console.error('getLiveMsg 错误:无效的 liveItem', {
  1127. liveItem: liveItem,
  1128. currentIndex: this.currentSwiperIndex,
  1129. });
  1130. return; // 直接返回,不执行后续逻辑
  1131. }
  1132. try {
  1133. const res = await liveMsg(this.liveId, 30, 1);
  1134. if (res.code == 200) {
  1135. const rows = Array.isArray(res.rows) ? res.rows : [];
  1136. const reversedTalkList = [...rows].reverse();
  1137. this.talklist = reversedTalkList;
  1138. this.$nextTick(() => {
  1139. // 滚动到最新消息(反转后最新消息在数组最后一位,索引为 reversedTalkList.length - 1)
  1140. this.scrollIntoView = `list_${reversedTalkList.length - 1}`;
  1141. });
  1142. }
  1143. } catch (error) {
  1144. console.error("获取聊天记录失败:", error);
  1145. }
  1146. },
  1147. // 点击红包
  1148. onRed() {
  1149. if (!this.liveId) return;
  1150. if (!this.redInfo?.redId) return;
  1151. if (this.redTimer) {
  1152. clearInterval(this.redTimer);
  1153. this.redTimer = null;
  1154. }
  1155. let data = {
  1156. liveId: this.liveId,
  1157. userId: this.userinfo.userId,
  1158. redId: this.redInfo.redId,
  1159. }
  1160. liveRed(data).then(res => {
  1161. this.isShowRed = false
  1162. this.redCard = res
  1163. this.isShowRedCard = true
  1164. },
  1165. rej => {}
  1166. );
  1167. },
  1168. handleSearchInput() {
  1169. // 使用防抖优化性能,避免频繁请求
  1170. clearTimeout(this.searchTimer);
  1171. this.searchTimer = setTimeout(() => {
  1172. this.queryCollect();
  1173. }, 500); // 500毫秒延迟
  1174. },
  1175. // 显示购买提示信息
  1176. showPurchaseMessage() {
  1177. // 清除之前的定时器
  1178. if (this.purchasePromptTimer) {
  1179. clearTimeout(this.purchasePromptTimer);
  1180. }
  1181. // 显示提示
  1182. this.showPurchasePrompt = true;
  1183. // 2秒后自动隐藏
  1184. this.purchasePromptTimer = setTimeout(() => {
  1185. this.showPurchasePrompt = false;
  1186. }, 2000);
  1187. },
  1188. truncateString(str, maxLength) {
  1189. return str.length > maxLength ? str.slice(0, maxLength) + '...' : str;
  1190. },
  1191. // 去订单列表
  1192. goOrderList() {
  1193. uni.navigateTo({
  1194. url: "/pages_shop/order"
  1195. })
  1196. },
  1197. // 初始化HLS播放器
  1198. initHlsPlayer() {
  1199. if (Hls.isSupported() && this.livingUrl) {
  1200. const video = document.getElementById('myVideo');
  1201. if (video) {
  1202. this.hlsPlayer = new Hls();
  1203. this.hlsPlayer.loadSource(this.livingUrl);
  1204. this.hlsPlayer.attachMedia(video);
  1205. this.hlsPlayer.on(Hls.Events.MANIFEST_PARSED, () => {
  1206. video.play();
  1207. });
  1208. }
  1209. }
  1210. },
  1211. // 视频错误处理
  1212. videoError(e, liveItem) {
  1213. if (!liveItem || !this.liveId) return;
  1214. // 初始化重试计数
  1215. if (this.videoRetryCounts[liveItem.liveId] === undefined) {
  1216. this.videoRetryCounts[liveItem.liveId] = 0;
  1217. }
  1218. // 限制重试次数
  1219. if (this.videoRetryCounts[liveItem.liveId] >= 3) {
  1220. console.error(`直播间 ${this.liveId} 视频加载失败,停止重试`);
  1221. // 显示错误提示
  1222. uni.showToast({
  1223. title: "视频加载失败,请检查网络",
  1224. icon: 'none',
  1225. duration: 2000
  1226. });
  1227. return;
  1228. }
  1229. this.videoRetryCounts[this.liveId]++;
  1230. // 延迟重试
  1231. setTimeout(() => {
  1232. if (this.liveId === this.liveId) {
  1233. console.log(`第${this.videoRetryCounts[this.liveId]}次重试播放视频`);
  1234. this.playVideo(liveItem);
  1235. }
  1236. }, 2000);
  1237. },
  1238. // 修改获取直播信息方法
  1239. async getliving(liveId) {
  1240. const param = {
  1241. id: liveId
  1242. };
  1243. try {
  1244. const res = await getlive(param);
  1245. if (res.code !== 200) {
  1246. uni.showToast({
  1247. title: res.msg,
  1248. icon: 'none'
  1249. });
  1250. return;
  1251. }
  1252. this.liveItem = res.data;
  1253. this.talklist = res.data.talklist;
  1254. // 强制更新视频源(覆盖旧值)
  1255. if (res.data.liveType === 2) {
  1256. // 回放视频
  1257. this.$set(this.liveItem, 'videoUrl', res.data.videoUrl);
  1258. this.$set(this.liveItem, 'livingUrl', ''); // 清空直播流
  1259. } else if (res.data.liveType === 1) {
  1260. // 直播流
  1261. this.$set(this.liveItem, 'livingUrl', res.data.flvHlsUrl);
  1262. this.$set(this.liveItem, 'videoUrl', ''); // 清空回放视频
  1263. } else {
  1264. // 未开播
  1265. this.$set(this.liveItem, 'livingUrl', '');
  1266. this.$set(this.liveItem, 'videoUrl', '');
  1267. }
  1268. this.startTimeTimer(this.liveItem);
  1269. await this.getLiveMsg(this.liveItem);
  1270. await this.getliveViewData(this.liveItem);
  1271. this.$set(this.liveItem, 'autoplay', res.data.liveType !== 0);
  1272. this.$set(this.liveItem, 'showType', res.data.showType);
  1273. this.storeId = res.storeId;
  1274. } catch (err) {
  1275. console.error("获取直播信息失败:", err);
  1276. uni.showToast({
  1277. title: "获取直播信息失败",
  1278. icon: 'none'
  1279. });
  1280. }
  1281. }, // 设置视频播放
  1282. maskString(str, maskChar = '*') {
  1283. // 如果str是undefined或null,直接返回空字符串
  1284. if (!str) return '';
  1285. // 确保str是字符串(如果是数字等类型,先转为字符串)
  1286. const strVal = String(str);
  1287. return strVal.split('').map((char, index) => (index === 0 ? char : maskChar)).join(
  1288. '');
  1289. },
  1290. getPureDecimal(num, precision = 6) {
  1291. const decimalPart = Math.abs(num).toFixed(precision).split('.')[1];
  1292. return decimalPart?.replace(/0+$/, '') || ''; // 移除末尾多余的0
  1293. },
  1294. // 返回上一个页面并关闭WebSocket
  1295. goBack() {
  1296. // 暂停当前视频
  1297. const currentLive = this.liveItem;
  1298. if (currentLive) {
  1299. this.pauseVideo(currentLive);
  1300. }
  1301. // 关闭所有WebSocket连接
  1302. this.closeWebSocket();
  1303. // 导航返回
  1304. const pages = getCurrentPages();
  1305. if (pages.length > 1) {
  1306. uni.navigateBack();
  1307. } else {
  1308. uni.reLaunch({
  1309. url: '/pages/list/index'
  1310. });
  1311. }
  1312. },
  1313. // 点赞
  1314. async onLike(liveItem) {
  1315. if (!liveItem || !this.liveId) return;
  1316. try {
  1317. const res = await liveDataLike(this.liveId);
  1318. if (res?.like) {
  1319. liveItem.liveViewData.like++; // 只更新当前直播间的点赞数
  1320. } else {
  1321. uni.showToast({
  1322. title: res.msg,
  1323. icon: 'none'
  1324. });
  1325. }
  1326. } catch (error) {
  1327. console.error('点赞失败:', error);
  1328. }
  1329. },
  1330. //直播间点赞、关注、在线人数数据
  1331. async getliveViewData(liveItem) {
  1332. if (!liveItem || !this.liveId) return;
  1333. try {
  1334. const res = await getLiveViewData(this.liveId);
  1335. if (res.code == 200) {
  1336. // 强制响应式更新,确保数据实时显示
  1337. this.$set(liveItem, 'liveViewData', res);
  1338. }
  1339. } catch (error) {
  1340. console.error("获取直播间数据失败:", error);
  1341. // 失败时兜底,避免显示异常
  1342. this.$set(liveItem, 'liveViewData', {
  1343. like: 0,
  1344. watchCount: 0
  1345. });
  1346. }
  1347. },
  1348. // 去购买,跳商品详情
  1349. goShop(productId, goodsId) {
  1350. // const currentLive = this.liveItem;
  1351. if (!this.liveId) return;
  1352. uni.navigateTo({
  1353. url: '/pages_shop/goods?productId=' + productId + '&liveId=' +
  1354. this
  1355. .liveId +
  1356. '&goodsId=' + goodsId + '&storeId=' + this.storeId
  1357. })
  1358. },
  1359. // 查询店铺
  1360. async queryCollect() {
  1361. if (!this.liveId) return;
  1362. if (this.inputInfo == null) this.inputInfo = ''
  1363. try {
  1364. const res = await liveStore(this.liveId, this.inputInfo);
  1365. if (res.code === 200) {
  1366. // 数据绑定到当前 liveItem,避免全局污染
  1367. this.products = res.data;
  1368. }
  1369. } catch (error) {
  1370. console.error('获取小黄车商品失败:', error);
  1371. }
  1372. },
  1373. // 时间戳
  1374. initTime() {
  1375. const now = new Date();
  1376. this.timestamp = now.getTime(); // 例如:'2023-04-01 12:00:00'
  1377. },
  1378. openCart() {
  1379. this.queryCollect()
  1380. this.shopping = true
  1381. },
  1382. close() {
  1383. this.showadd = false
  1384. },
  1385. // 关闭小黄车
  1386. closeShop() {
  1387. this.shopping = false;
  1388. },
  1389. getEWechatSdk() {
  1390. // 只在H5平台执行
  1391. // #ifdef H5
  1392. let eWechatSdk = '';
  1393. if (/(Android)/i.test(navigator.userAgent)) {
  1394. eWechatSdk = 'jWeixin';
  1395. } else if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
  1396. eWechatSdk = 'wx';
  1397. } else {
  1398. eWechatSdk = 'jWeixin';
  1399. }
  1400. uni.setStorageSync("wxSdk", eWechatSdk);
  1401. // #endif
  1402. },
  1403. // 修改关闭WebSocket方法
  1404. closeWebSocket() {
  1405. this.socket?.close();
  1406. },
  1407. initSocket() {
  1408. if (!this.liveId||this.userinfo?.userId ) return;
  1409. // 设置正在连接的直播间ID
  1410. // this.connectingLiveId = liveId;
  1411. // 生成签名
  1412. const signature = CryptoJS.HmacSHA256(
  1413. `${this.liveId}${this.userinfo.userId}${this.userType}${this.timestamp}`,
  1414. this.timestamp.toString()
  1415. ).toString(CryptoJS.enc.Hex);
  1416. try {
  1417. const socketTask = uni.connectSocket({
  1418. url: `${wsUrl}?userId=${this.userinfo.userId}&liveId=${this.liveId}&userType=${this.userType}&timestamp=${this.timestamp}&signature=${signature}`,
  1419. success: () => {},
  1420. fail: (err) => {}
  1421. });
  1422. socketTask.onOpen((res) => {
  1423. this.socket = socketTask;
  1424. });
  1425. socketTask.onMessage((res) => {
  1426. // 找到对应的直播间并更新数据
  1427. const targetLive = this.liveItem;
  1428. if (targetLive) {
  1429. this.handleSocketMessage(res, targetLive);
  1430. }
  1431. });
  1432. } catch (e) {
  1433. console.error('创建 WebSocket 异常:', e);
  1434. }
  1435. },
  1436. // 处理Socket消息
  1437. handleSocketMessage(message, liveItem) {
  1438. let data = JSON.parse(message.data)
  1439. const socketMessage = data.data // 服务端返回的消息体
  1440. if (data.code == 200) {
  1441. const messageData = {
  1442. ...socketMessage,
  1443. cmd: socketMessage.cmd || '', // 确保cmd字段存在
  1444. ts: Date.now() // 新增时间戳,用于排序(可选)
  1445. };
  1446. // 处理服务端返回的sendMsg消息,加入本地列表
  1447. if (socketMessage.cmd == 'sendMsg') {
  1448. const oldList = this.talklist || [];
  1449. // 创建新数组并添加元素
  1450. const newList = [...oldList, messageData];
  1451. // 1. 将消息追加到当前直播间的talklist中(响应式更新)
  1452. this.talklist = newList;
  1453. // 2. 延迟下一帧滚动到最新消息(确保DOM已更新)
  1454. this.$nextTick(() => {
  1455. const lastIndex = this.talklist.length - 1;
  1456. this.scrollIntoView = `list_${lastIndex}`;
  1457. });
  1458. } else if (socketMessage.cmd == 'red') {
  1459. this.redInfo = JSON.parse(socketMessage.data);
  1460. if (socketMessage.status == 1) {
  1461. this.isShowRed = true
  1462. let time = this.redInfo.duration
  1463. this.redTimer = setInterval(() => {
  1464. this.isShowRed = false
  1465. }, time * 60000)
  1466. } else {
  1467. this.isShowRed = false
  1468. }
  1469. } else if (socketMessage.cmd == 'goods') {
  1470. this.goodsCard = JSON.parse(socketMessage.data);
  1471. if (socketMessage.status == 1) {
  1472. this.isShowGoods = true
  1473. } else {
  1474. this.isShowGoods = false
  1475. }
  1476. } else if (socketMessage.cmd == 'lottery') {
  1477. this.lotteryInfo = JSON.parse(socketMessage.data);
  1478. if (socketMessage.status == 1) {
  1479. this.isShowLottery = true
  1480. let time = this.lotteryInfo.duration
  1481. this.lotteryTimer = setInterval(() => {
  1482. this.isShowLottery = false
  1483. }, time * 60000)
  1484. this.lotteryTime = time * 60 * 1000
  1485. // this.convertMinutesToHoursMinutes(time)
  1486. } else {
  1487. this.isShowLottery = false
  1488. }
  1489. } else if (socketMessage.cmd == 'entry' || socketMessage.cmd ==
  1490. 'out') {
  1491. // const oldList = this.talklist || [];
  1492. // 创建新数组并添加元素
  1493. // 1. 将消息追加到当前直播间的talklist中(响应式更新)
  1494. this.inAndOut = socketMessage;
  1495. this.showWelcomeMessage = false;
  1496. this.showWelcomeMessage = true;
  1497. this.getliveUser(false);
  1498. if (this.welcomeTimer) clearTimeout(this.welcomeTimer);
  1499. this.welcomeTimer = setTimeout(() => {
  1500. this.showWelcomeMessage = false;
  1501. }, 1000);
  1502. } else if (socketMessage.cmd == 'Integral') {
  1503. this.integral = {
  1504. msg: socketMessage.msg,
  1505. status: true
  1506. }
  1507. } else if (socketMessage.cmd == 'LotteryDetail') {
  1508. this.prizeInfo = JSON.parse(socketMessage.data);
  1509. this.isShowPrize = true;
  1510. this.isShowLottery = false
  1511. } else if (socketMessage.cmd == 'blockUser') {
  1512. uni.removeStorage({
  1513. key: 'AppToken',
  1514. success: () => {
  1515. uni.reLaunch({
  1516. url: '/pages/auth/login'
  1517. });
  1518. }
  1519. });
  1520. }
  1521. } else {
  1522. uni.showToast({
  1523. title: data.msg,
  1524. icon: 'none'
  1525. });
  1526. }
  1527. },
  1528. sendMsg() {
  1529. if (!this.liveId) return;
  1530. // 防止连续点击发送两次(短时锁定)
  1531. if (this.isSending) return;
  1532. this.isSending = true;
  1533. setTimeout(() => {
  1534. this.isSending = false;
  1535. }, 800); // 800ms 内禁止再次发送
  1536. const text = (this.value || '').trim();
  1537. if (!text) {
  1538. uni.showToast({
  1539. title: "不能发送空消息",
  1540. icon: 'none'
  1541. });
  1542. return;
  1543. }
  1544. const liveId = this.liveId;
  1545. const socketItem = this.socket;
  1546. this.value = ''; // 立即清空输入框,提升体验
  1547. // 构造发送给服务端的消息数据
  1548. const data = {
  1549. liveId,
  1550. userId: this.userinfo.userId,
  1551. userType: 0,
  1552. cmd: "sendMsg", // 指令标识,服务端会根据此返回sendMsg消息
  1553. msg: text,
  1554. nickName: this.userinfo.nickName,
  1555. avatar: this.userinfo.avatar
  1556. };
  1557. // 发送socket消息
  1558. try {
  1559. this.socket.send({
  1560. data: JSON.stringify(data),
  1561. success: () => {
  1562. // 发送成功无需额外操作,等待服务端返回消息后再更新列表
  1563. },
  1564. fail: (err) => {
  1565. uni.showToast({
  1566. title: err,
  1567. icon: 'none'
  1568. });
  1569. // 发送失败时可恢复输入框内容
  1570. this.value = text;
  1571. }
  1572. });
  1573. } catch (err) {}
  1574. }
  1575. }
  1576. };
  1577. </script>
  1578. <style scoped lang="scss">
  1579. .swiper-wrapper {
  1580. position: relative;
  1581. width: 100%;
  1582. height: 100vh;
  1583. overflow: hidden;
  1584. background-color: #000000;
  1585. }
  1586. .swiper-container {
  1587. width: 100%;
  1588. height: 100vh;
  1589. }
  1590. .container {
  1591. width: 100%;
  1592. height: 100%;
  1593. position: relative;
  1594. transition: opacity 0.3s ease;
  1595. transform: translateZ(0); // 开启GPU加速
  1596. will-change: opacity; // 告诉浏览器提前优化该属性
  1597. }
  1598. .loading-tip {
  1599. position: absolute;
  1600. top: 50%;
  1601. left: 50%;
  1602. transform: translate(-50%, -50%);
  1603. display: flex;
  1604. flex-direction: column;
  1605. align-items: center;
  1606. justify-content: center;
  1607. z-index: 10;
  1608. .loading-text {
  1609. color: #ffffff;
  1610. font-size: 28rpx;
  1611. margin-top: 20rpx;
  1612. }
  1613. }
  1614. .loading-more,
  1615. .no-more {
  1616. display: flex;
  1617. justify-content: center;
  1618. align-items: center;
  1619. padding: 20rpx 0;
  1620. color: #999;
  1621. font-size: 24rpx;
  1622. }
  1623. .loading-more {
  1624. flex-direction: column;
  1625. }
  1626. /* button自带样式清除 */
  1627. .student-orther-icon button::after {
  1628. border: none !important;
  1629. padding: 0 !important;
  1630. margin: 0 !important;
  1631. }
  1632. .student-orther-icon button {
  1633. background-color: transparent !important;
  1634. padding: 0 !important;
  1635. line-height: inherit !important;
  1636. margin: 0 !important;
  1637. width: auto !important;
  1638. font-weight: 500 !important;
  1639. border-radius: none !important;
  1640. }
  1641. .welcome-message {
  1642. position: fixed;
  1643. width: 100%;
  1644. bottom: 700rpx;
  1645. left: 50%;
  1646. transform: translateX(-50%);
  1647. color: white;
  1648. padding: 10px 20px;
  1649. border-radius: 20px;
  1650. z-index: 10;
  1651. transition: opacity 0.3s ease;
  1652. }
  1653. @keyframes fadeOut {
  1654. from {
  1655. opacity: 1;
  1656. }
  1657. to {
  1658. opacity: 0;
  1659. }
  1660. }
  1661. .send {
  1662. background-color: #fafafa;
  1663. border-radius: 28rpx;
  1664. padding: 14rpx 16rpx;
  1665. color: #181818;
  1666. font-weight: 500;
  1667. }
  1668. // .container {
  1669. // position: relative;
  1670. // width: 100%;
  1671. // height: 100vh;
  1672. // overflow: hidden;
  1673. // }
  1674. // .talktext {
  1675. // border-radius: 8rpx;
  1676. // background-color: rgba(255, 255, 255, 0.1);
  1677. // view {
  1678. // width: 100%;
  1679. // }
  1680. // }
  1681. .talk-list {
  1682. border-radius: 30rpx;
  1683. background-color: rgba(33, 33, 33, 0.5);
  1684. padding: 10rpx 30rpx;
  1685. }
  1686. // .zoom-button-active {
  1687. // transform: scale(1.5);
  1688. // }
  1689. // .background-image {
  1690. // position: absolute;
  1691. // top: 0;
  1692. // left: -40rpx;
  1693. // width: 110%;
  1694. // height: 110%;
  1695. // object-fit: cover;
  1696. // filter: blur(20px);
  1697. // z-index: 0;
  1698. // }
  1699. // .background-images {
  1700. // position: absolute;
  1701. // top: 0;
  1702. // left: -40rpx;
  1703. // width: 110%;
  1704. // height: 110%;
  1705. // object-fit: cover;
  1706. // filter: blur(20px);
  1707. // z-index: 6;
  1708. // }
  1709. // .blackbg {
  1710. // position: absolute;
  1711. // top: 0;
  1712. // left: 0;
  1713. // width: 100%;
  1714. // height: 100%;
  1715. // background: rgba(0, 0, 0, 0.7);
  1716. // object-fit: cover;
  1717. // filter: blur(10px);
  1718. // z-index: 1;
  1719. // }
  1720. .content {
  1721. position: relative;
  1722. z-index: 2;
  1723. height: 100%;
  1724. width: 100%;
  1725. top: 0;
  1726. left: 0;
  1727. display: flex;
  1728. flex-direction: column;
  1729. justify-content: space-between;
  1730. .activity-box {
  1731. position: fixed;
  1732. top: 188rpx;
  1733. left: 30rpx;
  1734. z-index: 5;
  1735. display: flex;
  1736. align-items: center;
  1737. .item-box {
  1738. border-radius: 16rpx;
  1739. background-color: rgba(77, 77, 77, 0.5);
  1740. width: 90rpx;
  1741. height: 100rpx;
  1742. margin-right: 20rpx;
  1743. position: relative;
  1744. .tip {
  1745. position: absolute;
  1746. width: 100%;
  1747. bottom: 0;
  1748. color: #fafcff;
  1749. font-size: 26rpx;
  1750. background-color: rgba(15, 15, 15, 0.8);
  1751. border-radius: 16rpx;
  1752. text-align: center;
  1753. }
  1754. .item {
  1755. margin: 0 auto;
  1756. padding: 10rpx 0;
  1757. image {
  1758. height: 100%;
  1759. }
  1760. }
  1761. }
  1762. }
  1763. .siderow-group {
  1764. position: absolute;
  1765. top: 65%;
  1766. right: 30rpx;
  1767. z-index: 9;
  1768. display: flex;
  1769. flex-direction: column;
  1770. align-items: center;
  1771. .side-item {
  1772. font-weight: 500;
  1773. font-size: 24rpx;
  1774. color: #FFFFFF;
  1775. margin-bottom: 32rpx;
  1776. text-align: center;
  1777. .button {
  1778. background-color: transparent;
  1779. margin: 0;
  1780. line-height: 1;
  1781. padding: 0;
  1782. }
  1783. .image {
  1784. width: 72rpx;
  1785. height: 72rpx;
  1786. }
  1787. }
  1788. }
  1789. .side-group {
  1790. position: absolute;
  1791. // top: 30%;
  1792. top: 65%;
  1793. right: 30rpx;
  1794. z-index: 9;
  1795. display: flex;
  1796. flex-direction: column;
  1797. align-items: center;
  1798. .side-item {
  1799. font-weight: 500;
  1800. font-size: 24rpx;
  1801. color: #FFFFFF;
  1802. margin-bottom: 32rpx;
  1803. text-align: center;
  1804. .button {
  1805. background-color: transparent;
  1806. margin: 0;
  1807. line-height: 1;
  1808. padding: 0;
  1809. }
  1810. .image {
  1811. width: 72rpx;
  1812. height: 72rpx;
  1813. }
  1814. }
  1815. }
  1816. .content-top {
  1817. width: 100%;
  1818. margin-top: 48rpx;
  1819. display: flex;
  1820. justify-content: space-between;
  1821. padding: 0 24rpx;
  1822. box-sizing: border-box;
  1823. .sum {
  1824. width: 80rpx;
  1825. height: 52rpx;
  1826. background: rgba(0, 0, 0, 0.5);
  1827. border-radius: 26rpx 26rpx 26rpx 26rpx;
  1828. font-size: 24rpx;
  1829. color: #FFFFFF;
  1830. text-align: center;
  1831. line-height: 52rpx;
  1832. }
  1833. }
  1834. .follow-btn {
  1835. padding: 8rpx 16rpx;
  1836. background: linear-gradient(270deg, #FF5C03 0%, #FFAC64 100%);
  1837. border-radius: 26rpx;
  1838. font-weight: 500;
  1839. font-size: 26rpx;
  1840. color: #FFFFFF;
  1841. }
  1842. }
  1843. .input-box {
  1844. position: fixed;
  1845. bottom: 20rpx;
  1846. }
  1847. .videolist {
  1848. position: relative;
  1849. height: 100vh;
  1850. width: 100%;
  1851. .video {
  1852. height: 100vh;
  1853. width: 100%;
  1854. background-color: rgba(57, 57, 57, 0.4);
  1855. .item {
  1856. width: 100%;
  1857. height: 100%;
  1858. }
  1859. .time {
  1860. color: #ffffff;
  1861. font-size: 20rpx;
  1862. margin-left: 10rpx;
  1863. }
  1864. .lable {
  1865. position: absolute;
  1866. top: 50rpx;
  1867. right: 16rpx;
  1868. background-color: rgba(57, 57, 57, 0.6);
  1869. padding: 4rpx 10rpx;
  1870. color: #fff;
  1871. border-radius: 15rpx;
  1872. }
  1873. }
  1874. .video_row {
  1875. position: absolute;
  1876. top: 18%;
  1877. max-height: 500rpx;
  1878. z-index: 99;
  1879. }
  1880. }
  1881. .prize-card {
  1882. width: 504rpx;
  1883. padding: 40rpx;
  1884. box-sizing: border-box;
  1885. display: flex;
  1886. flex-direction: column;
  1887. align-items: center;
  1888. .title {
  1889. font-size: 34rpx;
  1890. font-weight: 500;
  1891. margin: 0rpx 0 40rpx;
  1892. }
  1893. .prize-content {
  1894. width: 100%;
  1895. display: flex;
  1896. justify-content: space-between;
  1897. align-items: center;
  1898. font-size: 30rpx;
  1899. margin-bottom: 10rpx;
  1900. .txt {
  1901. font-weight: 600;
  1902. }
  1903. }
  1904. .tip {
  1905. font-size: 30rpx;
  1906. color: #222222;
  1907. margin: 30rpx 0;
  1908. }
  1909. .button {
  1910. width: 200rpx;
  1911. height: 70rpx;
  1912. line-height: 70rpx;
  1913. background: linear-gradient(180deg, #ff7c30 0%, #FF3A1E 100%);
  1914. border-radius: 28rpx;
  1915. font-weight: 500;
  1916. font-size: 32rpx;
  1917. color: #ffffff;
  1918. text-align: center;
  1919. }
  1920. }
  1921. .red-card {
  1922. width: 504rpx;
  1923. height: 636rpx;
  1924. position: relative;
  1925. image {
  1926. position: absolute;
  1927. width: 100%;
  1928. height: 100%;
  1929. }
  1930. .red-content {
  1931. position: relative;
  1932. z-index: 5;
  1933. display: flex;
  1934. flex-direction: column;
  1935. align-items: center;
  1936. .title {
  1937. font-size: 36rpx;
  1938. color: #FF3A1E;
  1939. margin: 180rpx 0 90rpx;
  1940. }
  1941. // .title {
  1942. // font-size: 36rpx;
  1943. // color: #FF3A1E;
  1944. // margin: 90rpx 0 20rpx;
  1945. // }
  1946. // .money {
  1947. // font-weight: 500;
  1948. // font-size: 32rpx;
  1949. // color: #FF3A1E;
  1950. // .bold{
  1951. // font-weight: bold;
  1952. // font-size: 128rpx;
  1953. // }
  1954. // }
  1955. .txt {
  1956. font-size: 28rpx;
  1957. color: #FFECC3;
  1958. margin: 80rpx 0 40rpx;
  1959. }
  1960. .button {
  1961. width: 392rpx;
  1962. height: 96rpx;
  1963. line-height: 96rpx;
  1964. background: linear-gradient(180deg, #FFF4D5 0%, #FFE5B1 100%);
  1965. border-radius: 48rpx 48rpx 48rpx 48rpx;
  1966. font-weight: 600;
  1967. font-size: 36rpx;
  1968. color: #C32008;
  1969. text-align: center;
  1970. }
  1971. }
  1972. }
  1973. .integral-box {
  1974. width: 500rpx;
  1975. display: flex;
  1976. flex-direction: column;
  1977. align-items: center;
  1978. border-radius: 20rpx;
  1979. overflow: hidden;
  1980. .top {
  1981. width: 100%;
  1982. position: relative;
  1983. .title {
  1984. width: 100%;
  1985. font-weight: 600;
  1986. font-size: 40rpx;
  1987. color: #FFFFFF;
  1988. text-shadow: 0px 4px 8px rgba(255, 89, 2, 0.8);
  1989. position: absolute;
  1990. top: 50rpx;
  1991. text-align: center;
  1992. }
  1993. .photo {
  1994. width: 100%;
  1995. }
  1996. }
  1997. .item {
  1998. padding: 20rpx;
  1999. .title {
  2000. font-weight: 500;
  2001. font-size: 32rpx;
  2002. text-align: center;
  2003. }
  2004. .button {
  2005. font-size: 32rpx;
  2006. margin-top: 20rpx;
  2007. background: linear-gradient(270deg, #ff4702 0%, #fe6304 100%);
  2008. color: #fff;
  2009. text-align: center;
  2010. padding: 18rpx 60rpx;
  2011. border-radius: 10rpx;
  2012. font-weight: 500;
  2013. }
  2014. }
  2015. }
  2016. .goods {
  2017. position: fixed;
  2018. bottom: 140rpx;
  2019. right: 100rpx;
  2020. // transform: translate(-50%, -50%);
  2021. z-index: 5;
  2022. background-color: #fff;
  2023. border-radius: 20rpx;
  2024. overflow: hidden;
  2025. width: 280rpx;
  2026. padding: 4rpx;
  2027. .top {
  2028. position: absolute;
  2029. top: 4rpx;
  2030. display: flex;
  2031. justify-content: space-between;
  2032. align-items: center;
  2033. color: #fff;
  2034. width: 100%;
  2035. padding-right: 10rpx;
  2036. box-sizing: border-box;
  2037. .left {
  2038. background-color: rgba(0, 0, 0, 0.8);
  2039. padding: 12rpx;
  2040. font-size: 22rpx;
  2041. display: flex;
  2042. justify-content: space-between;
  2043. align-items: center;
  2044. border-radius: 10rpx;
  2045. }
  2046. }
  2047. .photo {
  2048. width: 100%;
  2049. height: 180rpx;
  2050. border-radius: 16rpx 16rpx 0 0;
  2051. overflow: hidden;
  2052. }
  2053. .item {
  2054. padding: 4rpx;
  2055. .price {
  2056. font-size: 30rpx;
  2057. .red {
  2058. color: #FF5701;
  2059. font-weight: 600;
  2060. margin-right: 10rpx;
  2061. }
  2062. .del {
  2063. color: #828282;
  2064. font-weight: 500;
  2065. font-size: 28rpx;
  2066. text-decoration: line-through
  2067. }
  2068. }
  2069. .title {
  2070. font-weight: 500;
  2071. font-size: 30rpx;
  2072. margin: 10rpx 0 12rpx;
  2073. }
  2074. .button {
  2075. background: linear-gradient(270deg, #ff4702 0%, #fe6304 100%);
  2076. color: #fff;
  2077. text-align: center;
  2078. padding: 16rpx 0;
  2079. border-radius: 10rpx;
  2080. font-weight: 500;
  2081. font-size: 30rpx;
  2082. }
  2083. }
  2084. }
  2085. .icon-bg {
  2086. background-color: rgba(57, 57, 57, 0.8);
  2087. border-radius: 50%;
  2088. width: 88rpx;
  2089. height: 88rpx;
  2090. display: flex;
  2091. justify-content: center;
  2092. align-items: center;
  2093. transition: transform 0.2s ease;
  2094. .button {
  2095. background-color: transparent;
  2096. margin: 0;
  2097. line-height: 1;
  2098. padding: 0;
  2099. }
  2100. }
  2101. .list {
  2102. width: 80%;
  2103. margin-bottom: 20rpx;
  2104. animation: xxxawdawd .2s;
  2105. }
  2106. @keyframes xxxawdawd {
  2107. from {
  2108. margin-top: 0rpx;
  2109. opacity: 0;
  2110. }
  2111. to {
  2112. margin-top: 20rpx;
  2113. opacity: 1;
  2114. }
  2115. }
  2116. .shop-prompt {
  2117. position: absolute;
  2118. bottom: 750rpx;
  2119. left: 24rpx;
  2120. padding: 6rpx 20rpx;
  2121. background: rgba(230, 154, 34, 0.7);
  2122. border-radius: 24rpx;
  2123. z-index: 9;
  2124. font-weight: 500;
  2125. color: #FFFFFF;
  2126. transition: opacity 0.3s ease;
  2127. }
  2128. .red-box {
  2129. .button {
  2130. margin: 0 auto;
  2131. width: 80%;
  2132. line-height: 88rpx;
  2133. height: 88rpx;
  2134. background: linear-gradient(90deg, #FF5701 0%, #FFB501 100%);
  2135. box-shadow: 0rpx 8rpx 8rpx 0rpx rgba(238, 124, 80, 0.2);
  2136. border-radius: 44rpx 44rpx 44rpx 44rpx;
  2137. font-weight: 600;
  2138. font-size: 32rpx;
  2139. color: #FFFFFF;
  2140. text-align: center;
  2141. }
  2142. }
  2143. .view-box {
  2144. position: relative;
  2145. height: 40vh;
  2146. /* 设置弹出层高度为视窗高度的70% */
  2147. padding: 40rpx 0rpx 120rpx;
  2148. box-sizing: border-box;
  2149. display: flex;
  2150. flex-direction: column;
  2151. .scroll-content {
  2152. flex: 1;
  2153. overflow-y: auto;
  2154. padding: 0 40rpx;
  2155. }
  2156. .bottom {
  2157. padding: 20rpx 40rpx;
  2158. position: absolute;
  2159. bottom: 0;
  2160. width: 100%;
  2161. box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(195, 195, 195, 0.3);
  2162. background: #fff;
  2163. }
  2164. }
  2165. // 抽奖
  2166. .answerpop {
  2167. background: linear-gradient(to right, #fff7f8, #fee1e2);
  2168. margin-top: 30rpx;
  2169. padding: 10rpx 20rpx;
  2170. width: calc(100% - 40rpx);
  2171. border-radius: 20rpx;
  2172. display: flex;
  2173. align-items: center;
  2174. justify-content: space-between;
  2175. box-shadow: 2px 2px 4px 0 rgba(255, 124, 126, 0.1);
  2176. .answera {
  2177. display: flex;
  2178. justify-content: center;
  2179. background-color: #fff;
  2180. padding: 20rpx 0;
  2181. width: 35%;
  2182. border-radius: 40rpx;
  2183. align-items: center;
  2184. margin: 10rpx 0;
  2185. box-shadow: 2px 2px 4px 0 rgba(72, 72, 72, 0.1);
  2186. image {
  2187. width: 40rpx;
  2188. height: 40rpx;
  2189. margin-right: 10rpx;
  2190. }
  2191. }
  2192. }
  2193. // 抽奖
  2194. .prize-box {
  2195. position: relative;
  2196. .nav-img {
  2197. width: 311rpx;
  2198. position: absolute;
  2199. top: -122rpx;
  2200. left: 50%;
  2201. transform: translateX(-50%);
  2202. }
  2203. .bg-img {
  2204. position: absolute;
  2205. top: 0;
  2206. left: 0;
  2207. width: 100%;
  2208. height: 100%;
  2209. z-index: 1;
  2210. }
  2211. .prize-content {
  2212. position: relative;
  2213. z-index: 2;
  2214. padding: 24rpx 0 68rpx;
  2215. .white-item {
  2216. width: 40rpx;
  2217. height: 40rpx;
  2218. text-align: center;
  2219. overflow: hidden;
  2220. background: #FFFFFF;
  2221. box-shadow: inset 0rpx 2rpx 8rpx 0rpx #FFEBB2;
  2222. border-radius: 8rpx;
  2223. margin: 4rpx;
  2224. font-weight: 600;
  2225. font-size: 24rpx;
  2226. color: #F85D22;
  2227. line-height: 40rpx;
  2228. }
  2229. .item-group {
  2230. width: 100%;
  2231. // position: absolute;
  2232. // top: 250rpx;
  2233. // left: 0;
  2234. // width: 100%;
  2235. // height: 500rpx;
  2236. }
  2237. // .item-group {
  2238. // position: absolute;
  2239. // top: 250rpx;
  2240. // left: 0;
  2241. // width: 100%;
  2242. // display: flex;
  2243. // align-items: center;
  2244. // overflow: hidden;
  2245. // overflow-x: auto; // 仅横向滚动
  2246. // overflow-y: hidden;
  2247. // gap: 20rpx;
  2248. // padding: 0 20rpx; // 左右留间隙,避免item贴边
  2249. // scroll-behavior: smooth; // 平滑滚动(关键)
  2250. // scrollbar-width: none; // 隐藏滚动条(可选,美化)
  2251. // -ms-overflow-style: none; // IE隐藏滚动条
  2252. // .item {
  2253. // flex-shrink: 0;
  2254. // width: 348rpx;
  2255. // height: 348rpx;
  2256. // background: #FFFFFF;
  2257. // box-shadow: 0rpx 12rpx 19rpx 2rpx rgba(219, 73, 22, 0.6);
  2258. // border-radius: 24rpx 24rpx 24rpx 24rpx;
  2259. // border: 4rpx solid #FFCA96;
  2260. // display: flex;
  2261. // flex-direction: column;
  2262. // align-items: center;
  2263. // box-sizing: border-box;
  2264. // padding: 20rpx 0;
  2265. // .title {
  2266. // font-weight: 500;
  2267. // font-size: 32rpx;
  2268. // color: #222222;
  2269. // margin: 20rpx 0 20rpx;
  2270. // }
  2271. // .txt {
  2272. // font-size: 24rpx;
  2273. // color: #757575;
  2274. // }
  2275. // }
  2276. // .center {
  2277. // width: 440rpx;
  2278. // height: 450rpx;
  2279. // }
  2280. // }
  2281. .point-group {
  2282. margin: 20rpx 0 50rpx;
  2283. display: flex;
  2284. gap: 6rpx;
  2285. .item {
  2286. width: 20rpx;
  2287. height: 8rpx;
  2288. background: rgba(255, 255, 255, 0.5);
  2289. border-radius: 4rpx 4rpx 4rpx 4rpx;
  2290. }
  2291. .selected {
  2292. background: #FFEB66;
  2293. }
  2294. }
  2295. .button {
  2296. margin-top: 30rpx;
  2297. width: 520rpx;
  2298. height: 88rpx;
  2299. line-height: 88rpx;
  2300. background: linear-gradient(180deg, #FFF4D6 0%, #FFEB66 100%);
  2301. box-shadow: 0rpx 10rpx 8rpx 4rpx rgba(246, 82, 25, 0.5);
  2302. border-radius: 44rpx;
  2303. font-weight: 500;
  2304. font-size: 36rpx;
  2305. color: #F4410B;
  2306. text-align: center;
  2307. }
  2308. }
  2309. }
  2310. .shoppop {
  2311. padding: 22rpx 16rpx;
  2312. .shoppop-top {
  2313. display: flex;
  2314. justify-content: space-between;
  2315. align-items: center;
  2316. padding: 0 16rpx 22rpx;
  2317. .search-input {
  2318. width: 414rpx;
  2319. height: 76rpx;
  2320. background: #FFFFFF;
  2321. border-radius: 36rpx;
  2322. margin-left: 20rpx;
  2323. padding: 0 32rpx;
  2324. box-sizing: border-box;
  2325. font-size: 24rpx;
  2326. margin-right: 24rpx;
  2327. }
  2328. .search-top {
  2329. font-size: 20rpx;
  2330. color: #222222;
  2331. }
  2332. }
  2333. .shop-list {
  2334. overflow: hidden;
  2335. .list-item {
  2336. display: flex;
  2337. align-items: center;
  2338. padding: 20rpx 16rpx;
  2339. background: #FFFFFF;
  2340. border-radius: 16rpx;
  2341. margin-bottom: 16rpx;
  2342. .goods-img {
  2343. width: 200rpx;
  2344. height: 200rpx;
  2345. border-radius: 16rpx;
  2346. overflow: hidden;
  2347. position: relative;
  2348. margin-right: 24rpx;
  2349. image {
  2350. width: 100%;
  2351. height: 100%;
  2352. }
  2353. .goods-label {
  2354. position: absolute;
  2355. top: 0;
  2356. width: 64rpx;
  2357. height: 40rpx;
  2358. background: rgba(0, 0, 0, 0.5);
  2359. border-radius: 16rpx 0rpx 16rpx 0rpx;
  2360. text-align: center;
  2361. font-weight: 500;
  2362. font-size: 28rpx;
  2363. color: #FFFFFF;
  2364. }
  2365. }
  2366. .goods-right {
  2367. flex: 1;
  2368. .goods-title {
  2369. font-weight: 500;
  2370. font-size: 30rpx;
  2371. color: #000000;
  2372. margin-bottom: 10rpx;
  2373. }
  2374. .goods-details {
  2375. font-size: 24rpx;
  2376. color: #999999;
  2377. margin: 10rpx 0 20rpx;
  2378. }
  2379. .goods-people {
  2380. font-size: 22rpx;
  2381. color: #E69A22;
  2382. height: 56rpx;
  2383. }
  2384. .goods-shop {
  2385. display: flex;
  2386. justify-content: space-between;
  2387. .nummber {
  2388. color: #FF5C03;
  2389. font-size: 22rpx;
  2390. font-weight: 500;
  2391. }
  2392. .btn-group {
  2393. text-align: center;
  2394. line-height: 56rpx;
  2395. .collect-btn {
  2396. width: 72rpx;
  2397. background: #F5F7FA;
  2398. border-radius: 8rpx 0rpx 0rpx 8rpx;
  2399. }
  2400. .shop-btn {
  2401. width: 152rpx;
  2402. background: linear-gradient(270deg, #FF5C03 0%, #FFAC64 100%);
  2403. border-radius: 0rpx 8rpx 8rpx 0rpx;
  2404. font-weight: 500;
  2405. font-size: 30rpx;
  2406. color: #FFFFFF;
  2407. }
  2408. }
  2409. }
  2410. }
  2411. }
  2412. }
  2413. }
  2414. :deep(.u-list-item) {
  2415. width: 100%;
  2416. display: flex;
  2417. align-items: center;
  2418. }
  2419. :deep(.u-safe-area-inset-bottom) {
  2420. padding-bottom: 0
  2421. }
  2422. </style>