瀏覽代碼

fix: 全面优化

shuisheng 2 年之前
父節點
當前提交
c24ad4b68d

+ 21 - 18
billd-live.drawio

@@ -1,38 +1,41 @@
 <mxfile host="65bd71144e">
     <diagram id="dqmK7txivQjgTJvqpF2s" name="第 1 页">
-        <mxGraphModel dx="961" dy="602" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
+        <mxGraphModel dx="1738" dy="809" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
             <root>
                 <mxCell id="0"/>
                 <mxCell id="1" parent="0"/>
-                <mxCell id="5" value="推流" style="edgeStyle=none;html=1;entryX=0.442;entryY=0.017;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="2" target="3">
+                <mxCell id="5" value="推流" style="edgeStyle=none;html=1;entryX=0.442;entryY=0.017;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="2" target="3" edge="1">
                     <mxGeometry relative="1" as="geometry"/>
                 </mxCell>
-                <mxCell id="2" value="浏览器" style="whiteSpace=wrap;html=1;" vertex="1" parent="1">
-                    <mxGeometry x="90" y="120" width="120" height="60" as="geometry"/>
+                <mxCell id="2" value="安卓端" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
+                    <mxGeometry x="70" y="80" width="120" height="60" as="geometry"/>
                 </mxCell>
-                <mxCell id="3" value="服务器" style="whiteSpace=wrap;html=1;" vertex="1" parent="1">
-                    <mxGeometry x="180" y="300" width="120" height="60" as="geometry"/>
+                <mxCell id="3" value="流媒体服务器" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
+                    <mxGeometry x="150" y="300" width="120" height="60" as="geometry"/>
                 </mxCell>
-                <mxCell id="6" value="推流" style="edgeStyle=none;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="4" target="3">
+                <mxCell id="6" value="推流" style="edgeStyle=none;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="4" target="3" edge="1">
                     <mxGeometry relative="1" as="geometry"/>
                 </mxCell>
-                <mxCell id="4" value="浏览器" style="whiteSpace=wrap;html=1;" vertex="1" parent="1">
-                    <mxGeometry x="280" y="120" width="120" height="60" as="geometry"/>
+                <mxCell id="4" value="浏览器" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
+                    <mxGeometry x="270" y="80" width="120" height="60" as="geometry"/>
                 </mxCell>
-                <mxCell id="9" value="拉流" style="edgeStyle=none;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="7" target="3">
-                    <mxGeometry relative="1" as="geometry"/>
+                <mxCell id="9" value="拉流" style="edgeStyle=none;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="3" target="7" edge="1">
+                    <mxGeometry relative="1" as="geometry">
+                        <mxPoint x="170" y="350" as="targetPoint"/>
+                    </mxGeometry>
                 </mxCell>
-                <mxCell id="7" value="浏览器" style="whiteSpace=wrap;html=1;" vertex="1" parent="1">
-                    <mxGeometry x="85" y="470" width="120" height="60" as="geometry"/>
+                <mxCell id="7" value="安卓端" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
+                    <mxGeometry x="70" y="510" width="120" height="60" as="geometry"/>
                 </mxCell>
-                <mxCell id="10" value="拉流" style="edgeStyle=none;html=1;" edge="1" parent="1" source="8">
+                <mxCell id="8" value="浏览器" style="whiteSpace=wrap;html=1;" parent="1" vertex="1">
+                    <mxGeometry x="280" y="510" width="120" height="60" as="geometry"/>
+                </mxCell>
+                <mxCell id="13" value="拉流" style="edgeStyle=none;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="3" target="8">
                     <mxGeometry relative="1" as="geometry">
-                        <mxPoint x="240" y="360" as="targetPoint"/>
+                        <mxPoint x="220" y="339.9999999999998" as="sourcePoint"/>
+                        <mxPoint x="140" y="520.0000000000002" as="targetPoint"/>
                     </mxGeometry>
                 </mxCell>
-                <mxCell id="8" value="浏览器" style="whiteSpace=wrap;html=1;" vertex="1" parent="1">
-                    <mxGeometry x="290" y="470" width="120" height="60" as="geometry"/>
-                </mxCell>
             </root>
         </mxGraphModel>
     </diagram>

+ 4 - 4
src/App.vue

@@ -9,22 +9,22 @@ import { isMobile } from 'billd-utils';
 import { GlobalThemeOverrides, NConfigProvider } from 'naive-ui';
 import { onMounted } from 'vue';
 
+import { THEME_COLOR } from '@/constant';
 import { useCheckUpdate } from '@/hooks/use-checkUpdate';
 import { loginMessage } from '@/hooks/use-login';
+import { usePiniaCacheStore } from '@/store/cache';
 import { useUserStore } from '@/store/user';
 import { getLastBuildDate, setLastBuildDate } from '@/utils/localStorage/app';
 import { getToken } from '@/utils/localStorage/user';
 
-import { usePiniaCacheStore } from './store/cache';
-
 const { appInfo } = useCheckUpdate();
 const cacheStore = usePiniaCacheStore();
 const userStore = useUserStore();
 
 const themeOverrides: GlobalThemeOverrides = {
   common: {
-    primaryColor: '#ffd700',
-    primaryColorHover: '#ffd700',
+    primaryColor: THEME_COLOR,
+    primaryColorHover: THEME_COLOR,
   },
 };
 

+ 2 - 1
src/api/user.ts

