index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  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. if (uploadRef.value) {
  399. uploadRef.value.value = '';
  400. }
  401. }
  402. }
  403. }
  404. function handlePay() {
  405. window.$message.info('敬请期待!');
  406. }
  407. function handleRefresh() {
  408. if (appStore.liveRoomInfo) {
  409. handlePlay(appStore.liveRoomInfo);
  410. }
  411. }
  412. async function getGoodsList() {
  413. try {
  414. giftLoading.value = true;
  415. const res = await fetchGoodsList({
  416. type: GoodsTypeEnum.gift,
  417. orderName: 'created_at',
  418. orderBy: 'desc',
  419. });
  420. if (res.code === 200) {
  421. giftGoodsList.value = res.data.rows;
  422. }
  423. } catch (error) {
  424. console.log(error);
  425. } finally {
  426. giftLoading.value = false;
  427. }
  428. }
  429. function handleRecharge() {
  430. if (!loginTip()) return;
  431. showRecharge.value = true;
  432. }
  433. function handleScrollTop() {
  434. if (danmuListRef.value) {
  435. danmuListRef.value.scrollTop = danmuListRef.value.scrollHeight + 10000;
  436. }
  437. }
  438. </script>
  439. <style lang="scss" scoped>
  440. .pull-wrap {
  441. display: flex;
  442. justify-content: space-around;
  443. margin: 15px auto 0;
  444. width: $w-1275;
  445. .bg-img-wrap {
  446. position: absolute;
  447. top: $layout-head-h;
  448. left: 50%;
  449. max-width: 1920px;
  450. max-height: 890px;
  451. width: 100%;
  452. height: 100%;
  453. background-position: center center;
  454. background-size: cover;
  455. background-repeat: no-repeat;
  456. transform: translateX(-50%);
  457. .bg-img {
  458. position: absolute;
  459. top: 0;
  460. right: 0;
  461. left: 0;
  462. z-index: -1;
  463. width: 100%;
  464. height: 100%;
  465. background-position: center;
  466. background-size: cover;
  467. background-repeat: no-repeat;
  468. }
  469. .bg-video {
  470. position: absolute;
  471. top: 0;
  472. right: 0;
  473. left: 0;
  474. z-index: -1;
  475. width: 100%;
  476. height: 100%;
  477. }
  478. }
  479. .left {
  480. position: relative;
  481. display: inline-block;
  482. overflow: hidden;
  483. box-sizing: border-box;
  484. width: $w-1000;
  485. height: 100%;
  486. border-radius: 6px;
  487. background-color: papayawhip;
  488. color: #61666d;
  489. vertical-align: top;
  490. .head {
  491. display: flex;
  492. justify-content: space-between;
  493. padding: 10px 20px;
  494. .info {
  495. display: flex;
  496. align-items: center;
  497. text-align: initial;
  498. .avatar {
  499. margin-right: 20px;
  500. width: 50px;
  501. height: 50px;
  502. border-radius: 50%;
  503. cursor: pointer;
  504. @extend %containBg;
  505. }
  506. .detail {
  507. .top {
  508. margin-bottom: 10px;
  509. color: #18191c;
  510. }
  511. .bottom {
  512. font-size: 14px;
  513. .area {
  514. margin-left: 10px;
  515. color: #9499a0;
  516. cursor: pointer;
  517. }
  518. }
  519. }
  520. }
  521. .other {
  522. display: flex;
  523. flex-direction: column;
  524. justify-content: center;
  525. font-size: 14px;
  526. }
  527. }
  528. .container {
  529. display: flex;
  530. align-items: center;
  531. justify-content: space-between;
  532. height: 562px;
  533. background-color: rgba($color: #000000, $alpha: 0.5);
  534. .no-live {
  535. position: absolute;
  536. top: 50%;
  537. left: 50%;
  538. z-index: 20;
  539. color: white;
  540. font-size: 28px;
  541. transform: translate(-50%, -50%);
  542. }
  543. .video-wrap {
  544. position: relative;
  545. overflow: hidden;
  546. flex: 1;
  547. height: 100%;
  548. .cover {
  549. position: absolute;
  550. background-position: center center;
  551. background-size: cover;
  552. filter: blur(10px);
  553. inset: 0;
  554. }
  555. .videoControls {
  556. position: relative;
  557. z-index: 20;
  558. }
  559. .media-list {
  560. position: relative;
  561. height: 562px;
  562. :deep(video) {
  563. position: absolute;
  564. top: 50%;
  565. left: 50%;
  566. display: block;
  567. margin: 0 auto;
  568. // min-width: 100%;
  569. // min-height: 100%;
  570. max-width: $w-1000;
  571. max-height: 562px;
  572. transform: translate(-50%, -50%);
  573. }
  574. :deep(canvas) {
  575. position: absolute;
  576. top: 50%;
  577. left: 50%;
  578. display: block;
  579. margin: 0 auto;
  580. // min-width: 100%;
  581. // min-height: 100%;
  582. max-width: $w-1000;
  583. max-height: 562px;
  584. transform: translate(-50%, -50%);
  585. }
  586. // &.item {
  587. // :deep(video) {
  588. // width: 50%;
  589. // height: initial !important;
  590. // }
  591. // :deep(canvas) {
  592. // width: 50%;
  593. // height: initial !important;
  594. // }
  595. // }
  596. }
  597. .controls {
  598. display: none;
  599. }
  600. .tip-btn {
  601. position: absolute;
  602. top: 50%;
  603. left: 50%;
  604. z-index: 1;
  605. align-items: center;
  606. padding: 12px 26px;
  607. border: 2px solid rgba($color: $theme-color-gold, $alpha: 0.5);
  608. border-radius: 6px;
  609. background-color: rgba(0, 0, 0, 0.3);
  610. color: $theme-color-gold;
  611. cursor: pointer;
  612. transform: translate(-50%, -50%);
  613. &:hover {
  614. background-color: rgba($color: $theme-color-gold, $alpha: 0.5);
  615. color: white;
  616. }
  617. }
  618. }
  619. }
  620. .gift-list {
  621. position: relative;
  622. display: flex;
  623. align-items: center;
  624. justify-content: space-around;
  625. box-sizing: border-box;
  626. margin: 5px 0;
  627. height: 100px;
  628. > :last-child {
  629. position: absolute;
  630. }
  631. .item {
  632. display: flex;
  633. align-items: center;
  634. flex-direction: column;
  635. justify-content: center;
  636. box-sizing: border-box;
  637. width: 100px;
  638. height: 100px;
  639. text-align: center;
  640. cursor: pointer;
  641. &:hover {
  642. background-color: #ebe0ce;
  643. }
  644. .ico {
  645. position: relative;
  646. width: 45px;
  647. height: 45px;
  648. background-position: center center;
  649. background-size: cover;
  650. background-repeat: no-repeat;
  651. &.wallet {
  652. background-image: url('@/assets/img/wallet.webp');
  653. }
  654. .badge {
  655. position: absolute;
  656. top: -8px;
  657. right: -10px;
  658. display: flex;
  659. align-items: center;
  660. justify-content: center;
  661. padding: 2px;
  662. border-radius: 2px;
  663. color: white;
  664. .txt {
  665. display: inline-block;
  666. line-height: 1;
  667. transform-origin: center !important;
  668. @include minFont(10);
  669. }
  670. }
  671. }
  672. .name {
  673. color: #18191c;
  674. font-size: 12px;
  675. }
  676. .price {
  677. color: #9499a0;
  678. font-size: 12px;
  679. }
  680. }
  681. }
  682. }
  683. .right {
  684. position: relative;
  685. display: inline-block;
  686. box-sizing: border-box;
  687. width: $w-250;
  688. border-radius: 6px;
  689. background-color: papayawhip;
  690. color: #9499a0;
  691. .tab {
  692. display: flex;
  693. align-items: center;
  694. justify-content: space-evenly;
  695. padding: 5px 0;
  696. font-size: 12px;
  697. }
  698. .user-list {
  699. overflow-y: scroll;
  700. padding: 0 15px;
  701. height: 100px;
  702. background-color: papayawhip;
  703. @extend %customScrollbar;
  704. .item {
  705. display: flex;
  706. align-items: center;
  707. justify-content: space-between;
  708. margin-bottom: 10px;
  709. font-size: 12px;
  710. .info {
  711. display: flex;
  712. align-items: center;
  713. cursor: pointer;
  714. .avatar {
  715. margin-right: 5px;
  716. width: 25px;
  717. height: 25px;
  718. border-radius: 50%;
  719. @extend %containBg;
  720. }
  721. .username {
  722. color: black;
  723. }
  724. }
  725. }
  726. }
  727. .danmu-list {
  728. overflow-y: scroll;
  729. box-sizing: border-box;
  730. padding-top: 4px;
  731. height: 480px;
  732. background-color: #f6f7f8;
  733. text-align: initial;
  734. @extend %customScrollbar;
  735. .item {
  736. box-sizing: border-box;
  737. margin-bottom: 4px;
  738. padding: 2px 10px;
  739. white-space: normal;
  740. word-wrap: break-word;
  741. font-size: 13px;
  742. .name {
  743. color: #9499a0;
  744. cursor: pointer;
  745. &.system {
  746. color: red;
  747. }
  748. }
  749. .msg {
  750. margin-top: 4px;
  751. color: #61666d;
  752. &.img {
  753. img {
  754. width: 80%;
  755. }
  756. }
  757. }
  758. }
  759. }
  760. .send-msg {
  761. position: relative;
  762. box-sizing: border-box;
  763. padding: 4px 10px;
  764. width: 100%;
  765. .control {
  766. display: flex;
  767. margin: 4px 0;
  768. .ico {
  769. margin-right: 6px;
  770. width: 24px;
  771. height: 24px;
  772. cursor: pointer;
  773. .input-upload {
  774. width: 0;
  775. height: 0;
  776. opacity: 0;
  777. }
  778. &.face {
  779. @include setBackground('@/assets/img/msg-face.webp');
  780. }
  781. &.img {
  782. @include setBackground('@/assets/img/msg-img.webp');
  783. }
  784. }
  785. }
  786. .ipt {
  787. display: block;
  788. box-sizing: border-box;
  789. margin: 0 auto;
  790. padding: 10px;
  791. width: 100%;
  792. height: 60px;
  793. outline: none;
  794. border: 1px solid hsla(0, 0%, 60%, 0.2);
  795. border-radius: 4px;
  796. background-color: #f1f2f3;
  797. font-size: 14px;
  798. }
  799. .btn {
  800. box-sizing: border-box;
  801. margin-top: 10px;
  802. margin-left: auto;
  803. padding: 4px;
  804. width: 70px;
  805. border-radius: 4px;
  806. background-color: $theme-color-gold;
  807. color: white;
  808. text-align: center;
  809. font-size: 12px;
  810. cursor: pointer;
  811. }
  812. }
  813. }
  814. }
  815. // 屏幕宽度大于1500的时候
  816. @media screen and (min-width: $w-1500) {
  817. .pull-wrap {
  818. width: $w-1350;
  819. .left {
  820. width: $w-1000;
  821. }
  822. .right {
  823. width: $w-300;
  824. }
  825. }
  826. }
  827. </style>