Explorar o código

feat: 优化移动端

shuisheng %!s(int64=2) %!d(string=hai) anos
pai
achega
f0e58e3970

+ 0 - 7
src/App.vue

@@ -3,11 +3,9 @@
 </template>
 
 <script lang="ts" setup>
-import { isMobile } from 'billd-utils';
 import { onMounted } from 'vue';
 
 import { loginMessage } from '@/hooks/use-login';
-import router, { routerName } from '@/router';
 import { useUserStore } from '@/store/user';
 import cache from '@/utils/cache';
 
@@ -20,11 +18,6 @@ onMounted(() => {
     userStore.setToken(token);
     userStore.getUserInfo();
   }
-  if (isMobile()) {
-    router.push({ name: routerName.h5 });
-  } else {
-    router.push({ name: routerName.home });
-  }
   // 启用vconsole
   // import('vconsole')
   //   .then((VConsole) => {

+ 7 - 0
src/api/liveRoom.ts

@@ -17,3 +17,10 @@ export function fetchUpdateLiveRoomKey() {
     method: 'put',
   });
 }
+
+export function fetchFindLiveRoom(roomId: string) {
+  return request.instance({
+    url: `/live_room/find/${roomId}`,
+    method: 'get',
+  });
+}

BIN=BIN
src/assets/img/logo-txt.png


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 13
src/assets/img/logo-txt.svg


BIN=BIN
src/assets/img/logo.webp


+ 10 - 2
src/hooks/use-pull.ts

