index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858
  1. <template>
  2. <div class="pull-wrap">
  3. <div class="bg-img-wrap">
  4. <div
  5. v-if="configBg !== ''"
  6. class="bg-img"
  7. :style="{ backgroundImage: `url(${configBg})` }"
  8. ></div>
  9. <video
  10. v-if="configVideo !== ''"
  11. class="bg-video"
  12. :src="configVideo"
  13. muted
  14. autoplay
  15. loop
  16. ></video>
  17. <div
  18. v-else
  19. class="bg-img"
  20. ></div>
  21. </div>
  22. <div class="left">
  23. <div
  24. ref="topRef"
  25. class="head"
  26. >
  27. <div class="info">
  28. <div
  29. class="avatar"
  30. :style="{
  31. backgroundImage: `url(${anchorInfo?.avatar})`,
  32. }"
  33. @click="
  34. router.push({
  35. name: routerName.profile,
  36. params: { userId: anchorInfo?.id },
  37. })
  38. "
  39. ></div>
  40. <div class="detail">
  41. <div class="top">{{ anchorInfo?.username }}</div>
  42. <div class="bottom">
  43. <span>{{ appStore.liveRoomInfo?.desc }}</span>
  44. <span v-if="NODE_ENV === 'development'">
  45. socketId:{{ mySocketId }}
  46. </span>
  47. <span
  48. class="area"
  49. @click="
  50. router.push({
  51. name: routerName.area,
  52. query: { id: appStore.liveRoomInfo?.areas?.[0].id },
  53. })
  54. "
  55. >{{ appStore.liveRoomInfo?.areas?.[0].name }}</span
  56. >
  57. </div>
  58. </div>
  59. </div>
  60. <div class="other">在线人数:{{ liveUserList.length }}</div>
  61. </div>
  62. <div
  63. ref="containerRef"
  64. class="container"
  65. >
  66. <div
  67. class="no-live"
  68. v-if="!roomLiving"
  69. >
  70. 主播还没开播~
  71. </div>
  72. <div
  73. v-else
  74. v-loading="videoLoading"
  75. class="video-wrap"
  76. >
  77. <div
  78. class="cover"
  79. :style="{
  80. backgroundImage: `url(${
  81. appStore.liveRoomInfo?.cover_img || anchorInfo?.avatar
  82. })`,
  83. }"
  84. ></div>
  85. <div
  86. ref="remoteVideoRef"
  87. class="media-list"
  88. :class="{ item: appStore.allTrack.length > 1 }"
  89. ></div>
  90. <VideoControls
  91. :resolution="videoHeight"
  92. @refresh="handleRefresh"
  93. ></VideoControls>
  94. </div>
  95. </div>
  96. <div
  97. ref="bottomRef"
  98. v-loading="giftLoading"
  99. class="gift-list"
  100. >
  101. <div
  102. v-for="(item, index) in giftGoodsList"
  103. :key="index"
  104. class="item"
  105. @click="handlePay()"
  106. >
  107. <div
  108. class="ico"
  109. :style="{ backgroundImage: `url(${item.cover})` }"
  110. >
  111. <div
  112. v-if="item.badge"
  113. class="badge"
  114. :style="{ backgroundColor: item.badge_bg }"
  115. >
  116. <span class="txt">{{ item.badge }}</span>
  117. </div>
  118. </div>
  119. <div class="name">{{ item.name }}</div>
  120. <div class="price">¥{{ item.price }}</div>
  121. </div>
  122. <div
  123. class="item"
  124. @click="handleRecharge"
  125. >
  126. <div class="ico wallet"></div>
  127. <div class="name">余额:{{ userStore.userInfo?.wallet?.balance }}</div>
  128. <div class="price">立即充值</div>
  129. </div>
  130. </div>
  131. </div>
  132. <div class="right">
  133. <div class="tab">
  134. <span>在线用户</span>
  135. <span> | </span>
  136. <span>排行榜</span>
  137. </div>
  138. <div class="user-list">
  139. <div
  140. v-for="(item, index) in liveUserList.filter(
  141. (item) => item.id !== mySocketId
  142. )"
  143. :key="index"
  144. class="item"
  145. >
  146. <div class="info">
  147. <div
  148. class="avatar"
  149. :style="{ backgroundImage: `url(${item.userInfo?.avatar})` }"
  150. ></div>
  151. <div class="username">
  152. {{ item.userInfo?.username || item.id }}
  153. </div>
  154. </div>
  155. </div>
  156. <div
  157. v-if="userStore.userInfo"
  158. class="item"
  159. >
  160. <div class="info">
  161. <img
  162. :src="userStore.userInfo.avatar"
  163. class="avatar"
  164. alt=""
  165. />
  166. <div class="username">{{ userStore.userInfo.username }}</div>
  167. </div>
  168. </div>
  169. </div>
  170. <div
  171. ref="danmuListRef"
  172. class="danmu-list"
  173. >
  174. <div
  175. v-for="(item, index) in damuList"
  176. :key="index"
  177. class="item"
  178. >
  179. <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
  180. <span class="name">
  181. <span v-if="item.userInfo">
  182. {{ item.userInfo.username }}[{{
  183. item.userInfo.roles?.map((v) => v.role_name).join()
  184. }}]
  185. </span>
  186. <span v-else>{{ item.socket_id }}[游客]</span>
  187. </span>
  188. <span>:</span>
  189. <span
  190. class="msg"
  191. v-if="!item.msgIsFile"
  192. >
  193. {{ item.msg }}
  194. </span>
  195. <div
  196. class="msg img"
  197. v-else
  198. >
  199. <img
  200. :src="item.msg"
  201. alt=""
  202. @load="handleScrollTop"
  203. />
  204. </div>
  205. </template>
  206. <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
  207. <span class="name system">系统通知:</span>
  208. <span class="msg">
  209. {{ item.userInfo?.username || item.socket_id }}进入直播!
  210. </span>
  211. </template>
  212. <template v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved">
  213. <span class="name system">系统通知:</span>
  214. <span class="msg">
  215. {{ item.userInfo?.username || item.socket_id }}离开直播!
  216. </span>
  217. </template>
  218. </div>
  219. </div>
  220. <div
  221. class="send-msg"
  222. v-loading="msgLoading"
  223. >
  224. <div class="control">
  225. <div
  226. class="ico face"
  227. title="表情"
  228. @click="handleWait"
  229. ></div>
  230. <div
  231. class="ico img"
  232. title="图片"
  233. @click="mockClick"
  234. >
  235. <input
  236. ref="uploadRef"
  237. type="file"
  238. class="input-upload"
  239. accept=".webp,.png,.jpg,.jpeg,.gif"
  240. @change="uploadChange"
  241. />
  242. </div>
  243. </div>
  244. <textarea
  245. v-model="danmuStr"
  246. class="ipt"
  247. @keydown="keydownDanmu"
  248. ></textarea>
  249. <div
  250. class="btn"
  251. @click="sendDanmu"
  252. >
  253. 发送
  254. </div>
  255. </div>
  256. </div>
  257. <RechargeCpt
  258. :show="showRecharge"
  259. @close="(v) => (showRecharge = v)"
  260. ></RechargeCpt>
  261. </div>
  262. </template>
  263. <script lang="ts" setup>
  264. import { onMounted, onUnmounted, ref, watch } from 'vue';
  265. import { fetchGoodsList } from '@/api/goods';
  266. import { QINIU_LIVE } from '@/constant';
  267. import { loginTip } from '@/hooks/use-login';
  268. import { usePull } from '@/hooks/use-pull';
  269. import { useUpload } from '@/hooks/use-upload';
  270. import { DanmuMsgTypeEnum, GoodsTypeEnum, IGoods } from '@/interface';
  271. import router, { routerName } from '@/router';
  272. import { useAppStore } from '@/store/app';
  273. import { useUserStore } from '@/store/user';
  274. import { NODE_ENV } from 'script/constant';
  275. import RechargeCpt from './recharge/index.vue';
  276. const userStore = useUserStore();
  277. const appStore = useAppStore();
  278. const configBg = ref();
  279. const configVideo = ref();
  280. const giftGoodsList = ref<IGoods[]>([]);
  281. const height = ref(0);
  282. const giftLoading = ref(false);
  283. const showRecharge = ref(false);
  284. const msgLoading = ref(false);
  285. const topRef = ref<HTMLDivElement>();
  286. const bottomRef = ref<HTMLDivElement>();
  287. const danmuListRef = ref<HTMLDivElement>();
  288. const remoteVideoRef = ref<HTMLDivElement>();
  289. const containerRef = ref<HTMLDivElement>();
  290. const uploadRef = ref<HTMLInputElement>();
  291. const {
  292. initPull,
  293. closeWs,
  294. closeRtc,
  295. keydownDanmu,
  296. sendDanmu,
  297. handlePlay,
  298. msgIsFile,
  299. mySocketId,
  300. videoHeight,
  301. videoLoading,
  302. remoteVideo,
  303. roomLiving,
  304. damuList,
  305. liveUserList,
  306. danmuStr,
  307. anchorInfo,
  308. } = usePull();
  309. onMounted(() => {
  310. setTimeout(() => {
  311. scrollTo(0, 0);
  312. }, 100);
  313. appStore.setPlay(true);
  314. getGoodsList();
  315. if (topRef.value && bottomRef.value && containerRef.value) {
  316. const res =
  317. bottomRef.value.getBoundingClientRect().top -
  318. (topRef.value.getBoundingClientRect().top +
  319. topRef.value.getBoundingClientRect().height);
  320. height.value = res;
  321. }
  322. getBg();
  323. initPull();
  324. });
  325. onUnmounted(() => {
  326. closeWs();
  327. closeRtc();
  328. });
  329. watch(
  330. () => remoteVideo.value,
  331. (newVal) => {
  332. newVal.forEach((item) => {
  333. remoteVideoRef.value?.appendChild(item);
  334. });
  335. },
  336. {
  337. deep: true,
  338. immediate: true,
  339. }
  340. );
  341. watch(
  342. () => damuList.value.length,
  343. () => {
  344. setTimeout(() => {
  345. handleScrollTop();
  346. }, 0);
  347. }
  348. );
  349. watch(
  350. () => appStore.liveRoomInfo,
  351. () => {
  352. getBg();
  353. },
  354. {
  355. deep: true,
  356. }
  357. );
  358. function getBg() {
  359. try {
  360. const reg = /.+\.mp4$/g;
  361. const url = appStore.liveRoomInfo?.bg_img;
  362. if (url) {
  363. if (reg.exec(url)) {
  364. configVideo.value = url;
  365. } else {
  366. configBg.value = url;
  367. }
  368. }
  369. } catch (error) {
  370. console.log(error);
  371. }
  372. }
  373. function handleWait() {
  374. window.$message.warning('敬请期待!');
  375. }
  376. function mockClick() {
  377. uploadRef.value?.click();
  378. }
  379. async function uploadChange() {
  380. const fileList = uploadRef.value?.files;
  381. if (fileList?.length) {
  382. try {
  383. msgLoading.value = true;
  384. msgIsFile.value = true;
  385. const res = await useUpload({
  386. prefix: QINIU_LIVE.prefix['billd-live/msg-image/'],
  387. file: fileList[0],
  388. });
  389. if (res?.resultUrl) {
  390. danmuStr.value = res.resultUrl || '错误图片';
  391. sendDanmu();
  392. }
  393. } catch (error) {
  394. console.log(error);
  395. } finally {
  396. msgIsFile.value = false;
  397. msgLoading.value = false;
  398. }
  399. }
  400. }
  401. function handlePay() {
  402. window.$message.info('敬请期待!');
  403. }
  404. function handleRefresh() {
  405. if (appStore.liveRoomInfo) {
  406. handlePlay(appStore.liveRoomInfo);
  407. }
  408. }
  409. async function getGoodsList() {
  410. try {
  411. giftLoading.value = true;
  412. const res = await fetchGoodsList({
  413. type: GoodsTypeEnum.gift,
  414. orderName: 'created_at',
  415. orderBy: 'desc',
  416. });
  417. if (res.code === 200) {
  418. giftGoodsList.value = res.data.rows;
  419. }
  420. } catch (error) {
  421. console.log(error);
  422. } finally {
  423. giftLoading.value = false;
  424. }
  425. }
  426. function handleRecharge() {
  427. if (!loginTip()) return;
  428. showRecharge.value = true;
  429. }
  430. function handleScrollTop() {
  431. if (danmuListRef.value) {
  432. danmuListRef.value.scrollTop = danmuListRef.value.scrollHeight + 10000;
  433. }
  434. }
  435. </script>
  436. <style lang="scss" scoped>
  437. .pull-wrap {
  438. display: flex;
  439. justify-content: space-around;
  440. margin: 15px auto 0;
  441. width: $w-1275;
  442. .bg-img-wrap {
  443. position: absolute;
  444. top: $layout-head-h;
  445. left: 50%;
  446. max-width: 1920px;
  447. max-height: 890px;
  448. width: 100%;
  449. height: 100%;
  450. background-position: center center;
  451. background-size: cover;
  452. background-repeat: no-repeat;
  453. transform: translateX(-50%);
  454. .bg-img {
  455. position: absolute;
  456. top: 0;
  457. right: 0;
  458. left: 0;
  459. z-index: -1;
  460. width: 100%;
  461. height: 100%;
  462. background-position: center;
  463. background-size: cover;
  464. background-repeat: no-repeat;
  465. }
  466. .bg-video {
  467. position: absolute;
  468. top: 0;
  469. right: 0;
  470. left: 0;
  471. z-index: -1;
  472. width: 100%;
  473. height: 100%;
  474. }
  475. }
  476. .left {
  477. position: relative;
  478. display: inline-block;
  479. overflow: hidden;
  480. box-sizing: border-box;
  481. width: $w-1000;
  482. height: 100%;
  483. border-radius: 6px;
  484. background-color: papayawhip;
  485. color: #61666d;
  486. vertical-align: top;
  487. .head {
  488. display: flex;
  489. justify-content: space-between;
  490. padding: 10px 20px;
  491. .info {
  492. display: flex;
  493. align-items: center;
  494. text-align: initial;
  495. .avatar {
  496. margin-right: 20px;
  497. width: 50px;
  498. height: 50px;
  499. border-radius: 50%;
  500. cursor: pointer;
  501. @extend %containBg;
  502. }
  503. .detail {
  504. .top {
  505. margin-bottom: 10px;
  506. color: #18191c;
  507. }
  508. .bottom {
  509. font-size: 14px;
  510. .area {
  511. margin-left: 10px;
  512. color: #9499a0;
  513. cursor: pointer;
  514. }
  515. }
  516. }
  517. }
  518. .other {
  519. display: flex;
  520. flex-direction: column;
  521. justify-content: center;
  522. font-size: 14px;
  523. }
  524. }
  525. .container {
  526. display: flex;
  527. align-items: center;
  528. justify-content: space-between;
  529. height: 562px;
  530. background-color: rgba($color: #000000, $alpha: 0.5);
  531. .no-live {
  532. position: absolute;
  533. top: 50%;
  534. left: 50%;
  535. z-index: 20;
  536. color: white;
  537. font-size: 28px;
  538. transform: translate(-50%, -50%);
  539. }
  540. .video-wrap {
  541. position: relative;
  542. overflow: hidden;
  543. flex: 1;
  544. height: 100%;
  545. .cover {
  546. position: absolute;
  547. background-position: center center;
  548. background-size: cover;
  549. filter: blur(10px);
  550. inset: 0;
  551. }
  552. .videoControls {
  553. position: relative;
  554. z-index: 20;
  555. }
  556. .media-list {
  557. position: relative;
  558. height: 562px;
  559. :deep(video) {
  560. position: absolute;
  561. top: 50%;
  562. left: 50%;
  563. display: block;
  564. margin: 0 auto;
  565. // min-width: 100%;
  566. // min-height: 100%;
  567. max-width: $w-1000;
  568. max-height: 562px;
  569. transform: translate(-50%, -50%);
  570. }
  571. :deep(canvas) {
  572. position: absolute;
  573. top: 50%;
  574. left: 50%;
  575. display: block;
  576. margin: 0 auto;
  577. // min-width: 100%;
  578. // min-height: 100%;
  579. max-width: $w-1000;
  580. max-height: 562px;
  581. transform: translate(-50%, -50%);
  582. }
  583. // &.item {
  584. // :deep(video) {
  585. // width: 50%;
  586. // height: initial !important;
  587. // }
  588. // :deep(canvas) {
  589. // width: 50%;
  590. // height: initial !important;
  591. // }
  592. // }
  593. }
  594. .controls {
  595. display: none;
  596. }
  597. .tip-btn {
  598. position: absolute;
  599. top: 50%;
  600. left: 50%;
  601. z-index: 1;
  602. align-items: center;
  603. padding: 12px 26px;
  604. border: 2px solid rgba($color: $theme-color-gold, $alpha: 0.5);
  605. border-radius: 6px;
  606. background-color: rgba(0, 0, 0, 0.3);
  607. color: $theme-color-gold;
  608. cursor: pointer;
  609. transform: translate(-50%, -50%);
  610. &:hover {
  611. background-color: rgba($color: $theme-color-gold, $alpha: 0.5);
  612. color: white;
  613. }
  614. }
  615. }
  616. }
  617. .gift-list {
  618. position: relative;
  619. display: flex;
  620. align-items: center;
  621. justify-content: space-around;
  622. box-sizing: border-box;
  623. margin: 5px 0;
  624. height: 100px;
  625. > :last-child {
  626. position: absolute;
  627. }
  628. .item {
  629. display: flex;
  630. align-items: center;
  631. flex-direction: column;
  632. justify-content: center;
  633. box-sizing: border-box;
  634. width: 100px;
  635. height: 100px;
  636. text-align: center;
  637. cursor: pointer;
  638. &:hover {
  639. background-color: #ebe0ce;
  640. }
  641. .ico {
  642. position: relative;
  643. width: 45px;
  644. height: 45px;
  645. background-position: center center;
  646. background-size: cover;
  647. background-repeat: no-repeat;
  648. &.wallet {
  649. background-image: url('@/assets/img/wallet.webp');
  650. }
  651. .badge {
  652. position: absolute;
  653. top: -8px;
  654. right: -10px;
  655. display: flex;
  656. align-items: center;
  657. justify-content: center;
  658. padding: 2px;
  659. border-radius: 2px;
  660. color: white;
  661. .txt {
  662. display: inline-block;
  663. line-height: 1;
  664. transform-origin: center !important;
  665. @include minFont(10);
  666. }
  667. }
  668. }
  669. .name {
  670. color: #18191c;
  671. font-size: 12px;
  672. }
  673. .price {
  674. color: #9499a0;
  675. font-size: 12px;
  676. }
  677. }
  678. }
  679. }
  680. .right {
  681. position: relative;
  682. display: inline-block;
  683. box-sizing: border-box;
  684. width: $w-250;
  685. border-radius: 6px;
  686. background-color: papayawhip;
  687. color: #9499a0;
  688. .tab {
  689. display: flex;
  690. align-items: center;
  691. justify-content: space-evenly;
  692. padding: 5px 0;
  693. font-size: 12px;
  694. }
  695. .user-list {
  696. overflow-y: scroll;
  697. padding: 0 15px;
  698. height: 100px;
  699. background-color: papayawhip;
  700. @extend %customScrollbar;
  701. .item {
  702. display: flex;
  703. align-items: center;
  704. justify-content: space-between;
  705. margin-bottom: 10px;
  706. font-size: 12px;
  707. .info {
  708. display: flex;
  709. align-items: center;
  710. cursor: pointer;
  711. .avatar {
  712. margin-right: 5px;
  713. width: 25px;
  714. height: 25px;
  715. border-radius: 50%;
  716. @extend %containBg;
  717. }
  718. .username {
  719. color: black;
  720. }
  721. }
  722. }
  723. }
  724. .danmu-list {
  725. overflow-y: scroll;
  726. box-sizing: border-box;
  727. padding-top: 4px;
  728. height: 480px;
  729. background-color: #f6f7f8;
  730. text-align: initial;
  731. @extend %customScrollbar;
  732. .item {
  733. box-sizing: border-box;
  734. margin-bottom: 4px;
  735. padding: 2px 10px;
  736. white-space: normal;
  737. word-wrap: break-word;
  738. font-size: 13px;
  739. .name {
  740. color: #9499a0;
  741. cursor: pointer;
  742. &.system {
  743. color: red;
  744. }
  745. }
  746. .msg {
  747. margin-top: 4px;
  748. color: #61666d;
  749. &.img {
  750. img {
  751. width: 80%;
  752. }
  753. }
  754. }
  755. }
  756. }
  757. .send-msg {
  758. box-sizing: border-box;
  759. padding: 4px 10px;
  760. width: 100%;
  761. .control {
  762. display: flex;
  763. margin: 4px 0;
  764. .ico {
  765. margin-right: 6px;
  766. width: 24px;
  767. height: 24px;
  768. cursor: pointer;
  769. .input-upload {
  770. width: 0;
  771. height: 0;
  772. opacity: 0;
  773. }
  774. &.face {
  775. @include setBackground('@/assets/img/msg-face.webp');
  776. }
  777. &.img {
  778. @include setBackground('@/assets/img/msg-img.webp');
  779. }
  780. }
  781. }
  782. .ipt {
  783. display: block;
  784. box-sizing: border-box;
  785. margin: 0 auto;
  786. padding: 10px;
  787. width: 100%;
  788. height: 60px;
  789. outline: none;
  790. border: 1px solid hsla(0, 0%, 60%, 0.2);
  791. border-radius: 4px;
  792. background-color: #f1f2f3;
  793. font-size: 14px;
  794. }
  795. .btn {
  796. box-sizing: border-box;
  797. margin-top: 10px;
  798. margin-left: auto;
  799. padding: 4px;
  800. width: 70px;
  801. border-radius: 4px;
  802. background-color: $theme-color-gold;
  803. color: white;
  804. text-align: center;
  805. font-size: 12px;
  806. cursor: pointer;
  807. }
  808. }
  809. }
  810. }
  811. // 屏幕宽度大于1500的时候
  812. @media screen and (min-width: $w-1500) {
  813. .pull-wrap {
  814. width: $w-1350;
  815. .left {
  816. width: $w-1000;
  817. }
  818. .right {
  819. width: $w-300;
  820. }
  821. }
  822. }
  823. </style>