@@ -1,4 +1,5 @@
-import { IPaging, IUser } from '@/interface';
+import { IPaging } from '@/interface';
+import { IUser } from '@/types/IUser';
 import request from '@/utils/request';
 
 export function fetchQrcodeLogin({ platform, exp }) {

+ 8 - 0
src/api/wsMessage.ts

@@ -0,0 +1,8 @@
+import { IList, IPaging, IWsMessage } from '@/interface';
+import request from '@/utils/request';
+
+export function fetchGetWsMessageList(params: IList<IWsMessage>) {
+  return request.get<IPaging<IWsMessage>>('/ws_message/list', {
+    params,
+  });
+}

+ 0 - 0
src/main.scss → src/assets/main.scss


+ 11 - 7
src/components/Modal/index.vue

@@ -3,7 +3,10 @@
     class="modal-wrap"
     @click.self="maskClose"
   >
-    <div class="container">
+    <div
+      class="container"
+      :style="{ width }"
+    >
       <span class="title">{{ props.title }}</span>
       <i
         v-if="!hiddenClose"
@@ -36,8 +39,11 @@ const props = withDefaults(
     hiddenClose?: Boolean;
     maskClosable?: Boolean;
     title?: string;
+    width?: string;
   }>(),
-  {}
+  {
+    width: '320px',
+  }
 );
 
 function maskClose() {
@@ -61,7 +67,6 @@ const emits = defineEmits(['close']);
     left: 50%;
     box-sizing: border-box;
     padding: 20px;
-    width: 320px;
     border-radius: 10px;
     background-color: #fff;
     font-size: 14px;
@@ -81,17 +86,16 @@ const emits = defineEmits(['close']);
       @include cross(#ccc, 3px);
     }
     .content {
-      margin: 10px 0;
+      margin: 20px 0;
     }
     .footer {
       .btn {
         width: 280px;
         height: 44px;
         border-radius: 100px;
-        background: linear-gradient(270deg, #929dff 4.71%, #3cfdfd 103.24%),
-          linear-gradient(270deg, #b3acff 4.71%, #3ccffd 103.24%),
-          linear-gradient(270deg, #b3acff 4.71%, #3ccffd 103.24%);
+        background: $theme-color-gold;
         color: white;
+        text-align: center;
         font-weight: 600;
         font-size: 16px;
         line-height: 44px;

+ 2 - 1
src/components/VideoControls/index.vue

@@ -119,9 +119,10 @@ import {
 import { debounce } from 'billd-utils';
 import { ref } from 'vue';
 
-import { LiveLineEnum, LiveRoomTypeEnum } from '@/interface';
+import { LiveLineEnum } from '@/interface';
 import { useAppStore } from '@/store/app';
 import { usePiniaCacheStore } from '@/store/cache';
+import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
 
 withDefaults(
   defineProps<{

+ 4 - 8
src/constant.ts

@@ -8,28 +8,22 @@ export const WECHAT_GZH_APPID = 'wxbd243c01ac5ad1b7'; // 公众号
 export const WECHAT_GZH_OAUTH_URL =
   'https://open.weixin.qq.com/connect/oauth2/authorize?';
 
-export const WECHAT_REDIRECT_URI = `http://www.hfgmupw.cn/oauth/wechat_login`;
+export const WECHAT_REDIRECT_URI = `https://live.hsslive.cn/oauth/wechat_login`;
 
-export const QRCODE_LOGIN_URI = `http://www.hfgmupw.cn/qrcodeLogin`;
+export const QRCODE_LOGIN_URI = `https://live.hsslive.cn/qrcodeLogin`;
 
 export const AUTHOR_GITHUB = 'https://github.com/galaxy-s10';
 
-// wss://srs-pull.hsslive.cn
-// ws://www.hfgmupw.cn
 export const WEBSOCKET_URL =
   process.env.NODE_ENV === 'development'
     ? 'ws://localhost:4300'
     : 'wss://srs-pull.hsslive.cn';
 
-// https://live-api.hsslive.cn
-// http://www.hfgmupw.cn/api/
 export const AXIOS_BASEURL =
   process.env.NODE_ENV === 'development'
     ? '/api'
     : 'https://live-api.hsslive.cn';
 
-// .hsslive.cn
-// .hfgmupw.cn
 export const COOKIE_DOMAIN =
   process.env.NODE_ENV === 'development' ? undefined : '.hsslive.cn';
 
@@ -43,6 +37,8 @@ export const COMMON_URL = {
   payCoursesArticle: 'https://www.hsslive.cn/article/151',
 };
 
+export const THEME_COLOR = '#ffd700';
+
 export const SRS_CB_URL_PARAMS = {
   publishKey: 'pushkey',
   publishType: 'pushtype',

+ 17 - 5
src/hooks/modal/index.vue

@@ -7,8 +7,10 @@
       :title="title"
       :mask-closable="maskClosable"
       @close="handleCancel()"
+      :width="width"
     >
-      {{ msg }}
+      <div ref="domRef"></div>
+      <template v-if="typeof content === 'string'">{{ content }}</template>
       <template #footer>
         <div class="footer">
           <div
@@ -31,7 +33,7 @@
 </template>
 
 <script lang="ts">
-import { defineComponent, ref } from 'vue';
+import { VNode, defineComponent, ref, render, watch } from 'vue';
 
 import ModalCpt from '@/components/Modal/index.vue';
 
@@ -39,12 +41,20 @@ export default defineComponent({
   components: { ModalCpt },
   emits: ['ok', 'cancel'],
   setup() {
-    const title = ref('提示');
-    const msg = ref('');
+    const title = ref('');
+    const width = ref('320px');
+    const content = ref<string | VNode>('');
     const show = ref(false);
     const hiddenCancel = ref(false);
     const hiddenClose = ref(false);
     const maskClosable = ref(true);
+    const domRef = ref();
+    watch([() => show.value, () => domRef.value], ([val1, val2]) => {
+      if (!val1 || !val2) return;
+      if (typeof content.value !== 'string') {
+        render(content.value, domRef.value);
+      }
+    });
     function handleCancel(cb?) {
       cb?.();
     }
@@ -53,11 +63,13 @@ export default defineComponent({
     }
     return {
       title,
-      msg,
+      width,
+      content,
       show,
       hiddenCancel,
       hiddenClose,
       maskClosable,
+      domRef,
       handleCancel,
       handleOk,
     };

+ 8 - 16
src/hooks/use-pull.ts

@@ -6,16 +6,15 @@ import { useFlvPlay, useHlsPlay } from '@/hooks/use-play';
 import { useWebsocket } from '@/hooks/use-websocket';
 import {
   DanmuMsgTypeEnum,
-  IDanmu,
-  ILiveRoom,
   LiveLineEnum,
-  LiveRoomTypeEnum,
+  WsMessageMsgIsFileEnum,
 } from '@/interface';
-import { WsMessageType, WsMsgTypeEnum } from '@/interface-ws';
 import { useAppStore } from '@/store/app';
 import { usePiniaCacheStore } from '@/store/cache';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
+import { ILiveRoom, LiveRoomTypeEnum } from '@/types/ILiveRoom';
+import { WsMessageType, WsMsgTypeEnum } from '@/types/websocket';
 import { createVideo, videoToCanvas } from '@/utils';
 
 export function usePull(roomId: string) {
@@ -25,7 +24,7 @@ export function usePull(roomId: string) {
   const appStore = useAppStore();
   const localStream = ref<MediaStream>();
   const danmuStr = ref('');
-  const msgIsFile = ref(false);
+  const msgIsFile = ref<WsMessageMsgIsFileEnum>(WsMessageMsgIsFileEnum.no);
   const autoplayVal = ref(false);
   const videoLoading = ref(false);
   const isPlaying = ref(false);
@@ -409,28 +408,21 @@ export function usePull(roomId: string) {
     const instance = networkStore.wsMap.get(roomId);
     if (!instance) return;
     const requestId = getRandomString(8);
-    const danmu: IDanmu = {
-      request_id: requestId,
-      socket_id: mySocketId.value,
-      userInfo: userStore.userInfo!,
-      msgType: DanmuMsgTypeEnum.danmu,
-      msg: danmuStr.value,
-      msgIsFile: msgIsFile.value,
-      sendMsgTime: +new Date(),
-    };
     const messageData: WsMessageType['data'] = {
+      socket_id: '',
       msg: danmuStr.value,
       msgType: DanmuMsgTypeEnum.danmu,
       live_room_id: Number(roomId),
       msgIsFile: msgIsFile.value,
-      sendMsgTime: +new Date(),
+      send_msg_time: +new Date(),
+      user_agent: navigator.userAgent,
     };
     instance.send({
       requestId,
       msgType: WsMsgTypeEnum.message,
       data: messageData,
     });
-    damuList.value.push(danmu);
+    // damuList.value.push(danmu);
     danmuStr.value = '';
   }
 

+ 19 - 14
src/hooks/use-push.ts

@@ -6,12 +6,13 @@ import {
   fetchCreateUserLiveRoom,
   fetchUserHasLiveRoom,
 } from '@/api/userLiveRoom';
-import { DanmuMsgTypeEnum, ILiveRoom } from '@/interface';
-import { WsMessageType, WsMsgTypeEnum, WsMsrBlobType } from '@/interface-ws';
+import { DanmuMsgTypeEnum, WsMessageMsgIsFileEnum } from '@/interface';
 import { handleMaxFramerate } from '@/network/webRTC';
 import { useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
+import { ILiveRoom } from '@/types/ILiveRoom';
+import { WsMessageType, WsMsgTypeEnum, WsMsrBlobType } from '@/types/websocket';
 import { createVideo, generateBase64 } from '@/utils';
 
 import { commentAuthTip, loginTip } from './use-login';
@@ -31,7 +32,7 @@ export function usePush() {
   const liveRoomInfo = ref<ILiveRoom>();
   const localStream = ref<MediaStream>();
   const videoElArr = ref<HTMLVideoElement[]>([]);
-  const msgIsFile = ref(false);
+  const msgIsFile = ref<WsMessageMsgIsFileEnum>(WsMessageMsgIsFileEnum.no);
 
   const {
     roomLiving,
@@ -153,7 +154,7 @@ export function usePush() {
       if (newVal) {
         const res = await handleUserHasLiveRoom();
         if (!res) {
-          await useTip('你还没有直播间,是否立即开通?');
+          await useTip({ content: '你还没有直播间,是否立即开通?' });
           await handleCreateUserLiveRoom();
         } else {
           roomName.value = liveRoomInfo.value?.name || '';
@@ -205,7 +206,7 @@ export function usePush() {
     if (!loginTip()) return;
     const flag = await handleUserHasLiveRoom();
     if (!flag) {
-      await useTip('你还没有直播间,是否立即开通?');
+      await useTip({ content: '你还没有直播间,是否立即开通?' });
       await handleCreateUserLiveRoom();
       return;
     }
@@ -320,21 +321,25 @@ export function usePush() {
       requestId: getRandomString(8),
       msgType: WsMsgTypeEnum.message,
       data: {
+        socket_id: '',
         msg: danmuStr.value,
         msgType: DanmuMsgTypeEnum.danmu,
         live_room_id: Number(roomId.value),
         msgIsFile: msgIsFile.value,
-        sendMsgTime: +new Date(),
+        send_msg_time: +new Date(),
+        user_agent: navigator.userAgent,
       },
     });
-    damuList.value.push({
-      socket_id: mySocketId.value,
-      msgType: DanmuMsgTypeEnum.danmu,
-      msg: danmuStr.value,
-      userInfo: userStore.userInfo!,
-      msgIsFile: msgIsFile.value,
-      sendMsgTime: +new Date(),
-    });
+    // damuList.value.push({
+    //   user_agent: navigator.userAgent,
+    //   live_room_id: Number(roomId.value),
+    //   socket_id: mySocketId.value,
+    //   msgType: DanmuMsgTypeEnum.danmu,
+    //   msg: danmuStr.value,
+    //   userInfo: userStore.userInfo!,
+    //   msgIsFile: msgIsFile.value,
+    //   send_msg_time: +new Date(),
+    // });
     danmuStr.value = '';
   }
 

+ 13 - 9
src/hooks/use-tip.ts

@@ -1,4 +1,4 @@
-import { ComponentPublicInstance, createApp } from 'vue';
+import { ComponentPublicInstance, VNode, createApp } from 'vue';
 
 import ModalCpt from './modal/index.vue';
 
@@ -10,15 +10,19 @@ const instance: ComponentPublicInstance<InstanceType<typeof ModalCpt>> =
 
 document.body.appendChild(container);
 
-export function useTip(
-  msg: string,
-  hiddenCancel?: boolean,
-  hiddenClose?: boolean
-) {
+export function useTip(data: {
+  title?: string;
+  width?: string;
+  content: string | VNode;
+  hiddenCancel?: boolean;
+  hiddenClose?: boolean;
+}) {
   instance.show = true;
-  instance.msg = msg;
-  instance.hiddenCancel = !!hiddenCancel;
-  instance.hiddenClose = !!hiddenClose;
+  instance.title = data.title || '提示';
+  instance.width = data.width || '320px';
+  instance.content = data.content;
+  instance.hiddenCancel = !!data.hiddenCancel;
+  instance.hiddenClose = !!data.hiddenClose;
   return new Promise((resolve, reject) => {
     instance.handleOk = () => {
       instance.show = false;

+ 93 - 48
src/hooks/use-websocket.ts

@@ -1,18 +1,26 @@
-import { getRandomString } from 'billd-utils';
-import { useDialog } from 'naive-ui';
-import { computed, onUnmounted, ref, watch } from 'vue';
+import { copyToClipBoard, getRandomString } from 'billd-utils';
+import { NButton, useDialog } from 'naive-ui';
+import { computed, h, onUnmounted, ref, watch } from 'vue';
 
 import { fetchRtcV1Publish } from '@/api/srs';
-import { SRS_CB_URL_PARAMS, WEBSOCKET_URL } from '@/constant';
+import { SRS_CB_URL_PARAMS, THEME_COLOR, WEBSOCKET_URL } from '@/constant';
 import {
   DanmuMsgTypeEnum,
-  IDanmu,
   ILiveUser,
-  IUser,
-  LiveRoomTypeEnum,
+  WsMessageMsgIsFileEnum,
 } from '@/interface';
+import { WebRTCClass } from '@/network/webRTC';
+import { WebSocketClass, prettierReceiveWsMsg } from '@/network/webSocket';
+import router, { routerName } from '@/router';
+import { useAppStore } from '@/store/app';
+import { useNetworkStore } from '@/store/network';
+import { useUserStore } from '@/store/user';
+import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
+import { IUser } from '@/types/IUser';
 import {
+  IDanmu,
   WSGetRoomAllUserType,
+  WSLivePkKeyType,
   WsAnswerType,
   WsCandidateType,
   WsConnectStatusEnum,
@@ -27,15 +35,11 @@ import {
   WsOtherJoinType,
   WsRoomLivingType,
   WsStartLiveType,
-} from '@/interface-ws';
-import { WebRTCClass } from '@/network/webRTC';
-import { WebSocketClass, prettierReceiveWsMsg } from '@/network/webSocket';
-import { useAppStore } from '@/store/app';
-import { useNetworkStore } from '@/store/network';
-import { useUserStore } from '@/store/user';
-import { createVideo, formatDownTime } from '@/utils';
+} from '@/types/websocket';
+import { createVideo } from '@/utils';
 
 import { useRTCParams } from './use-rtcParams';
+import { useTip } from './use-tip';
 
 export const useWebsocket = () => {
   const appStore = useAppStore();
@@ -367,8 +371,10 @@ export const useWebsocket = () => {
           api: `/rtc/v1/publish/`,
           clientip: null,
           sdp: offerSdp!.sdp!,
+          // eslint-disable-next-line
           streamurl: `${myLiveRoom.rtmp_url!}?${
             SRS_CB_URL_PARAMS.publishKey
+            // eslint-disable-next-line
           }=${myLiveRoom.key!}&${SRS_CB_URL_PARAMS.publishType}=${
             LiveRoomTypeEnum.user_srs
           }`,
@@ -410,6 +416,41 @@ export const useWebsocket = () => {
       ws.update();
     });
 
+    // 收到livePkKey
+    ws.socketIo.on(WsMsgTypeEnum.livePkKey, (data: WSLivePkKeyType['data']) => {
+      console.log('收到livePkKey', data);
+      const url = router.resolve({
+        name: routerName.pull,
+        params: { roomId: data.live_room_id },
+        query: {
+          pkKey: data.key,
+        },
+      });
+      const pkurl = `${window.location.origin}${url.href}`;
+      useTip({
+        title: '邀请主播加入PK',
+        width: '360px',
+        hiddenCancel: true,
+        content: h('div', [
+          h('div', { style: { marginBottom: '5px' } }, `${pkurl}`),
+          h(
+            NButton,
+            {
+              size: 'small',
+              type: 'primary',
+              color: THEME_COLOR,
+              onClick: () => {
+                copyToClipBoard(pkurl);
+                window.$message.success('复制成功!');
+              },
+            },
+            () => '复制链接' // 用箭头函数返回性能更好。
+          ),
+          h('div', { style: { marginTop: '5px' } }, '注意,有效期:5分钟'),
+        ]),
+      }).catch(() => {});
+    });
+
     // 收到srsOffer
     ws.socketIo.on(
       WsMsgTypeEnum.srsOffer,
@@ -617,13 +658,15 @@ export const useWebsocket = () => {
     ws.socketIo.on(WsMsgTypeEnum.message, (data: WsMessageType) => {
       prettierReceiveWsMsg(WsMsgTypeEnum.message, data);
       damuList.value.push({
+        user_agent: data.data.user_agent,
+        live_room_id: data.data.live_room_id,
         request_id: data.request_id,
         socket_id: data.socket_id,
         msgType: DanmuMsgTypeEnum.danmu,
         msg: data.data.msg,
         userInfo: data.user_info,
         msgIsFile: data.data.msgIsFile,
-        sendMsgTime: data.data.sendMsgTime,
+        send_msg_time: data.data.send_msg_time,
       });
     });
 
@@ -632,35 +675,35 @@ export const useWebsocket = () => {
       WsMsgTypeEnum.disableSpeaking,
       (data: WsDisableSpeakingType['data']) => {
         prettierReceiveWsMsg(WsMsgTypeEnum.disableSpeaking, data);
-        if (data.is_disable_speaking) {
-          window.$message.error('你已被禁言!');
-          appStore.disableSpeaking.set(data.live_room_id, {
-            exp: data.disable_expired_at,
-            label: formatDownTime({
-              startTime: +new Date(),
-              endTime: data.disable_expired_at,
-            }),
-          });
-          clearTimeout(timerObj.value[data.live_room_id]);
-          timerObj.value[data.live_room_id] = setInterval(() => {
-            if (
-              data.disable_expired_at &&
-              +new Date() > data.disable_expired_at
-            ) {
-              clearTimeout(timerObj.value[data.live_room_id]);
-            }
-            appStore.disableSpeaking.set(data.live_room_id, {
-              exp: data.disable_expired_at!,
-              label: formatDownTime({
-                startTime: +new Date(),
-                endTime: data.disable_expired_at!,
-              }),
-            });
-          }, 1000);
-          damuList.value = damuList.value.filter(
-            (v) => v.request_id !== data.request_id
-          );
-        }
+        // if (data.is_disable_speaking) {
+        //   window.$message.error('你已被禁言!');
+        //   appStore.disableSpeaking.set(data.live_room_id, {
+        //     exp: data.disable_expired_at,
+        //     label: formatDownTime({
+        //       startTime: +new Date(),
+        //       endTime: data.disable_expired_at,
+        //     }),
+        //   });
+        //   clearTimeout(timerObj.value[data.live_room_id]);
+        //   timerObj.value[data.live_room_id] = setInterval(() => {
+        //     if (
+        //       data.disable_expired_at &&
+        //       +new Date() > data.disable_expired_at
+        //     ) {
+        //       clearTimeout(timerObj.value[data.live_room_id]);
+        //     }
+        //     appStore.disableSpeaking.set(data.live_room_id, {
+        //       exp: data.disable_expired_at!,
+        //       label: formatDownTime({
+        //         startTime: +new Date(),
+        //         endTime: data.disable_expired_at!,
+        //       }),
+        //     });
+        //   }, 1000);
+        //   damuList.value = damuList.value.filter(
+        //     (v) => v.request_id !== data.request_id
+        //   );
+        // }
         if (data.user_id !== userStore.userInfo?.id && data.disable_ok) {
           window.$message.success('禁言成功!');
         }
@@ -708,13 +751,14 @@ export const useWebsocket = () => {
       // });
       const requestId = getRandomString(8);
       const danmu: IDanmu = {
+        live_room_id: data.live_room.id!,
         request_id: requestId,
         msgType: DanmuMsgTypeEnum.otherJoin,
         socket_id: data.join_socket_id,
         userInfo: data.join_user_info,
-        msgIsFile: false,
+        msgIsFile: WsMessageMsgIsFileEnum.no,
         msg: '',
-        sendMsgTime: +new Date(),
+        send_msg_time: +new Date(),
       };
       damuList.value.push(danmu);
       ws.send<WsGetLiveUserType['data']>({
@@ -758,12 +802,13 @@ export const useWebsocket = () => {
       // );
       // liveUserList.value = res;
       damuList.value.push({
+        live_room_id: Number(roomId.value),
         socket_id: data.socket_id,
         msgType: DanmuMsgTypeEnum.userLeaved,
-        msgIsFile: false,
+        msgIsFile: WsMessageMsgIsFileEnum.no,
         userInfo: data.user_info,
         msg: '',
-        sendMsgTime: +new Date(),
+        send_msg_time: +new Date(),
       });
     });
   }

+ 97 - 155
src/interface.ts

@@ -1,4 +1,9 @@
-/** 这里放项目里面的类型 */
+import {
+  ILiveRoom,
+  LiveRoomIsShowEnum,
+  LiveRoomStatusEnum,
+} from './types/ILiveRoom';
+import { IUser } from './types/IUser';
 
 export interface IQiniuData {
   id?: number;
@@ -15,6 +20,92 @@ export interface IQiniuData {
   qiniu_md5?: string;
 }
 
+export enum WsMessageMsgIsFileEnum {
+  yes,
+  no,
+}
+
+export enum WsMessageMsgIsShowEnum {
+  yes,
+  no,
+}
+
+export enum WsMessageMsgIsVerifyEnum {
+  yes,
+  no,
+}
+
+export interface IWsMessage {
+  id?: number;
+  username?: string;
+  origin_username?: string;
+  content?: string;
+  origin_content?: string;
+  redbag_send_id?: number;
+  live_room_id?: number;
+  user_id?: number;
+  ip?: string;
+  msg_is_file?: WsMessageMsgIsFileEnum;
+  msg_type?: DanmuMsgTypeEnum;
+  user_agent?: string;
+  send_msg_time?: number;
+  is_show?: WsMessageMsgIsShowEnum;
+  is_verify?: WsMessageMsgIsVerifyEnum;
+
+  user?: IUser;
+  redbag_send?: IRedbagSend;
+
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+}
+
+export interface IRedbagSend {
+  id?: number;
+
+  user_id?: number;
+  live_room_id?: number;
+
+  total_amount?: string;
+  remaining_amount?: string;
+  total_nums?: number;
+  remaining_nums?: number;
+  remark?: string;
+
+  /** 用户信息 */
+  user?: IUser;
+  /** 直播间信息 */
+  live_room?: IGoods;
+
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+}
+
+export enum RedbagIsGrantEnum {
+  yes,
+  no,
+}
+
+export interface IRedbagRecv {
+  id?: number;
+
+  user_id?: number;
+  redbag_send_id?: number;
+  amount?: string;
+  remark?: string;
+
+  /** 抢到红包了,是否已发放 */
+  is_grant?: RedbagIsGrantEnum;
+
+  /** 用户信息 */
+  user?: IUser;
+
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+}
+
 export enum LiveLineEnum {
   rtc = 'rtc',
   hls = 'hls',
@@ -80,9 +171,10 @@ export interface IWallet {
   updated_at?: string;
   deleted_at?: string;
 }
+
 export type IList<T> = {
-  nowPage?: string;
-  pageSize?: string;
+  nowPage?: number;
+  pageSize?: number;
   orderBy?: string;
   orderName?: string;
   keyWord?: string;
@@ -195,86 +287,6 @@ export interface IGoods {
   deleted_at?: string;
 }
 
-/** 拉流是否需要鉴权 */
-export enum LiveRoomPullIsShouldAuthEnum {
-  /** 需要鉴权 */
-  yes,
-  /** 不需要鉴权 */
-  no,
-}
-
-/** 是否使用cdn */
-export enum LiveRoomUseCDNEnum {
-  /** 使用cdn */
-  yes,
-  /** 不使用cdn */
-  no,
-}
-
-/** 直播间状态 */
-export enum LiveRoomStatusEnum {
-  /** 正常 */
-  normal,
-  /** 禁用 */
-  disable,
-}
-
-/** 直播间是否显示 */
-export enum LiveRoomIsShowEnum {
-  /** 显示 */
-  yes,
-  /** 不显示 */
-  no,
-}
-
-export interface ILiveRoom {
-  id?: number;
-  /** 直播间名称 */
-  name?: string;
-  /** 直播间简介 */
-  desc?: string;
-  /** 直播间备注 */
-  remark?: string;
-  /** 是否使用cdn */
-  cdn?: LiveRoomUseCDNEnum;
-  /** 拉流是否需要鉴权 */
-  pull_is_should_auth?: LiveRoomPullIsShouldAuthEnum;
-  /** 权重 */
-  weight?: number;
-  /** 推流秘钥 */
-  key?: string;
-  /** 直播间类型 */
-  type?: LiveRoomTypeEnum;
-  /** 开播预览图 */
-  cover_img?: string;
-  /** 直播间背景图 */
-  bg_img?: string;
-  /** 直播间状态 */
-  status?: LiveRoomStatusEnum;
-  /** 直播间是否显示 */
-  is_show?: LiveRoomIsShowEnum;
-
-  /** 用户信息 */
-  user?: IUser;
-  /** 用户信息 */
-  users?: IUser[];
-  /** 分区信息 */
-  area?: IArea;
-  /** 分区信息 */
-  areas?: IArea[];
-  /** 直播信息 */
-  live?: ILive;
-  user_live_room?: IUserLiveRoom & { user: IUser };
-
-  rtmp_url?: string;
-  flv_url?: string;
-  hls_url?: string;
-
-  created_at?: string;
-  updated_at?: string;
-  deleted_at?: string;
-}
-
 export interface IUserLiveRoom {
   id?: number;
   user_id?: number;
@@ -288,22 +300,6 @@ export interface IUserLiveRoom {
   deleted_at?: string;
 }
 
-/** 直播间类型 */
-export enum LiveRoomTypeEnum {
-  /** 系统直播 */
-  system,
-  /** 主播使用webrtc直播 */
-  user_wertc,
-  /** 主播使用srs直播 */
-  user_srs,
-  /** 主播使用obs/ffmpeg直播 */
-  user_obs,
-  /** 主播使用msr直播 */
-  user_msr,
-  /** 主播打pk */
-  user_pk,
-}
-
 export interface BilldHtmlWebpackPluginLog {
   pkgName: string;
   pkgVersion: string;
@@ -349,52 +345,6 @@ export interface IRole {
   c_roles?: number[];
 }
 
-export interface IUser {
-  id?: number;
-  username?: string;
-  password?: string;
-  email?: string;
-  status?: number;
-  avatar?: string;
-  desc?: string;
-  token?: string;
-
-  wallet?: IWallet;
-  live_room?: ILiveRoom;
-  live_rooms?: ILiveRoom[];
-
-  roles?: IRole[];
-  auths?: IAuth[];
-  user_roles?: number[];
-
-  qq_users?: IQqUser[];
-
-  created_at?: string;
-  updated_at?: string;
-  deleted_at?: string;
-}
-
-export interface IQqUser {
-  id?: number;
-  client_id?: number;
-  openid?: string;
-  unionid?: string;
-  username?: string;
-  figureurl?: string;
-  figureurl_1?: string;
-  figureurl_2?: string;
-  figureurl_qq_1?: string;
-  figureurl_qq_2?: string;
-  constellation?: string;
-  gender?: string;
-  city?: string;
-  province?: string;
-  year?: string;
-  created_at?: string;
-  updated_at?: string;
-  deleted_at?: any;
-}
-
 export interface IArea {
   id?: number;
   name?: string;
@@ -477,6 +427,8 @@ export enum DanmuMsgTypeEnum {
   danmu,
   otherJoin,
   userLeaved,
+  system,
+  redbag,
 }
 
 export interface IUpdateJoinInfo {
@@ -501,13 +453,3 @@ export interface ILiveUser {
     userInfo?: IUser;
   };
 }
-
-export interface IDanmu {
-  msgType: DanmuMsgTypeEnum;
-  msg: string;
-  socket_id: string;
-  request_id?: string;
-  userInfo?: IUser;
-  msgIsFile: boolean;
-  sendMsgTime: number;
-}

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

@@ -273,7 +273,7 @@
                 class="item"
                 @click.prevent="handleStartLive(LiveRoomTypeEnum.user_pk)"
               >
-                <div class="txt">打PK</div>
+                <div class="txt">一对一打PK</div>
               </a>
             </div>
           </template>
@@ -328,10 +328,10 @@ import VPIconChevronDown from '@/components/icons/VPIconChevronDown.vue';
 import VPIconExternalLink from '@/components/icons/VPIconExternalLink.vue';
 import { COMMON_URL, MODULE_CONFIG_SWITCH } from '@/constant';
 import { loginTip } from '@/hooks/use-login';
-import { LiveRoomTypeEnum } from '@/interface';
 import { routerName } from '@/router';
 import { useAppStore } from '@/store/app';
 import { useUserStore } from '@/store/user';
+import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
 
 const router = useRouter();
 const userStore = useUserStore();

+ 2 - 2
src/main.ts

@@ -1,7 +1,7 @@
 // 一定要引入webrtc-adapter(约等于垫片,适配safari等其他浏览器)
+import '@/assets/main.scss';
+import '@/utils/showBilldVersion';
 import 'webrtc-adapter';
-import './main.scss';
-import './showBilldVersion';
 // import 'windi.css'; // windicss-webpack-plugin会解析windi.css这个MODULE_ID
 import { createApp } from 'vue';
 

+ 1 - 1
src/network/webRTC.ts

@@ -1,9 +1,9 @@
 import { getRandomString } from 'billd-utils';
 
 import { MediaTypeEnum } from '@/interface';
-import { WsCandidateType, WsMsgTypeEnum } from '@/interface-ws';
 import { AppRootState, useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
+import { WsCandidateType, WsMsgTypeEnum } from '@/types/websocket';
 
 /** 设置分辨率 */
 export async function handleResolutionRatio(data: {

+ 5 - 1
src/network/webSocket.ts

@@ -1,8 +1,12 @@
 import { Socket, io } from 'socket.io-client';
 
-import { IWsFormat, WsConnectStatusEnum, WsMsgTypeEnum } from '@/interface-ws';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
+import {
+  IWsFormat,
+  WsConnectStatusEnum,
+  WsMsgTypeEnum,
+} from '@/types/websocket';
 
 export function prettierReceiveWsMsg(...arg) {
   console.warn('【websocket】收到消息', ...arg);

+ 2 - 1
src/store/app/index.ts

@@ -1,8 +1,9 @@
 import { UploadFileInfo } from 'naive-ui';
 import { defineStore } from 'pinia';
 
-import { ILiveRoom, LiveLineEnum, MediaTypeEnum } from '@/interface';
+import { LiveLineEnum, MediaTypeEnum } from '@/interface';
 import { mobileRouterName } from '@/router';
+import { ILiveRoom } from '@/types/ILiveRoom';
 
 export type AppRootState = {
   play: boolean;

+ 2 - 1
src/store/user/index.ts

@@ -1,7 +1,8 @@
 import { defineStore } from 'pinia';
 
 import { fetchLogin, fetchUserInfo } from '@/api/user';
-import { IAuth, IRole, IUser } from '@/interface';
+import { IAuth, IRole } from '@/interface';
+import { IUser } from '@/types/IUser';
 import cache from '@/utils/cache';
 
 type UserRootState = {

+ 241 - 0
src/types/ILiveRoom.ts

@@ -0,0 +1,241 @@
+import { IArea, ILive, IUserLiveRoom } from '@/interface';
+import { IUser } from '@/types/IUser';
+
+/** 是否使用cdn */
+export enum LiveRoomUseCDNEnum {
+  /** 使用cdn */
+  yes,
+  /** 不使用cdn */
+  no,
+}
+
+/** 直播间类型 */
+export enum LiveRoomTypeEnum {
+  /** 系统直播 */
+  system,
+  /** 主播使用webrtc直播 */
+  user_wertc,
+  /** 主播使用srs直播 */
+  user_srs,
+  /** 主播使用obs/ffmpeg直播 */
+  user_obs,
+  /** 主播使用msr直播 */
+  user_msr,
+  /** 主播打pk */
+  user_pk,
+}
+
+/** 拉流是否需要鉴权 */
+export enum LiveRoomPullIsShouldAuthEnum {
+  /** 需要鉴权 */
+  yes,
+  /** 不需要鉴权 */
+  no,
+}
+
+/** 直播间状态 */
+export enum LiveRoomStatusEnum {
+  /** 正常 */
+  normal,
+  /** 禁用 */
+  disable,
+}
+
+/** 直播间是否显示 */
+export enum LiveRoomIsShowEnum {
+  /** 显示 */
+  yes,
+  /** 不显示 */
+  no,
+}
+
+/** 直播间是否开启聊天 */
+export enum LiveRoomOpenChatEnum {
+  yes,
+  no,
+}
+
+/** 游客能否发言 */
+export enum LiveRoomTouristSendMsgEnum {
+  yes,
+  no,
+}
+
+/** 是否显示弹窗广告 */
+export enum LiveRoomIsShowAdEnum {
+  yes,
+  no,
+}
+
+/** 关闭房间 */
+export enum LiveRoomIsCloseEnum {
+  yes,
+  no,
+}
+
+/** 开启红包 */
+export enum LiveRoomIsShowRedbagEnum {
+  yes,
+  no,
+}
+
+/** 开启签到 */
+export enum LiveRoomIsShowSigninEnum {
+  yes,
+  no,
+}
+
+/** 开启手机看直播 */
+export enum LiveRoomIsShowPhoneLiveEnum {
+  yes,
+  no,
+}
+
+/** 是否显示直播间在线人数 */
+export enum LiveRoomIsShowLiveUserNumsEnum {
+  yes,
+  no,
+}
+
+/** 是否开启聊天审核 */
+export enum LiveRoomMsgVerifyEnum {
+  yes,
+  no,
+}
+
+/** 是否直播间视频底部的广告图 */
+export enum LiveRoomIsShowLiveVideoBottomImgEnum {
+  yes,
+  no,
+}
+
+/** 是否显示公众号二维码 */
+export enum LiveRoomIsShowOfficialAccountEnum {
+  yes,
+  no,
+}
+
+/** 是否开启转盘抽奖 */
+export enum LiveRoomIsShowTurntableEnum {
+  yes,
+  no,
+}
+
+/** 提醒游客登录 */
+export enum LiveRoomTipTouristLoginEnum {
+  /** 关闭,即不提醒游客登录 */
+  close,
+  /** 开启提醒游客登录,游客可关闭 */
+  open,
+  /** 强制游客登录,即游客不可关闭 */
+  force,
+}
+
+export interface ILiveRoom {
+  id?: number;
+  /** 直播间名称 */
+  name?: string;
+  /** 直播间简介 */
+  desc?: string;
+  /** 是否使用cdn */
+  cdn?: LiveRoomUseCDNEnum;
+  /** 拉流是否需要鉴权 */
+  pull_is_should_auth?: LiveRoomPullIsShouldAuthEnum;
+  /** 权重 */
+  weight?: number;
+  /** 推流秘钥 */
+  key?: string;
+  /** 直播间类型 */
+  type?: LiveRoomTypeEnum;
+  /** 开播预览图 */
+  cover_img?: string;
+  /** 直播间背景图 */
+  bg_img?: string;
+  /** 直播间状态 */
+  status?: LiveRoomStatusEnum;
+  /** 直播间是否显示 */
+  is_show?: LiveRoomIsShowEnum;
+  /** 直播间是否开启聊天 */
+  open_chat?: LiveRoomOpenChatEnum;
+  /** 提醒游客登录 */
+  tip_tourist_login?: LiveRoomTipTouristLoginEnum;
+  /** 提醒游客登录间隔 */
+  tip_tourist_login_delay?: number;
+  /** 游客能否发言 */
+  tourist_send_msg?: LiveRoomTouristSendMsgEnum;
+  /** 聊天关键词过滤 */
+  keyword_filter_msg?: string;
+  /** 用户名关键词过滤 */
+  keyword_filter_username?: string;
+  /** 历史消息条数 */
+  history_msg_total?: number;
+  /** 新注册用户不能发言时间(秒) */
+  newuser_send_msg_delay?: number;
+  /** 房间密码 */
+  room_pwd?: string;
+  /** 显示弹窗广告 */
+  is_show_ad?: LiveRoomIsShowAdEnum;
+  /** 弹窗广告图片 */
+  ad_img_url?: string;
+  /** 关闭房间 */
+  is_close?: LiveRoomIsCloseEnum;
+  /** 关闭房间描述 */
+  is_close_desc?: string;
+  /** 发送消息间隔(秒) */
+  send_msg_throttle?: number;
+  /** 公众号二维码 */
+  official_account_img_url?: string;
+  /** 是否显示公众号二维码 */
+  is_show_official_account?: LiveRoomIsShowOfficialAccountEnum;
+  /** 是否开启转盘抽奖 */
+  is_show_turntable?: LiveRoomIsShowTurntableEnum;
+  /** 是否开启红包 */
+  is_show_redbag?: LiveRoomIsShowRedbagEnum;
+  /** 是否开启签到 */
+  is_show_signin?: LiveRoomIsShowSigninEnum;
+  /** 是否开启手机看直播 */
+  is_show_phone_live?: LiveRoomIsShowPhoneLiveEnum;
+  /** 每天转盘抽奖次数 */
+  turntable_num?: number;
+  /** 公告 */
+  announcement_msg?: string;
+  /** 通知 */
+  notice_msg?: string;
+  /** 系统消息 */
+  system_msg?: string;
+  /** 显示直播间在线人数 */
+  is_show_live_user_nums?: LiveRoomIsShowLiveUserNumsEnum;
+  /** 设置直播间最低在线人数 */
+  mock_live_user_nums_min?: number;
+  /** 设置直播间最高在线人数 */
+  mock_live_user_nums_max?: number;
+  /** 直播间最在线人数刷新间隔 */
+  mock_live_user_nums_refresh_delay?: number;
+  /** 聊天审核 */
+  msg_verify?: LiveRoomMsgVerifyEnum;
+  /** 是否直播间视频底部的广告图 */
+  is_show_live_video_bottom_img?: LiveRoomIsShowLiveVideoBottomImgEnum;
+  /** 直播间视频底部的广告图 */
+  live_video_bottom_img?: string;
+  rtmp_url?: string;
+  flv_url?: string;
+  hls_url?: string;
+  /** 直播间备注 */
+  remark?: string;
+
+  /** 用户信息 */
+  user?: IUser;
+  /** 用户信息 */
+  users?: IUser[];
+  /** 分区信息 */
+  area?: IArea;
+  /** 分区信息 */
+  areas?: IArea[];
+  /** 直播信息 */
+  live?: ILive;
+  user_live_room?: IUserLiveRoom & { user: IUser };
+
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+}

+ 167 - 0
src/types/IUser.ts

@@ -0,0 +1,167 @@
+import { IAuth, IRole, IWallet } from '@/interface';
+
+import { ILiveRoom } from './ILiveRoom';
+
+export interface IQqUser {
+  id?: number;
+  client_id?: number;
+  openid?: string;
+  unionid?: string;
+  nickname?: string;
+  figureurl?: string;
+  figureurl_1?: string;
+  figureurl_2?: string;
+  figureurl_qq_1?: string;
+  figureurl_qq_2?: string;
+  constellation?: string;
+  gender?: string;
+  city?: string;
+  province?: string;
+  year?: string;
+  ret?: number;
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+}
+
+export interface IWechatUser {
+  id?: number;
+  appid?: string;
+  openid?: string;
+  nickname?: string;
+  sex?: number;
+  province?: string;
+  city?: string;
+  country?: string;
+  headimgurl?: string;
+  privilege?: string;
+  unionid?: string;
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+}
+
+export interface IEmailUser {
+  id?: number;
+  email?: string;
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: any;
+}
+
+export interface IGithubUser {
+  id?: number;
+  client_id?: string;
+  login?: string;
+  github_id?: number;
+  node_id?: string;
+  avatar_url?: string;
+  gravatar_id?: string;
+  url?: string;
+  html_url?: string;
+  type?: string;
+  site_admin?: string;
+  name?: string;
+  company?: string;
+  blog?: string;
+  location?: string;
+  email?: any;
+  hireable?: any;
+  bio?: string;
+  twitter_username?: any;
+  public_repos?: number;
+  public_gists?: number;
+  followers?: number;
+  following?: number;
+  github_created_at?: string;
+  github_updated_at?: string;
+  private_gists?: number;
+  total_private_repos?: number;
+  owned_private_repos?: number;
+  disk_usage?: number;
+  collaborators?: number;
+  two_factor_authentication?: string;
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: any;
+}
+
+export interface IThirdUser {
+  id?: number;
+  user_id?: number;
+  third_user_id?: number;
+  third_platform?: number;
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+}
+
+/** 用户状态 */
+export enum UserStatusEnum {
+  /** 正常 */
+  normal,
+  /** 禁用 */
+  disable,
+}
+
+export enum UserIsTouristEnum {
+  yes,
+  no,
+}
+
+export enum UserIsRobotEnum {
+  yes,
+  no,
+}
+
+export enum UserCanMsgEnum {
+  yes,
+  no,
+}
+
+export enum UserCanWatchLiveEnum {
+  yes,
+  no,
+}
+
+export enum UserIsKickEnum {
+  yes,
+  no,
+}
+
+export interface IUser {
+  id?: number;
+  username?: string;
+  password?: string;
+  status?: UserStatusEnum;
+  avatar?: string;
+  desc?: string;
+  token?: string;
+  is_tourist?: UserIsTouristEnum;
+  is_robot?: UserIsRobotEnum;
+  can_msg?: UserCanMsgEnum;
+  can_watch_live?: UserCanWatchLiveEnum;
+  is_kick?: UserIsKickEnum;
+  remark?: string;
+  batch_create_user?: IUser[];
+
+  qq_users?: IQqUser[];
+  wechat_users?: IWechatUser[];
+  github_users?: IGithubUser[];
+  email_users?: IEmailUser[];
+  parent_user_id?: number;
+  parent_user_username?: string;
+
+  roles?: IRole[];
+  auths?: IAuth[];
+
+  user_roles?: number[];
+  wallet?: IWallet;
+
+  live_room?: ILiveRoom;
+  live_rooms?: ILiveRoom[];
+
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+}

+ 0 - 0
src/shims-vue.d.ts → src/types/shims-vue.d.ts


+ 66 - 0
src/types/srs.ts

@@ -0,0 +1,66 @@
+export interface IApiV1Streams {
+  code: number;
+  server: string;
+  service: string;
+  pid: string;
+  streams: {
+    id: string;
+    name: string;
+    vhost: string;
+    app: string;
+    tcUrl: string;
+    url: string;
+    live_ms: number;
+    clients: number;
+    frames: number;
+    send_bytes: number;
+    recv_bytes: number;
+    kbps: {
+      recv_30s: number;
+      send_30s: number;
+    };
+    publish: {
+      active: boolean;
+      cid: string;
+    };
+    video: {
+      codec: string;
+      profile: string;
+      level: string;
+      width: number;
+      height: number;
+    };
+    audio: {
+      codec: string;
+      sample_rate: number;
+      channel: number;
+      profile: string;
+    };
+  }[];
+}
+export interface IApiV1Clients {
+  code: number;
+  server: string;
+  service: string;
+  pid: string;
+  clients: {
+    id: string;
+    vhost: string;
+    stream: string;
+    ip: string;
+    pageUrl: string;
+    swfUrl: string;
+    tcUrl: string;
+    url: string;
+    name: string;
+    type: string;
+    publish: boolean;
+    alive: number;
+    send_bytes: number;
+    recv_bytes: number;
+    kbps: {
+      recv_30s: number;
+      send_30s: number;
+    };
+  }[];
+}

+ 32 - 9
src/interface-ws.ts → src/types/websocket.ts

@@ -1,10 +1,10 @@
 import {
   DanmuMsgTypeEnum,
-  ILiveRoom,
   ILiveUser,
-  IUser,
-  LiveRoomTypeEnum,
-} from './interface';
+  WsMessageMsgIsFileEnum,
+} from '@/interface';
+import { ILiveRoom, LiveRoomTypeEnum } from '@/types/ILiveRoom';
+import { IUser } from '@/types/IUser';
 
 /** websocket连接状态 */
 export enum WsConnectStatusEnum {
@@ -44,11 +44,18 @@ export enum WsMsgTypeEnum {
   roomLiving = 'roomLiving',
   /** 房间不在直播 */
   roomNoLive = 'roomNoLive',
+  /** 获取在线用户 */
   getLiveUser = 'getLiveUser',
+  /** 更新加入信息 */
   updateJoinInfo = 'updateJoinInfo',
+  /** 心跳 */
   heartbeat = 'heartbeat',
+  /** 开始直播 */
   startLive = 'startLive',
+  /** 结束直播 */
   endLive = 'endLive',
+  /** 直播pk秘钥 */
+  livePkKey = 'livePkKey',
   /** 主播禁言用户 */
   disableSpeaking = 'disableSpeaking',
   /** 主播踢掉用户 */
@@ -85,6 +92,12 @@ export type WsUpdateJoinInfoType = IWsFormat<{
   rtmp_url?: string;
 }>;
 
+/** 直播pk秘钥 */
+export type WSLivePkKeyType = IWsFormat<{
+  live_room_id: number;
+  key: string;
+}>;
+
 /** 获取在线用户 */
 export type WSGetRoomAllUserType = IWsFormat<{
   liveUser: ILiveUser[];
@@ -106,14 +119,24 @@ export type WsRoomNoLiveType = IWsFormat<{
   live_room: ILiveRoom;
 }>;
 
-/** ws消息 */
-export type WsMessageType = IWsFormat<{
+export interface IDanmu {
   msgType: DanmuMsgTypeEnum;
-  msgIsFile: boolean;
+  msgIsFile: WsMessageMsgIsFileEnum;
   msg: string;
-  sendMsgTime: number;
+  username?: string;
+  user_agent?: string;
+  send_msg_time: number;
   live_room_id: number;
-}>;
+  redbag_send_id?: number;
+  msg_id?: number;
+
+  socket_id: string;
+  request_id?: string;
+  userInfo?: IUser;
+}
+
+/** ws消息 */
+export type WsMessageType = IWsFormat<IDanmu>;
 
 /** 禁言用户 */
 export type WsDisableSpeakingType = IWsFormat<{

+ 0 - 0
src/showBilldVersion.ts → src/utils/showBilldVersion.ts


+ 2 - 1
src/views/account/index.vue

@@ -81,9 +81,10 @@ import { fetchUpdateLiveRoomKey } from '@/api/liveRoom';
 import { fetchUserInfo } from '@/api/user';
 import { DEFAULT_AUTH_INFO, SRS_CB_URL_PARAMS } from '@/constant';
 import { loginTip } from '@/hooks/use-login';
-import { IUser, LiveRoomTypeEnum } from '@/interface';
 import { routerName } from '@/router';
 import { useUserStore } from '@/store/user';
+import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
+import { IUser } from '@/types/IUser';
 import { getLiveRoomPageUrl } from '@/utils';
 
 const newRtmpUrl = ref();

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

@@ -56,13 +56,13 @@ import { onMounted, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { fetchLiveRoomList } from '@/api/area';
+import router, { routerName } from '@/router';
 import {
   ILiveRoom,
   LiveRoomIsShowEnum,
   LiveRoomPullIsShouldAuthEnum,
   LiveRoomUseCDNEnum,
-} from '@/interface';
-import router, { routerName } from '@/router';
+} from '@/types/ILiveRoom';
 
 const liveRoomList = ref<ILiveRoom[]>([]);
 

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

@@ -52,13 +52,13 @@ import { onMounted, ref } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { fetchLiveRoomList } from '@/api/area';
+import router, { routerName } from '@/router';
 import {
   ILiveRoom,
   LiveRoomIsShowEnum,
   LiveRoomPullIsShouldAuthEnum,
   LiveRoomUseCDNEnum,
-} from '@/interface';
-import router, { routerName } from '@/router';
+} from '@/types/ILiveRoom';
 
 const liveRoomList = ref<ILiveRoom[]>([]);
 

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

@@ -78,14 +78,15 @@
 import { onMounted, onUnmounted, ref } from 'vue';
 
 import { fetchAreaLiveRoomList } from '@/api/area';
+import { IArea, IAreaLiveRoom } from '@/interface';
+
+import router, { mobileRouterName, routerName } from '@/router';
 import {
-  IArea,
-  IAreaLiveRoom,
   LiveRoomIsShowEnum,
   LiveRoomPullIsShouldAuthEnum,
   LiveRoomUseCDNEnum,
-} from '@/interface';
-import router, { mobileRouterName, routerName } from '@/router';
+} from '@/types/ILiveRoom';
+
 import { useAppStore } from '@/store/app';
 
 const appStore = useAppStore();

+ 6 - 5
src/views/h5/room/index.vue

@@ -81,7 +81,7 @@
               >
                 <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
                   <span class="time"
-                    >[{{ formatTimeHour(item.sendMsgTime) }}]</span
+                    >[{{ formatTimeHour(item.send_msg_time) }}]</span
                   >
                   <span class="name">
                     <span v-if="item.userInfo">
@@ -102,7 +102,7 @@
                   </span>
                   <span
                     class="msg"
-                    v-if="!item.msgIsFile"
+                    v-if="item.msgIsFile === WsMessageMsgIsFileEnum.no"
                   >
                     {{ item.msg }}
                   </span>
@@ -204,7 +204,7 @@
       <n-button
         type="info"
         size="small"
-        color="#ffd700"
+        :color="THEME_COLOR"
         @click="sendDanmu"
       >
         发送
@@ -220,13 +220,14 @@ import { useRoute } from 'vue-router';
 
 import { fetchFindLiveConfigByKey } from '@/api/liveConfig';
 import { fetchFindLiveRoom } from '@/api/liveRoom';
-import { MODULE_CONFIG_SWITCH } from '@/constant';
+import { MODULE_CONFIG_SWITCH, THEME_COLOR } from '@/constant';
 import { emojiArray } from '@/emoji';
 import { usePull } from '@/hooks/use-pull';
-import { DanmuMsgTypeEnum, LiveRoomTypeEnum } from '@/interface';
+import { DanmuMsgTypeEnum, WsMessageMsgIsFileEnum } from '@/interface';
 import router, { mobileRouterName } from '@/router';
 import { useAppStore } from '@/store/app';
 import { usePiniaCacheStore } from '@/store/cache';
+import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
 import { formatTimeHour } from '@/utils';
 
 const route = useRoute();

+ 4 - 5
src/views/home/index.vue

@@ -204,16 +204,15 @@ import { fetchLiveList } from '@/api/live';
 import { fetchFindLiveConfigByKey } from '@/api/liveConfig';
 import { MODULE_CONFIG_SWITCH, sliderList } from '@/constant';
 import { usePull } from '@/hooks/use-pull';
+import { ILive, LiveLineEnum } from '@/interface';
+import { routerName } from '@/router';
+import { useAppStore } from '@/store/app';
 import {
-  ILive,
-  LiveLineEnum,
   LiveRoomIsShowEnum,
   LiveRoomPullIsShouldAuthEnum,
   LiveRoomTypeEnum,
   LiveRoomUseCDNEnum,
-} from '@/interface';
-import { routerName } from '@/router';
-import { useAppStore } from '@/store/app';
+} from '@/types/ILiveRoom';
 
 const route = useRoute();
 const router = useRouter();

+ 63 - 8
src/views/pull/index.vue

@@ -184,7 +184,7 @@
           class="item"
         >
           <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
-            <span class="time">[{{ formatTimeHour(item.sendMsgTime) }}]</span>
+            <span class="time">[{{ formatTimeHour(item.send_msg_time) }}]</span>
             <span class="name">
               <span
                 v-if="
@@ -249,7 +249,7 @@
             <span>:</span>
             <span
               class="msg"
-              v-if="!item.msgIsFile"
+              v-if="item.msgIsFile === WsMessageMsgIsFileEnum.no"
             >
               {{ item.msg }}
             </span>
@@ -355,17 +355,25 @@ import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { fetchGoodsList } from '@/api/goods';
+import { fetchGetWsMessageList } from '@/api/wsMessage';
 import { MODULE_CONFIG_SWITCH, QINIU_LIVE } from '@/constant';
 import { emojiArray } from '@/emoji';
 import { commentAuthTip, loginTip } from '@/hooks/use-login';
 import { usePull } from '@/hooks/use-pull';
 import { useUpload } from '@/hooks/use-upload';
-import { DanmuMsgTypeEnum, GoodsTypeEnum, IGoods } from '@/interface';
-import { WsDisableSpeakingType, WsMsgTypeEnum } from '@/interface-ws';
+import {
+  DanmuMsgTypeEnum,
+  GoodsTypeEnum,
+  IGoods,
+  WsMessageMsgIsFileEnum,
+  WsMessageMsgIsShowEnum,
+  WsMessageMsgIsVerifyEnum,
+} from '@/interface';
 import router, { routerName } from '@/router';
 import { useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
+import { WsDisableSpeakingType, WsMsgTypeEnum } from '@/types/websocket';
 import { formatTimeHour } from '@/utils';
 import { NODE_ENV } from 'script/constant';
 
@@ -414,6 +422,7 @@ onMounted(() => {
   setTimeout(() => {
     scrollTo(0, 0);
   }, 100);
+  handleHistoryMsg();
   appStore.setPlay(true);
   if (MODULE_CONFIG_SWITCH.pullGiftList) {
     getGoodsList();
@@ -434,6 +443,52 @@ onUnmounted(() => {
   closeRtc();
 });
 
+async function handleHistoryMsg() {
+  try {
+    const res = await fetchGetWsMessageList({
+      nowPage: 1,
+      pageSize: appStore.liveRoomInfo?.history_msg_total || 10,
+      orderName: 'created_at',
+      orderBy: 'desc',
+      live_room_id: Number(roomId.value),
+      is_show: WsMessageMsgIsShowEnum.yes,
+      is_verify: WsMessageMsgIsVerifyEnum.yes,
+    });
+    if (res.code === 200) {
+      res.data.rows.forEach((v) => {
+        damuList.value.unshift({
+          ...v,
+          live_room_id: v.live_room_id!,
+          msg_id: v.id!,
+          socket_id: '',
+          msgType: v.msg_type!,
+          msgIsFile: v.msg_is_file!,
+          userInfo: v.user,
+          msg: v.content!,
+          username: v.username!,
+          send_msg_time: Number(v.send_msg_time),
+          redbag_send_id: v.redbag_send_id,
+        });
+      });
+      if (
+        appStore.liveRoomInfo?.system_msg &&
+        appStore.liveRoomInfo?.system_msg !== ''
+      ) {
+        damuList.value.push({
+          live_room_id: Number(roomId.value),
+          socket_id: '',
+          msgType: DanmuMsgTypeEnum.system,
+          msgIsFile: WsMessageMsgIsFileEnum.no,
+          msg: appStore.liveRoomInfo.system_msg,
+          send_msg_time: Number(+new Date()),
+        });
+      }
+    }
+  } catch (error) {
+    console.log(error);
+  }
+}
+
 async function handleUserMedia({ video, audio }) {
   try {
     const event = await navigator.mediaDevices.getUserMedia({
@@ -593,7 +648,7 @@ async function uploadChange() {
   if (fileList?.length) {
     try {
       msgLoading.value = true;
-      msgIsFile.value = true;
+      msgIsFile.value = WsMessageMsgIsFileEnum.yes;
       const res = await useUpload({
         prefix: QINIU_LIVE.prefix['billd-live/msg-image/'],
         file: fileList[0],
@@ -605,7 +660,7 @@ async function uploadChange() {
     } catch (error) {
       console.log(error);
     } finally {
-      msgIsFile.value = false;
+      msgIsFile.value = WsMessageMsgIsFileEnum.no;
       msgLoading.value = false;
       if (uploadRef.value) {
         uploadRef.value.value = '';
@@ -1044,8 +1099,8 @@ function handleScrollTop() {
           left: 0;
           overflow: scroll;
           box-sizing: border-box;
-          padding-top: 5px;
-          padding-left: 5px;
+          padding: 3px;
+          padding-right: 0;
           height: 160px;
           background-color: #fff;
           transform: translateY(-100%);

+ 62 - 7
src/views/push/index.vue

@@ -221,7 +221,7 @@
                 <span>:</span>
                 <span
                   class="msg"
-                  v-if="!item.msgIsFile"
+                  v-if="item.msgIsFile === WsMessageMsgIsFileEnum.no"
                 >
                   {{ item.msg }}
                 </span>
@@ -260,10 +260,23 @@
           v-loading="msgLoading"
         >
           <div class="control">
+            <div
+              class="emoji-list"
+              v-if="showEmoji"
+            >
+              <div
+                class="item"
+                v-for="(item, index) in emojiArray"
+                :key="index"
+                @click="handlePushStr(item)"
+              >
+                {{ item }}
+              </div>
+            </div>
             <div
               class="ico face"
               title="表情"
-              @click="handleWait"
+              @click="showEmoji = !showEmoji"
             ></div>
             <div
               class="ico img"
@@ -358,15 +371,21 @@ import { useRoute } from 'vue-router';
 import * as workerTimers from 'worker-timers';
 
 import { QINIU_LIVE, mediaTypeEnumMap } from '@/constant';
+import { emojiArray } from '@/emoji';
 import { commentAuthTip, loginTip } from '@/hooks/use-login';
 import { usePush } from '@/hooks/use-push';
 import { useRTCParams } from '@/hooks/use-rtcParams';
 import { useUpload } from '@/hooks/use-upload';
-import { DanmuMsgTypeEnum, LiveRoomTypeEnum, MediaTypeEnum } from '@/interface';
+import {
+  DanmuMsgTypeEnum,
+  MediaTypeEnum,
+  WsMessageMsgIsFileEnum,
+} from '@/interface';
 import { AppRootState, useAppStore } from '@/store/app';
 import { usePiniaCacheStore } from '@/store/cache';
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
+import { LiveRoomTypeEnum } from '@/types/ILiveRoom';
 import {
   createVideo,
   formatDownTime,
@@ -430,6 +449,7 @@ const startTime = ref(+new Date());
 const msgLoading = ref(false);
 const uploadRef = ref<HTMLInputElement>();
 const nullAudioStream = ref<MediaStream>();
+const showEmoji = ref(false);
 
 const timeCanvasDom = ref<Raw<fabric.Text>[]>([]);
 const stopwatchCanvasDom = ref<Raw<fabric.Text>[]>([]);
@@ -510,6 +530,11 @@ watch(
   }
 );
 
+function handlePushStr(str) {
+  danmuStr.value += str;
+  showEmoji.value = false;
+}
+
 function handleScrollTop() {
   if (danmuListRef.value) {
     danmuListRef.value.scrollTop = danmuListRef.value.scrollHeight + 10000;
@@ -563,7 +588,7 @@ async function uploadChange() {
   if (fileList?.length) {
     try {
       msgLoading.value = true;
-      msgIsFile.value = true;
+      msgIsFile.value = WsMessageMsgIsFileEnum.yes;
       const res = await useUpload({
         prefix: QINIU_LIVE.prefix['billd-live/msg-image/'],
         file: fileList[0],
@@ -575,7 +600,7 @@ async function uploadChange() {
     } catch (error) {
       console.log(error);
     } finally {
-      msgIsFile.value = false;
+      msgIsFile.value = WsMessageMsgIsFileEnum.no;
       msgLoading.value = false;
       if (uploadRef.value) {
         uploadRef.value.value = '';
@@ -689,13 +714,17 @@ function clearFrame() {
 }
 
 function renderFrame() {
-  // currentMaxFramerate等于20,实际fps是17.68
   /**
+   * 理论情况:
    * currentMaxFramerate等于20,即每秒20帧,即1000 / 20 = 50毫秒轮询一次
    * currentMaxFramerate等于30,即每秒30帧,即1000 / 30 = 33.333毫秒轮询一次
    * currentMaxFramerate等于30,即每秒60帧,即1000 / 60 = 16.666毫秒轮询一次
+   * 实际情况:
+   * currentMaxFramerate等于20,即50毫秒轮询一次,实际fps是18
+   * currentMaxFramerate等于20,希望fps是20,即需要(18/20) * 50 = 45毫秒轮询一次
    */
-  const delay = Math.floor(1000 / currentMaxFramerate.value); // 60帧的话即16.666666666666668
+  let delay = 1000 / currentMaxFramerate.value;
+  delay = (18 / 20) * delay;
   workerTimerId.value = workerTimers.setInterval(() => {
     renderAll();
   }, delay);
@@ -2189,6 +2218,32 @@ function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
         .control {
           display: flex;
           margin: 4px 0;
+          .emoji-list {
+            position: absolute;
+            top: 0;
+            right: 0;
+            left: 0;
+            overflow: scroll;
+            box-sizing: border-box;
+            padding: 3px;
+            padding-right: 0;
+            height: 160px;
+            background-color: #fff;
+            transform: translateY(-100%);
+
+            @extend %customScrollbar;
+            .item {
+              display: inline-flex;
+              align-items: center;
+              justify-content: center;
+              box-sizing: border-box;
+              width: 14%;
+              height: 18%;
+              border: 1px solid #f8f8f8;
+              font-size: 20px;
+              cursor: pointer;
+            }
+          }
           .ico {
             margin-right: 6px;
             width: 24px;

+ 3 - 6
src/views/rank/index.vue

@@ -105,13 +105,10 @@ import { fetchLiveRoomList } from '@/api/liveRoom';
 import { fetchBlogUserList, fetchUserList } from '@/api/user';
 import { fetchWalletList } from '@/api/wallet';
 import { fullLoading } from '@/components/FullLoading';
-import {
-  ILiveRoom,
-  IUser,
-  LiveRoomIsShowEnum,
-  RankTypeEnum,
-} from '@/interface';
+import { RankTypeEnum } from '@/interface';
 import router, { routerName } from '@/router';
+import { ILiveRoom, LiveRoomIsShowEnum } from '@/types/ILiveRoom';
+import { IUser } from '@/types/IUser';
 
 export interface IRankType {
   type: RankTypeEnum;