@@ -60,6 +60,7 @@ export function usePull({
   const balance = ref('0.00');
   const damuList = ref<IDanmu[]>([]);
   const liveUserList = ref<ILiveUser[]>([]);
+  const autoplayVal = ref(false);
   const videoLoading = ref(false);
   const isDone = ref(false);
   const roomNoLive = ref(false);
@@ -174,8 +175,11 @@ export function usePull({
     }
   );
 
-  function initPull() {
-    videoLoading.value = true;
+  function initPull(autolay = true) {
+    autoplayVal.value = autolay;
+    if (autoplayVal.value) {
+      videoLoading.value = true;
+    }
     console.warn('开始new WebSocketClass');
     const ws = new WebSocketClass({
       roomId: roomId.value,
@@ -468,11 +472,13 @@ export function usePull({
         if (route.query.liveType === liveTypeEnum.srsWebrtcPull) {
           instance.send({ msgType: WsMsgTypeEnum.getLiveUser });
         } else if (route.query.liveType === liveTypeEnum.srsFlvPull) {
+          if (!autoplayVal.value) return;
           await startFlvPlay({
             flvurl: flvurl.value,
             videoEl: remoteVideoRef.value!,
           });
         } else if (route.query.liveType === liveTypeEnum.srsHlsPull) {
+          if (!autoplayVal.value) return;
           await startHlsPlay({
             hlsurl: hlsurl.value,
             videoEl: remoteVideoRef.value!,
@@ -483,12 +489,14 @@ export function usePull({
           data.data.live_room?.type === LiveRoomTypeEnum.system
         ) {
           if (judgeDevice().isIphone) {
+            if (!autoplayVal.value) return;
             await startHlsPlay({
               hlsurl: flvurl.value,
               videoEl: remoteVideoRef.value!,
             });
             videoLoading.value = false;
           } else {
+            if (!autoplayVal.value) return;
             await startFlvPlay({
               flvurl: flvurl.value,
               videoEl: remoteVideoRef.value!,

+ 33 - 2
src/router/index.ts

@@ -1,13 +1,18 @@
+import { isMobile } from 'billd-utils';
 import { createRouter, createWebHistory } from 'vue-router';
 
 import Layout from '@/layout/index.vue';
 
 import type { RouteRecordRaw } from 'vue-router';
 
+export const mobileRouterName = {
+  h5: 'h5',
+  h5Room: 'h5Room',
+};
+
 export const routerName = {
   home: 'home',
   about: 'about',
-  h5: 'h5',
   account: 'account',
   rank: 'rank',
   sponsors: 'sponsors',
@@ -26,6 +31,7 @@ export const routerName = {
 
   pull: 'pull',
   push: 'push',
+  ...mobileRouterName,
 };
 
 // 默认路由
@@ -120,10 +126,15 @@ export const defaultRoutes: RouteRecordRaw[] = [
     ],
   },
   {
-    name: routerName.h5,
+    name: mobileRouterName.h5,
     path: '/h5',
     component: () => import('@/views/h5/index.vue'),
   },
+  {
+    name: mobileRouterName.h5Room,
+    path: '/h5/:roomId',
+    component: () => import('@/views/h5/room/index.vue'),
+  },
   {
     name: routerName.oauth,
     path: '/oauth/:platform',
@@ -142,4 +153,24 @@ const router = createRouter({
   history: createWebHistory(),
 });
 
+router.beforeEach((to, from, next) => {
+  if (to.name === routerName.oauth) {
+    return next();
+  }
+  if (isMobile()) {
+    if (!Object.keys(mobileRouterName).includes(to.name as string)) {
+      // 当前移动端,但是跳转了非移动端路由
+      return next({ name: mobileRouterName.h5 });
+    } else {
+      return next();
+    }
+  } else {
+    if (Object.keys(mobileRouterName).includes(to.name as string)) {
+      // 当前非移动端,但是跳转了移动端路由
+      return next({ name: routerName.home });
+    }
+    return next();
+  }
+});
+
 export default router;

+ 14 - 2
src/views/h5/index.vue

@@ -25,13 +25,14 @@
       >
         <div class="title">
           <div class="left">{{ item.name }}</div>
-          <div class="right">进去看看</div>
+          <div class="right">查看全部</div>
         </div>
         <div class="live-room-list">
           <div
             v-for="(iten, indey) in item.area_live_rooms"
             :key="indey"
             class="live-room"
+            @click="goRoom(iten)"
           >
             <div
               class="cover"
@@ -61,7 +62,8 @@
 import { onMounted, ref } from 'vue';
 
 import { fetchAreaLiveRoomList } from '@/api/area';
-import { IArea } from '@/interface';
+import { IArea, IAreaLiveRoom, liveTypeEnum } from '@/interface';
+import router, { routerName } from '@/router';
 
 const navList = ref([
   { id: 1, name: '频道' },
@@ -86,6 +88,16 @@ async function getLiveRoomList() {
   }
 }
 
+function goRoom(item: IAreaLiveRoom) {
+  router.push({
+    name: routerName.h5Room,
+    params: { roomId: item.live_room_id },
+    query: {
+      liveType: liveTypeEnum.srsHlsPull,
+    },
+  });
+}
+
 onMounted(() => {
   getLiveRoomList();
 });

+ 350 - 0
src/views/h5/room/index.vue

@@ -0,0 +1,350 @@
+<template>
+  <div class="h5-room-wrap">
+    <div class="head">
+      <div class="left">
+        <div
+          class="avatar"
+          :style="{
+            backgroundImage: `url(${liveRoomInfo?.user_live_room?.user.avatar})`,
+          }"
+        ></div>
+        <div class="username">
+          {{ liveRoomInfo?.user_live_room?.user.username }}
+        </div>
+      </div>
+      <div class="right">
+        <div
+          class="btn"
+          @click="router.push({ name: mobileRouterName.h5 })"
+        >
+          返回主页
+        </div>
+      </div>
+    </div>
+    <div
+      v-loading="videoLoading"
+      class="video-wrap"
+    >
+      <div
+        class="cover"
+        :style="{
+          backgroundImage: `url(${liveRoomInfo?.cover_img})`,
+        }"
+      ></div>
+      <video
+        id="remoteVideo"
+        ref="remoteVideoRef"
+        autoplay
+        webkit-playsinline="true"
+        playsinline
+        x-webkit-airplay="allow"
+        x5-video-player-type="h5"
+        x5-video-player-fullscreen="true"
+        x5-video-orientation="portraint"
+        :muted="appStore.muted"
+      ></video>
+      <div
+        v-if="showTip"
+        class="tip-btn"
+        @click="startPull"
+      >
+        点击播放
+      </div>
+      <div class="controls">
+        <VideoControls></VideoControls>
+      </div>
+    </div>
+    <div class="danmu-list">
+      <div class="title">弹幕专区</div>
+      <div
+        ref="containerRef"
+        class="list"
+      >
+        <div
+          v-for="(item, index) in damuList"
+          :key="index"
+          class="item"
+        >
+          <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
+            <span class="name">
+              {{ item.userInfo?.username || item.socket_id }}:
+            </span>
+            <span class="msg">{{ item.msg }}</span>
+          </template>
+          <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
+            <span class="name system">系统通知:</span>
+            <span class="msg">
+              {{ item.userInfo?.username || item.socket_id }}进入直播!
+            </span>
+          </template>
+          <template v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved">
+            <span class="name system">系统通知:</span>
+            <span class="msg">
+              {{ item.userInfo?.username || item.socket_id }}离开直播!
+            </span>
+          </template>
+        </div>
+      </div>
+    </div>
+    <div
+      ref="bottomRef"
+      class="send-msg"
+    >
+      <input
+        v-model="danmuStr"
+        class="ipt"
+        @keydown="keydownDanmu"
+      />
+      <n-button
+        type="info"
+        size="small"
+        @click="sendDanmu"
+      >
+        发送
+      </n-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, ref, watch } from 'vue';
+import { useRoute } from 'vue-router';
+
+import { fetchFindLiveRoom } from '@/api/liveRoom';
+import { useHlsPlay } from '@/hooks/use-play';
+import { usePull } from '@/hooks/use-pull';
+import { DanmuMsgTypeEnum, IGoods, ILiveRoom, liveTypeEnum } from '@/interface';
+import router, { mobileRouterName } from '@/router';
+import { useAppStore } from '@/store/app';
+import { useUserStore } from '@/store/user';
+
+const route = useRoute();
+const userStore = useUserStore();
+const appStore = useAppStore();
+
+const giftGoodsList = ref<IGoods[]>([]);
+const showControls = ref(false);
+const giftLoading = ref(false);
+const showRecharge = ref(false);
+const showJoin = ref(true);
+const showSidebar = ref(true);
+const topRef = ref<HTMLDivElement>();
+const bottomRef = ref<HTMLDivElement>();
+const containerRef = ref<HTMLDivElement>();
+const remoteVideoRef = ref<HTMLVideoElement>();
+const localVideoRef = ref<HTMLVideoElement[]>([]);
+const showTip = ref(true);
+const clickShowTip = ref(false);
+
+const { startHlsPlay, destroyHls } = useHlsPlay();
+
+const {
+  initPull,
+  closeWs,
+  closeRtc,
+  getSocketId,
+  keydownDanmu,
+  sendDanmu,
+  batchSendOffer,
+  startGetUserMedia,
+  startGetDisplayMedia,
+  addTrack,
+  addVideo,
+  videoLoading,
+  balance,
+  roomName,
+  userName,
+  userAvatar,
+  currentLiveRoom,
+  coverImg,
+  roomNoLive,
+  damuList,
+  giftList,
+  liveUserList,
+  danmuStr,
+  localStream,
+  sender,
+  sidebarList,
+} = usePull({
+  localVideoRef,
+  remoteVideoRef,
+  isFlv: route.query.liveType === liveTypeEnum.srsFlvPull,
+  isSRS: route.query.liveType === liveTypeEnum.srsWebrtcPull,
+});
+
+const liveRoomInfo = ref<ILiveRoom>();
+
+watch(
+  () => damuList.value.length,
+  () => {
+    setTimeout(() => {
+      if (containerRef.value) {
+        containerRef.value.scrollTop = containerRef.value.scrollHeight;
+      }
+    }, 0);
+  }
+);
+
+async function getLiveRoomInfo() {
+  const res = await fetchFindLiveRoom(route.params.roomId as string);
+  if (res.code === 200) {
+    liveRoomInfo.value = res.data;
+  }
+}
+
+async function startPull() {
+  showTip.value = false;
+  await startHlsPlay({
+    hlsurl: liveRoomInfo.value!.hls_url!,
+    videoEl: remoteVideoRef.value!,
+  });
+  setTimeout(() => {
+    appStore.setMuted(false);
+  }, 0);
+}
+
+onMounted(() => {
+  getLiveRoomInfo();
+  initPull(false);
+  if (containerRef.value && bottomRef.value) {
+    const res =
+      bottomRef.value.getBoundingClientRect().top -
+      containerRef.value.getBoundingClientRect().top;
+    containerRef.value.style.height = `${res}px`;
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.h5-room-wrap {
+  .head {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    box-sizing: border-box;
+    padding: 0 20px;
+    width: 100%;
+    height: 70px;
+    background-color: black;
+    color: white;
+    .left {
+      display: flex;
+      align-items: center;
+      .avatar {
+        width: 40px;
+        height: 40px;
+        border-radius: 50%;
+
+        @extend %containBg;
+      }
+      .username {
+        margin-left: 10px;
+      }
+    }
+    .right {
+      .btn {
+      }
+    }
+  }
+  .video-wrap {
+    position: relative;
+    overflow: hidden;
+    flex: 1;
+    height: 230px;
+    background-color: rgba($color: #000000, $alpha: 0.5);
+    .cover {
+      position: absolute;
+      background-position: center center;
+      background-size: cover;
+      filter: blur(10px);
+
+      inset: 0;
+    }
+    :deep(video) {
+      position: absolute;
+      top: 0;
+      left: 50%;
+      width: 100%;
+      height: 100%;
+      transform: translate(-50%);
+    }
+    .tip-btn {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      z-index: 1;
+      align-items: center;
+      padding: 10px 20px;
+      border: 2px solid rgba($color: papayawhip, $alpha: 0.5);
+      border-radius: 6px;
+      background-color: rgba(0, 0, 0, 0.3);
+      color: $theme-color-gold;
+      font-size: 14px;
+      cursor: pointer;
+      transform: translate(-50%, -50%);
+      &:hover {
+        background-color: rgba($color: papayawhip, $alpha: 0.5);
+        color: white;
+      }
+    }
+    .controls {
+      display: block;
+    }
+  }
+
+  .danmu-list {
+    box-sizing: border-box;
+    background-color: #0c1622;
+    text-align: initial;
+    padding: 0 15px;
+    .title {
+      padding: 15px 0;
+      color: #fff;
+      font-size: 16px;
+    }
+    .list {
+      overflow-y: scroll;
+      height: 100vh;
+    }
+    .item {
+      margin-bottom: 10px;
+      font-size: 12px;
+      .name {
+        color: #ccc;
+        &.system {
+          color: red;
+        }
+      }
+      .msg {
+        color: #fff;
+      }
+    }
+  }
+  .send-msg {
+    background-color: #0c1622;
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    box-sizing: border-box;
+    padding: 0 10px;
+    width: 100%;
+    height: 40px;
+    .ipt {
+      display: block;
+      box-sizing: border-box;
+      margin-right: 10px;
+      padding: 10px;
+      width: 80%;
+      height: 30px;
+      outline: none;
+      border: 1px solid hsla(0, 0%, 60%, 0.2);
+      border-radius: 4px;
+      background-color: #f1f2f3;
+      font-size: 14px;
+    }
+  }
+}
+</style>

+ 1 - 1
src/views/home/index.vue

@@ -318,7 +318,7 @@ function joinHlsRoom() {
         position: absolute;
         background-position: center center;
         background-size: cover;
-        filter: blur(30px);
+        filter: blur(10px);
 
         inset: 0;
       }

+ 18 - 4
src/views/pull/index.vue

@@ -175,7 +175,10 @@
             </div>
           </div>
         </div>
-        <div class="danmu-list">
+        <div
+          ref="danmuListRef"
+          class="danmu-list"
+        >
           <div
             v-for="(item, index) in damuList"
             :key="index"
@@ -222,7 +225,7 @@
 
 <script lang="ts" setup>
 import { isMobile } from 'billd-utils';
-import { nextTick, onMounted, onUnmounted, ref } from 'vue';
+import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { fetchGoodsList } from '@/api/goods';
@@ -252,6 +255,7 @@ const showJoin = ref(true);
 const showSidebar = ref(true);
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
+const danmuListRef = ref<HTMLDivElement>();
 const containerRef = ref<HTMLDivElement>();
 const remoteVideoRef = ref<HTMLVideoElement>();
 const localVideoRef = ref<HTMLVideoElement[]>([]);
@@ -344,8 +348,18 @@ onUnmounted(() => {
   closeRtc();
 });
 
+watch(
+  () => damuList.value.length,
+  () => {
+    setTimeout(() => {
+      if (danmuListRef.value) {
+        danmuListRef.value.scrollTop = danmuListRef.value.scrollHeight;
+      }
+    }, 0);
+  }
+);
+
 onMounted(() => {
-  console.log(currentLiveRoom.value, 1111);
   getGoodsList();
   if (
     [liveTypeEnum.srsFlvPull, liveTypeEnum.srsWebrtcPull].includes(
@@ -450,7 +464,7 @@ onMounted(() => {
           position: absolute;
           background-position: center center;
           background-size: cover;
-          filter: blur(30px);
+          filter: blur(10px);
 
           inset: 0;
         }

+ 17 - 4
src/views/push/index.vue

@@ -152,7 +152,10 @@
       <div class="danmu-card">
         <div class="title">弹幕互动</div>
         <div class="list-wrap">
-          <div class="list">
+          <div
+            ref="danmuListRef"
+            class="list"
+          >
             <div
               v-for="(item, index) in damuList"
               :key="index"
@@ -203,7 +206,7 @@
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref } from 'vue';
+import { onMounted, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { usePush } from '@/hooks/use-push';
@@ -216,6 +219,7 @@ const userStore = useUserStore();
 const liveType = route.query.liveType;
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
+const danmuListRef = ref<HTMLDivElement>();
 const containerRef = ref<HTMLDivElement>();
 const localVideoRef = ref<HTMLVideoElement>();
 const remoteVideoRef = ref<HTMLVideoElement[]>([]);
@@ -241,7 +245,16 @@ const {
   remoteVideoRef,
   isSRS: liveType === liveTypeEnum.srsPush,
 });
-
+watch(
+  () => damuList.value.length,
+  () => {
+    setTimeout(() => {
+      if (danmuListRef.value) {
+        danmuListRef.value.scrollTop = danmuListRef.value.scrollHeight;
+      }
+    }, 0);
+  }
+);
 onMounted(() => {
   if (topRef.value && bottomRef.value && containerRef.value) {
     const res =
@@ -435,9 +448,9 @@ onMounted(() => {
         margin-bottom: 10px;
       }
       .list {
+        overflow: scroll;
         margin-bottom: 10px;
         height: 300px;
-        overflow: scroll;
 
         .item {
           margin-bottom: 10px;

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio