livingno.vue 60 KB

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