瀏覽代碼

feat: 优化

shuisheng 2 年之前
父節點
當前提交
95c82ae197

+ 6 - 0
README.md

@@ -34,6 +34,12 @@ billd 直播间,目前实现了类似 [bilibili 的 Web 在线直播](https://
 - [x] 适配移动端
 - [ ] 在线后台
 
+## 设备兼容
+
+- [x] iphone 14
+- [x] 三星 s10
+- [x] ipad air 3
+
 ## 预览
 
 线上地址:[https://live.hsslive.cn](https://live.hsslive.cn)

+ 2 - 2
script/TerminalPrintPlugin.ts

@@ -19,10 +19,10 @@ class TerminalPrintPlugin {
         `- Network:  ${chalk.cyan(`http://${localIPv4!}:${port}${publicPath}`)}`
       );
       console.log(
-        `- 赞助打赏: ${chalk.cyan(`https://live.hsslive.cn/sponsors`)}`
+        `- 支持赞助: ${chalk.cyan(`https://live.hsslive.cn/support`)}`
       );
       console.log(
-        `- 付费支持: ${chalk.cyan(`https://live.hsslive.cn/support`)}`
+        `- 付费课程: ${chalk.cyan(`https://www.hsslive.cn/article/151`)}`
       );
       console.log(
         `- 欢迎PR:   ${chalk.cyan(

+ 7 - 0
src/App.vue

@@ -28,4 +28,11 @@ onMounted(() => {
 });
 </script>
 
+<style lang="scss">
+#app {
+  font-size: 16px;
+  // naive的message会影响全局line-height
+  line-height: initial;
+}
+</style>
 <style lang="scss" scoped></style>

+ 6 - 1
src/api/live.ts

@@ -1,6 +1,11 @@
 import request from '@/utils/request';
 
-export function fetchLiveList(params: { orderName: string; orderBy: string }) {
+export function fetchLiveList(params: {
+  orderName: string;
+  orderBy: string;
+  nowPage?: number;
+  pageSize?: number;
+}) {
   return request.instance({
     url: '/live/list',
     method: 'get',

+ 1 - 1
src/components/Dropdown/index.vue

@@ -45,7 +45,7 @@ const emits = defineEmits(['update:modelValue']);
     position: absolute;
     top: 100%;
     right: 0;
-    z-index: 2;
+    z-index: 3;
     display: none;
     .wrap {
       box-sizing: border-box;

+ 4 - 5
src/hooks/use-play.ts

@@ -35,9 +35,8 @@ export function useFlvPlay() {
   );
 
   function setMuted(val) {
-    console.log(val, '--------');
     if (flvPlayer.value) {
-      // flvPlayer.value.muted = val;
+      flvPlayer.value.muted = val;
     }
     if (flvVideoEl.value) {
       flvVideoEl.value.muted = val;
@@ -64,8 +63,8 @@ export function useFlvPlay() {
           });
           flvVideoEl.value.addEventListener('playing', () => {
             console.log('flv-playing', isSafari());
+            // setMuted(false);
             setMuted(appStore.muted);
-
             resolve({
               width: flvVideoEl.value?.videoWidth || 0,
               height: flvVideoEl.value?.videoHeight || 0,
@@ -128,7 +127,7 @@ export function useHlsPlay() {
     destroyHls();
     const videoEl = document.createElement('video');
     videoEl.autoplay = true;
-    videoEl.muted = true;
+    videoEl.muted = appStore.muted;
     videoEl.playsInline = true;
     videoEl.setAttribute('webkit-playsinline', 'true');
     hlsVideoEl.value = videoEl;
@@ -152,7 +151,7 @@ export function useHlsPlay() {
           });
           hlsPlayer.value?.on('playing', () => {
             console.log('hls-playing');
-            appStore.setMuted(false);
+            appStore.setMuted(hlsVideoEl.value?.muted || false);
             // console.log(hlsPlayer.value?.videoHeight()); // 获取到的是正确的!
             const childNodes = hlsPlayer.value?.el().childNodes;
             if (childNodes) {

+ 2 - 0
src/interface.ts

@@ -157,6 +157,8 @@ export interface ILiveRoom {
   live?: ILive;
   user_live_room?: IUserLiveRoom & { user: IUser };
   name?: string;
+  /** 1:使用cdn;2:不使用cdn */
+  cdn?: number;
   /** 权重 */
   weight?: number;
   key?: string;

+ 31 - 0
src/layout/mobile/head/index.vue

@@ -5,6 +5,20 @@
         class="logo"
         @click="router.push({ name: mobileRouterName.h5 })"
       ></div>
+      <a
+        class="github"
+        target="_blank"
+        href="https://github.com/galaxy-s10/billd-live"
+      >
+        <img
+          :src="githubStar"
+          alt=""
+        />
+        <img
+          :src="githubFork"
+          alt=""
+        />
+      </a>
     </div>
     <nav class="nav-list">
       <div
@@ -21,17 +35,27 @@
 </template>
 
 <script lang="ts" setup>
+import { onMounted, ref } from 'vue';
 import { useRoute } from 'vue-router';
 
 import router, { mobileRouterName } from '@/router';
 import { AppRootState, useAppStore } from '@/store/app';
 
+const githubStar = ref();
+const githubFork = ref();
 const route = useRoute();
 const appStore = useAppStore();
 
 function changeRoute(item: AppRootState['navList'][0]) {
   router.push({ name: item.routeName });
 }
+
+onMounted(() => {
+  githubStar.value =
+    'https://img.shields.io/github/stars/galaxy-s10/billd-live?label=Star&logo=GitHub&labelColor=white&logoColor=black&style=social&cacheSeconds=3600';
+  githubFork.value =
+    'https://img.shields.io/github/forks/galaxy-s10/billd-live?label=Fork&logo=GitHub&labelColor=white&logoColor=black&style=social&cacheSeconds=3600';
+});
 </script>
 
 <style lang="scss" scoped>
@@ -40,6 +64,7 @@ function changeRoute(item: AppRootState['navList'][0]) {
     display: flex;
     align-items: center;
     box-sizing: border-box;
+    justify-content: space-between;
     padding: 0 20px;
     height: 40px;
     border-bottom: 1px solid #e7e7e7;
@@ -50,6 +75,12 @@ function changeRoute(item: AppRootState['navList'][0]) {
 
       @include setBackground('@/assets/img/logo-txt.png');
     }
+    .github {
+      display: flex;
+      img {
+        margin-left: 6px;
+      }
+    }
   }
   .nav-list {
     display: flex;

+ 2 - 2
src/layout/pc/index.vue

@@ -17,14 +17,14 @@ import SidebarCpt from './sidebar/index.vue';
 
 <style lang="scss" scoped>
 .layout {
-  min-width: $large-width;
+  // min-width: $large-width;
   min-height: 100vh;
 }
 
 // 屏幕宽度小于$large-width的时候
 @media screen and (max-width: $large-width) {
   .layout {
-    min-width: $medium-width;
+    // min-width: $medium-width;
   }
 }
 </style>

+ 25 - 2
src/views/area/index.vue

@@ -18,6 +18,12 @@
             }')`,
           }"
         >
+          <div
+            v-if="iten?.cdn === 1"
+            class="cdn-ico"
+          >
+            <div class="txt">CDN</div>
+          </div>
           <div class="txt">{{ iten?.users?.[0].username }}</div>
         </div>
         <div class="desc">{{ iten?.name }}</div>
@@ -101,8 +107,8 @@ async function getData() {
     flex-wrap: wrap;
     .live-room {
       display: inline-block;
-      margin-right: 35px;
-      margin-bottom: 10px;
+      margin-right: 25px;
+      margin-bottom: 12px;
       width: 300px;
       cursor: pointer;
       .cover {
@@ -113,6 +119,23 @@ async function getData() {
         border-radius: 8px;
         background-position: center center;
         background-size: cover;
+        .cdn-ico {
+          position: absolute;
+          right: -10px;
+          top: -10px;
+          background-color: #f87c48;
+          color: white;
+          z-index: 2;
+          height: 28px;
+          width: 70px;
+          transform-origin: bottom;
+          transform: rotate(45deg);
+          .txt {
+            margin-left: 18px;
+            font-size: 13px;
+            background-image: initial !important;
+          }
+        }
 
         .txt {
           position: absolute;

+ 27 - 1
src/views/h5/area/index.vue

@@ -16,6 +16,12 @@
             }')`,
           }"
         >
+          <div
+            v-if="iten?.cdn === 1"
+            class="cdn-ico"
+          >
+            <div class="txt">CDN</div>
+          </div>
           <div class="txt">{{ iten?.users?.[0].username }}</div>
         </div>
         <div class="desc">{{ iten?.name }}</div>
@@ -92,6 +98,25 @@ async function getData() {
         border-radius: 8px;
         background-position: center center;
         background-size: cover;
+        .cdn-ico {
+          position: absolute;
+          top: -12px;
+          right: -12px;
+          z-index: 2;
+          width: 70px;
+          height: 22px;
+          background-color: #f87c48;
+          transform: rotate(45deg);
+          transform-origin: bottom;
+
+          .txt {
+            margin-left: 18px;
+            background-image: initial !important;
+            color: white;
+            font-size: 10px;
+            transform: scale(0.83333) translate(2px, 3px);
+          }
+        }
 
         .txt {
           position: absolute;
@@ -114,8 +139,9 @@ async function getData() {
         }
       }
       .desc {
-        font-size: 14px;
         margin-top: 4px;
+        font-size: 14px;
+
         @extend %singleEllipsis;
       }
     }

+ 31 - 4
src/views/h5/index.vue

@@ -41,6 +41,12 @@
                 }')`,
               }"
             >
+              <div
+                v-if="iten.live_room?.cdn === 1"
+                class="cdn-ico"
+              >
+                <div class="txt">CDN</div>
+              </div>
               <div class="txt">{{ iten.live_room?.users?.[0].username }}</div>
             </div>
             <div class="desc">{{ iten.live_room?.name }}</div>
@@ -63,9 +69,6 @@ import { onMounted, ref } from 'vue';
 import { fetchAreaLiveRoomList } from '@/api/area';
 import { IArea, IAreaLiveRoom, liveTypeEnum } from '@/interface';
 import router, { mobileRouterName, routerName } from '@/router';
-import { useAppStore } from '@/store/app';
-
-const appStore = useAppStore();
 
 const navList = ref([
   { id: 1, name: '频道' },
@@ -73,7 +76,6 @@ const navList = ref([
   { id: 3, name: '我的' },
 ]);
 
-const currentNav = ref(navList.value[0]);
 const liveRoomList = ref<IArea[]>([]);
 
 const swiperList = ref([
@@ -107,6 +109,10 @@ function showAll(item: IArea) {
 }
 
 function goRoom(item: IAreaLiveRoom) {
+  if (!item.live_room?.live) {
+    window.$message.info('该直播间没在直播~');
+    return;
+  }
   router.push({
     name: routerName.h5Room,
     params: { roomId: item.live_room_id },
@@ -205,6 +211,8 @@ onMounted(() => {
           display: inline-block;
           margin-bottom: 10px;
           width: 48%;
+          height: 130px;
+
           .cover {
             position: relative;
             overflow: hidden;
@@ -213,6 +221,25 @@ onMounted(() => {
             border-radius: 8px;
             background-position: center center;
             background-size: cover;
+            .cdn-ico {
+              position: absolute;
+              top: -12px;
+              right: -12px;
+              z-index: 2;
+              width: 70px;
+              height: 22px;
+              background-color: #f87c48;
+              transform: rotate(45deg);
+              transform-origin: bottom;
+
+              .txt {
+                margin-left: 18px;
+                background-image: initial !important;
+                color: white;
+                font-size: 10px;
+                transform: scale(0.83333) translate(2px, 3px);
+              }
+            }
 
             .txt {
               position: absolute;

+ 3 - 2
src/views/h5/room/index.vue

@@ -25,12 +25,12 @@
       v-loading="videoLoading"
       class="video-wrap"
     >
-      <!-- <div
+      <div
         class="cover"
         :style="{
           backgroundImage: `url(${liveRoomInfo?.cover_img})`,
         }"
-      ></div> -->
+      ></div>
       <!-- <video
         ref="canvasRef"
         autoplay
@@ -199,6 +199,7 @@ async function startPull() {
 }
 
 onMounted(() => {
+  scrollTo({ top: 0 });
   getLiveRoomInfo();
   initPull(false);
   if (containerRef.value && bottomRef.value) {

+ 164 - 14
src/views/home/index.vue

@@ -1,8 +1,14 @@
 <template>
   <div class="home-wrap">
     <div class="banner"></div>
-    <div class="container">
+    <div class="play-container">
       <div class="left">
+        <div
+          v-if="currentLiveRoom?.live_room?.cdn === 1"
+          class="cdn-ico"
+        >
+          <div class="txt">CDN</div>
+        </div>
         <div
           class="cover"
           :style="{
@@ -63,11 +69,11 @@
       </div>
       <div class="right">
         <div
-          v-if="liveRoomList.length"
+          v-if="topLiveRoomList.length"
           class="list"
         >
           <div
-            v-for="(item, index) in liveRoomList"
+            v-for="(item, index) in topLiveRoomList"
             :key="index"
             :class="{
               item: 1,
@@ -102,6 +108,43 @@
         </div>
       </div>
     </div>
+    <div class="area-container">
+      <div class="area-item">
+        <div class="title">推荐直播</div>
+        <div class="live-room-list">
+          <div
+            v-for="(iten, indey) in otherLiveRoomList"
+            :key="indey"
+            class="live-room"
+            @click="goRoom(iten.live_room)"
+          >
+            <div
+              class="cover"
+              :style="{
+                backgroundImage: `url('${
+                  iten?.live_room?.cover_img || iten?.user?.avatar
+                }')`,
+              }"
+            >
+              <div
+                v-if="iten?.live_room?.cdn === 1"
+                class="cdn-ico"
+              >
+                <div class="txt">CDN</div>
+              </div>
+              <div class="txt">{{ iten?.user?.username }}</div>
+            </div>
+            <div class="desc">{{ iten?.live_room?.name }}</div>
+          </div>
+          <div
+            v-if="!otherLiveRoomList.length"
+            class="null"
+          >
+            暂无数据
+          </div>
+        </div>
+      </div>
+    </div>
 
     <div class="foot">*部分内容来源网络,如有侵权,请联系我删除~</div>
   </div>
@@ -114,20 +157,19 @@ import { useRouter } from 'vue-router';
 
 import { fetchLiveList } from '@/api/live';
 import { flvJs, useFlvPlay, useHlsPlay } from '@/hooks/use-play';
-import { ILive, LiveRoomTypeEnum, liveTypeEnum } from '@/interface';
+import { ILive, ILiveRoom, LiveRoomTypeEnum, liveTypeEnum } from '@/interface';
 import { routerName } from '@/router';
-import { useAppStore } from '@/store/app';
 import { videoToCanvas } from '@/utils';
 
 const canvasRef = ref<Element>();
-const appStore = useAppStore();
 const router = useRouter();
 const showControls = ref(false);
-const liveRoomList = ref<ILive[]>([]);
+const topLiveRoomList = ref<ILive[]>([]);
+const otherLiveRoomList = ref<ILive[]>([]);
 const currentLiveRoom = ref<ILive>();
 
 const { flvVideoEl, startFlvPlay, destroyFlv } = useFlvPlay();
-const { hlsVideoEl, startHlsPlay, destroyHls } = useHlsPlay();
+const { startHlsPlay, destroyHls } = useHlsPlay();
 
 async function changeLiveRoom(item: ILive) {
   if (item.id === currentLiveRoom.value?.id) return;
@@ -168,9 +210,11 @@ async function getLiveRoomList() {
       orderBy: 'desc',
     });
     if (res.code === 200) {
-      liveRoomList.value = res.data.rows;
+      const top = 6;
+      topLiveRoomList.value = res.data.rows.slice(0, top);
+      otherLiveRoomList.value = res.data.rows.slice(top);
       if (res.data.total) {
-        currentLiveRoom.value = res.data.rows[0];
+        currentLiveRoom.value = topLiveRoomList.value[0];
         nextTick(async () => {
           if (
             currentLiveRoom.value?.live_room?.type ===
@@ -209,7 +253,7 @@ onMounted(() => {
   getLiveRoomList();
 });
 
-async function joinRoom() {
+function joinRoom() {
   if (currentLiveRoom.value?.live_room?.type === LiveRoomTypeEnum.user_srs) {
     router.push({
       name: routerName.pull,
@@ -233,6 +277,16 @@ async function joinRoom() {
   }
 }
 
+function goRoom(item: ILiveRoom) {
+  router.push({
+    name: routerName.pull,
+    params: { roomId: item.id },
+    query: {
+      liveType: liveTypeEnum.srsFlvPull,
+    },
+  });
+}
+
 function joinFlvRoom() {
   router.push({
     name: routerName.pull,
@@ -256,11 +310,15 @@ function joinHlsRoom() {
 
 <style lang="scss" scoped>
 .home-wrap {
-  .container {
+  .play-container {
     padding: 20px 0;
-    min-width: $large-width;
+    // min-width: $large-width;
     background-color: papayawhip;
     text-align: center;
+    white-space: nowrap;
+    &.area {
+      text-align: initial;
+    }
     .left {
       position: relative;
       display: inline-block;
@@ -274,6 +332,25 @@ function joinHlsRoom() {
 
       @extend %coverBg;
 
+      .cdn-ico {
+        position: absolute;
+        right: -10px;
+        top: -9px;
+        background-color: #f87c48;
+        color: white;
+        z-index: 2;
+        height: 32px;
+        width: 70px;
+        transform-origin: bottom;
+        transform: rotate(45deg);
+        .txt {
+          margin-top: 11px;
+          margin-left: 2px;
+          font-size: 14px;
+          background-image: initial !important;
+        }
+      }
+
       .cover {
         position: absolute;
         background-position: center center;
@@ -436,6 +513,76 @@ function joinHlsRoom() {
       }
     }
   }
+  .area-container {
+    width: 1336px;
+    margin: 10px auto;
+    .area-item {
+      .title {
+        padding: 10px 0;
+        font-size: 26px;
+      }
+      .live-room {
+        display: inline-block;
+        margin-right: 32px;
+        margin-bottom: 10px;
+        width: 300px;
+        cursor: pointer;
+
+        .cover {
+          position: relative;
+          overflow: hidden;
+          width: 100%;
+          height: 150px;
+          border-radius: 8px;
+          background-position: center center;
+          background-size: cover;
+          .cdn-ico {
+            position: absolute;
+            right: -10px;
+            top: -10px;
+            background-color: #f87c48;
+            color: white;
+            z-index: 2;
+            height: 28px;
+            width: 70px;
+            transform-origin: bottom;
+            transform: rotate(45deg);
+            .txt {
+              margin-left: 18px;
+              font-size: 13px;
+              background-image: initial !important;
+            }
+          }
+
+          .txt {
+            position: absolute;
+            bottom: 0;
+            left: 0;
+            box-sizing: border-box;
+            padding: 4px 8px;
+            width: 100%;
+            border-radius: 0 0 4px 4px;
+            background-image: linear-gradient(
+              -180deg,
+              rgba(0, 0, 0, 0),
+              rgba(0, 0, 0, 0.6)
+            );
+            color: white;
+            text-align: initial;
+            font-size: 13px;
+
+            @extend %singleEllipsis;
+          }
+        }
+        .desc {
+          margin-top: 4px;
+          font-size: 14px;
+
+          @extend %singleEllipsis;
+        }
+      }
+    }
+  }
   .foot {
     margin-top: 10px;
     text-align: center;
@@ -445,7 +592,7 @@ function joinHlsRoom() {
 // 屏幕宽度小于$large-width的时候
 @media screen and (max-width: $large-width) {
   .home-wrap {
-    .container {
+    .play-container {
       .left {
         width: $medium-left-width;
         height: 460px;
@@ -461,6 +608,9 @@ function joinHlsRoom() {
         }
       }
     }
+    .area-container {
+      width: 1085px;
+    }
   }
 }
 </style>

+ 5 - 5
src/views/pull/index.vue

@@ -30,16 +30,16 @@
             v-loading="videoLoading"
             class="video-wrap"
           >
-            <!-- <div
+            <div
               class="cover"
               :style="{
                 backgroundImage: `url(${
                   coverImg || currentLiveRoom?.user?.avatar
                 })`,
               }"
-            ></div> -->
+            ></div>
             <div ref="canvasRef"></div>
-            <div
+            <!-- <div
               v-if="
                 route.query.liveType === liveTypeEnum.srsHlsPull &&
                 hlsurl &&
@@ -49,7 +49,7 @@
               @click="startPull"
             >
               点击播放
-            </div>
+            </div> -->
             <VideoControls></VideoControls>
           </div>
 
@@ -361,7 +361,7 @@ onMounted(() => {
     containerRef.value.style.height = `${res}px`;
   }
   if (route.query.liveType === liveTypeEnum.srsHlsPull) {
-    initPull(false);
+    initPull();
   } else {
     initPull();
   }