فهرست منبع

fix: 全面优化

shuisheng 2 سال پیش
والد
کامیت
f9647181f1

+ 1 - 0
package.json

@@ -48,6 +48,7 @@
     "pinia-plugin-persistedstate": "^3.2.0",
     "qrcode": "^1.5.3",
     "socket.io-client": "^4.7.2",
+    "spark-md5": "^3.0.2",
     "video.js": "^8.3.0",
     "vue": "^3.3.4",
     "vue-demi": "^0.13.11",

+ 7 - 0
pnpm-lock.yaml

@@ -53,6 +53,9 @@ dependencies:
   socket.io-client:
     specifier: ^4.7.2
     version: 4.7.2
+  spark-md5:
+    specifier: ^3.0.2
+    version: 3.0.2
   video.js:
     specifier: ^8.3.0
     version: 8.3.0
@@ -9845,6 +9848,10 @@ packages:
     deprecated: Please use @jridgewell/sourcemap-codec instead
     dev: false
 
+  /spark-md5@3.0.2:
+    resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
+    dev: false
+
   /spdx-correct@3.2.0:
     resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
     dependencies:

+ 37 - 0
src/api/liveConfig.ts

@@ -0,0 +1,37 @@
+import { ILiveConfig } from '@/interface';
+import request from '@/utils/request';
+
+export function fetchFrontendList(params) {
+  return request.get('/live_config/list', { params });
+}
+
+export function fetchFindFrontend(id: number) {
+  return request.get(`/live_config/find/${id}`);
+}
+
+export function fetchFindLiveConfigByKey(key: string) {
+  return request.get(`/live_config/find_by_key/${key}`);
+}
+
+export function fetchCreateFrontend(data: ILiveConfig) {
+  return request.instance({
+    url: `/live_config/create`,
+    method: 'post',
+    data,
+  });
+}
+
+export function fetchUpdateFrontend(data: ILiveConfig) {
+  return request.instance({
+    url: `/live_config/update/${data.id!}`,
+    method: 'put',
+    data,
+  });
+}
+
+export function fetchDeleteFrontend(id: number) {
+  return request.instance({
+    url: `/live_config/delete/${id}`,
+    method: 'delete',
+  });
+}

+ 99 - 0
src/api/qiniuData.ts

@@ -0,0 +1,99 @@
+import { IQiniuData } from '@/interface';
+import request from '@/utils/request';
+
+export interface IQiniuKey {
+  prefix: string;
+  hash: string;
+  ext: string;
+}
+
+export function fetchQiniuDataList(params) {
+  return request.instance({
+    url: '/qiniu_data/list',
+    method: 'get',
+    params,
+  });
+}
+export function fetchDiff(params) {
+  return request.instance({
+    url: '/qiniu_data/diff',
+    method: 'get',
+    params,
+  });
+}
+
+// 上传图片
+export function fetchUpload(data: IQiniuKey) {
+  // data:new FormData {prefix,uploadFiles}
+  return request.post<{
+    flag: boolean;
+    respBody?: any;
+    respErr?: any;
+    respInfo?: any;
+    resultUrl?: string;
+  }>('/qiniu_data/upload', data, {
+    timeout: 1000 * 60,
+  });
+}
+
+// 上传chunk
+export function fetchUploadChunk(data) {
+  // data:new FormData {prefix,uploadFiles}
+  return request.post<{ percentage: number }>(
+    '/qiniu_data/upload_chunk',
+    data,
+    {
+      headers: { 'Content-Type': 'multipart/form-data;' },
+      timeout: 1000 * 60,
+    }
+  );
+}
+
+// 合并chunk
+export function fetchUploadMergeChunk(data) {
+  // data:new FormData {prefix,uploadFiles}
+  return request.post('/qiniu_data/merge_chunk', data, {
+    timeout: 1000 * 60,
+  });
+}
+
+// 获取上传图片进度
+export function fetchUploadProgress(params: IQiniuKey) {
+  return request.get<{ percentage?: number }>('/qiniu_data/progress', {
+    timeout: 1000 * 10, // 以免并发轮询获取进度的时候超时
+    params,
+  });
+}
+
+export function fetchCreateLink(data: IQiniuData) {
+  return request.instance({
+    url: '/qiniu_data/create',
+    method: 'post',
+    data,
+  });
+}
+export function fetchUpdateQiniuData(data: IQiniuData) {
+  return request.instance({
+    url: `/qiniu_data/update/${data.id!}`,
+    method: 'put',
+    data,
+  });
+}
+
+export function fetchDeleteQiniuData(id: number) {
+  return request.instance({
+    url: `/qiniu_data/delete/${id}`,
+    method: 'delete',
+  });
+}
+// eslint-disable-next-line camelcase
+export function fetchDeleteQiniuDataByQiniuKey(qiniu_key: string) {
+  return request.instance({
+    url: `/qiniu_data/delete_by_qiniukey`,
+    // delete请求的话,设置params参数都是在地址栏的,因此会将如果参数是数组,会将数组序列化,如http://127.0.0.1:3300/role/batch_delete_child_roles?id=1&c_roles=18&c_roles=2&c_roles=%2733%27
+    method: 'delete',
+    // data: { qiniu_key },
+    // eslint-disable-next-line camelcase
+    params: { qiniu_key }, // 后端的koa-body设置了strict:true,则delete不会解析data数据,因此需要使用params
+  });
+}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 3
src/assets/img/logo-txt.svg


BIN
src/assets/img/msg-face.png


BIN
src/assets/img/msg-img.png


+ 27 - 1
src/constant.ts

@@ -5,13 +5,26 @@ export const QQ_OAUTH_URL = 'https://graph.qq.com/oauth2.0';
 export const QQ_REDIRECT_URI = 'https://live.hsslive.cn/oauth/qq_login';
 
 export const AUTHOR_GITHUB = 'https://github.com/galaxy-s10';
-export const LIVE_CLIENT_URL = 'https://live.hsslive.cn';
 
+// 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';
+
 export const COMMON_URL = {
   apifox:
     'https://apifox.com/apidoc/shared-c7556b54-17b2-494e-a039-572d83f103ed/',
@@ -31,6 +44,19 @@ export const SRS_CB_URL_PARAMS = {
   randomId: 'randomid',
 };
 
+export const QINIU_BLOG = {
+  domain: 'resource.hsslive.cn',
+  url: 'https://resource.hsslive.cn/',
+  bucket: 'hssblog',
+  prefix: {
+    'image/': 'image/',
+    'backupsDatabase/': 'backupsDatabase/',
+    'media/': 'media/',
+    'nuxt-blog-client/': 'nuxt-blog-client/',
+    'billd-live/image/': 'billd-live/image/',
+  },
+};
+
 // 全局的cookie的key
 export const COOKIE_KEY = {
   loginInfo: 'loginInfo',

+ 7 - 4
src/hooks/use-pull.ts

@@ -7,11 +7,10 @@ import {
   DanmuMsgTypeEnum,
   IDanmu,
   ILiveRoom,
-  IMessage,
   LiveLineEnum,
   LiveRoomTypeEnum,
 } from '@/interface';
-import { WsMsgTypeEnum } from '@/interface-ws';
+import { WsMessageType, WsMsgTypeEnum } from '@/interface-ws';
 import { useAppStore } from '@/store/app';
 import { usePiniaCacheStore } from '@/store/cache';
 import { useNetworkStore } from '@/store/network';
@@ -27,6 +26,7 @@ export function usePull() {
   const roomId = ref(route.params.roomId as string);
   const localStream = ref<MediaStream>();
   const danmuStr = ref('');
+  const msgIsFile = ref(false);
   const autoplayVal = ref(false);
   const videoLoading = ref(false);
   const flvurl = ref('');
@@ -364,14 +364,16 @@ export function usePull() {
     if (!instance) return;
     const danmu: IDanmu = {
       socket_id: mySocketId.value,
-      userInfo: userStore.userInfo,
+      userInfo: userStore.userInfo!,
       msgType: DanmuMsgTypeEnum.danmu,
       msg: danmuStr.value,
+      msgIsFile: msgIsFile.value,
     };
-    const messageData: IMessage['data'] = {
+    const messageData: WsMessageType['data'] = {
       msg: danmuStr.value,
       msgType: DanmuMsgTypeEnum.danmu,
       live_room_id: Number(roomId.value),
+      msgIsFile: msgIsFile.value,
     };
     instance.send({
       msgType: WsMsgTypeEnum.message,
@@ -390,6 +392,7 @@ export function usePull() {
     keydownDanmu,
     sendDanmu,
     addVideo,
+    msgIsFile,
     mySocketId,
     videoHeight,
     remoteVideo,

+ 6 - 4
src/hooks/use-push.ts

@@ -6,8 +6,8 @@ import {
   fetchCreateUserLiveRoom,
   fetchUserHasLiveRoom,
 } from '@/api/userLiveRoom';
-import { DanmuMsgTypeEnum, ILiveRoom, IMessage } from '@/interface';
-import { WsMsgTypeEnum, WsMsrBlobType } from '@/interface-ws';
+import { DanmuMsgTypeEnum, ILiveRoom } from '@/interface';
+import { WsMessageType, WsMsgTypeEnum, WsMsrBlobType } from '@/interface-ws';
 import { handleMaxFramerate } from '@/network/webRTC';
 import { useAppStore } from '@/store/app';
 import { useNetworkStore } from '@/store/network';
@@ -322,19 +322,21 @@ export function usePush() {
       window.$message.error('还没开播,不能发送弹幕!');
       return;
     }
-    instance.send<IMessage['data']>({
+    instance.send<WsMessageType['data']>({
       msgType: WsMsgTypeEnum.message,
       data: {
         msg: danmuStr.value,
         msgType: DanmuMsgTypeEnum.danmu,
         live_room_id: Number(roomId.value),
+        msgIsFile: false,
       },
     });
     damuList.value.push({
       socket_id: mySocketId.value,
       msgType: DanmuMsgTypeEnum.danmu,
       msg: danmuStr.value,
-      userInfo: userStore.userInfo,
+      userInfo: userStore.userInfo!,
+      msgIsFile: false,
     });
     danmuStr.value = '';
   }

+ 2 - 0
src/hooks/use-srs-ws.ts

@@ -335,6 +335,7 @@ export const useSrsWs = () => {
         msgType: DanmuMsgTypeEnum.danmu,
         msg: data.data.msg,
         userInfo: data.user_info,
+        msgIsFile: data.data.msgIsFile,
       });
     });
 
@@ -366,6 +367,7 @@ export const useSrsWs = () => {
         msgType: DanmuMsgTypeEnum.otherJoin,
         socket_id: data.join_socket_id,
         userInfo: data.join_user_info,
+        msgIsFile: false,
         msg: '',
       };
       damuList.value.push(danmu);

+ 116 - 0
src/hooks/use-upload.ts

@@ -0,0 +1,116 @@
+import { ref } from 'vue';
+
+import {
+  fetchUpload,
+  fetchUploadChunk,
+  fetchUploadMergeChunk,
+  fetchUploadProgress,
+} from '@/api/qiniuData';
+import { getHash, splitFile } from '@/utils';
+
+export async function useUpload({
+  prefix,
+  file,
+}: {
+  prefix: string;
+  file: File;
+}): Promise<
+  | {
+      flag: boolean;
+      respBody?: any;
+      respErr?: any;
+      respInfo?: any;
+      resultUrl?: string | undefined;
+    }
+  | undefined
+> {
+  const timer = ref();
+  let isMerge = false;
+
+  const mergeAndUpload = async ({ hash, ext, prefix }) => {
+    await fetchUploadMergeChunk({ hash, ext, prefix });
+    const { data } = await fetchUpload({
+      hash,
+      ext,
+      prefix,
+    });
+    clearInterval(timer.value);
+    return data;
+  };
+
+  try {
+    const { hash, ext } = await getHash(file);
+    const { code } = await fetchUploadProgress({ prefix, hash, ext });
+    if (code === 3) {
+      const res = await fetchUpload({ prefix, hash, ext });
+      return new Promise((resolve) => {
+        resolve(res.data);
+      });
+    }
+    const chunkList = splitFile(file);
+    return new Promise<{
+      flag: boolean;
+      respBody?: any;
+      respErr?: any;
+      respInfo?: any;
+      resultUrl?: string | undefined;
+    }>((resolve) => {
+      for (let i = 0; i < chunkList.length; i += 1) {
+        const v = chunkList[i];
+        const form = new FormData();
+        form.append('prefix', prefix);
+        form.append('hash', hash);
+        form.append('ext', ext);
+        form.append('chunkName', v.chunkName);
+        form.append('chunkTotal', `${chunkList.length}`);
+        form.append('uploadFiles', v.chunk);
+        fetchUploadChunk(form).then((res) => {
+          if (res.data.percentage === 50) {
+            if (!isMerge) {
+              mergeAndUpload({ hash, ext, prefix })
+                .then((uploadRes) => {
+                  console.log('mergeAndUpload成功', uploadRes);
+                  resolve(uploadRes);
+                })
+                .catch((err) => {
+                  console.error('mergeAndUpload失败', err);
+                  resolve({ flag: false });
+                });
+              isMerge = true;
+            }
+          }
+        });
+      }
+      let flag = false;
+      timer.value = setInterval(async () => {
+        try {
+          const { code, data, message } = await fetchUploadProgress({
+            hash,
+            prefix,
+            ext,
+          });
+          if (flag) {
+            clearInterval(timer.value);
+            return;
+          }
+          if (code === 1) {
+            const percentage = data.percentage!;
+            if (percentage === 100) {
+              flag = true;
+            }
+          } else {
+            clearInterval(timer.value);
+            console.error(code, data, message);
+          }
+        } catch (error) {
+          console.error(error);
+          clearInterval(timer.value);
+        }
+      }, 1000);
+    });
+  } catch (error) {
+    console.error(error);
+  } finally {
+    clearInterval(timer.value);
+  }
+}

+ 1 - 0
src/interface-ws.ts

@@ -87,6 +87,7 @@ export type WsGetLiveUserType = IWsFormat<{
 
 export type WsMessageType = IWsFormat<{
   msgType: DanmuMsgTypeEnum;
+  msgIsFile: boolean;
   msg: string;
   live_room_id: number;
 }>;

+ 52 - 14
src/interface.ts

@@ -1,5 +1,20 @@
 /** 这里放项目里面的类型 */
 
+export interface IQiniuData {
+  id?: number;
+  user_id?: number;
+  prefix?: string;
+  bucket?: string;
+  qiniu_key?: string;
+  qiniu_hash?: string;
+  qiniu_fsize?: number;
+  qiniu_mimeType?: string;
+  qiniu_putTime?: number;
+  qiniu_type?: number;
+  qiniu_status?: number;
+  qiniu_md5?: string;
+}
+
 export enum LiveLineEnum {
   rtc = 'rtc',
   hls = 'hls',
@@ -84,6 +99,31 @@ export interface IPaging<T> {
   rows: T[];
 }
 
+export enum FormTypeEnum {
+  'input' = 'input',
+  'password' = 'password',
+  'number' = 'number',
+  'select' = 'select',
+  'radio' = 'radio',
+  'checkbox' = 'checkbox',
+  'markdown' = 'markdown',
+  'switch' = 'switch',
+  'upload' = 'upload',
+  'treeSelect' = 'treeSelect',
+  'datePicker' = 'datePicker',
+}
+
+export interface ILiveConfig {
+  id?: number;
+  key?: string;
+  value?: string;
+  desc?: string;
+  type?: FormTypeEnum;
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+}
+
 export interface IOrder {
   id?: number;
   /** 用户信息 */
@@ -264,6 +304,7 @@ export interface IRole {
   role_auths?: number[];
   c_roles?: number[];
 }
+
 export interface IUser {
   id?: number;
   username?: string;
@@ -273,13 +314,20 @@ export interface IUser {
   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;
-  qq_users?: IQqUser[];
-  live_rooms?: ILiveRoom[];
-  wallet?: IWallet;
 }
 
 export interface IQqUser {
@@ -386,15 +434,5 @@ export interface IDanmu {
   msg: string;
   socket_id: string;
   userInfo?: IUser;
-}
-
-export interface IMessage {
-  socket_id: string;
-  is_anchor: boolean;
-  user_info?: IUser;
-  data: {
-    msgType: DanmuMsgTypeEnum;
-    msg: string;
-    live_room_id: number;
-  };
+  msgIsFile: boolean;
 }

+ 1 - 26
src/layout/pc/modal/index.vue

@@ -5,38 +5,13 @@
     preset="dialog"
     positive-text="OK"
   >
-    <p>
-      使用obs/ffmpeg推流到billd-live!
-      <b
-        class="link"
-        @click="openToTarget('https://www.hsslive.cn/article/150')"
-      >
-        查看教程
-      </b>
-    </p>
-    <p>
-      <span class="hot">billd-live付费课火热报名中!</span>
-      <b
-        class="link"
-        @click="openToTarget(COMMON_URL.payCoursesArticle)"
-      >
-        查看详情
-      </b>
-    </p>
-    <p>
-      <span class="hot">
-        欢迎有兴趣且有能力的各位参与billd-live贡献,如该项目有收益,将分配给所有参与贡献的人!
-      </span>
-    </p>
+    <p></p>
   </n-modal>
 </template>
 
 <script lang="ts" setup>
-import { COMMON_URL } from '@/constant';
-import { openToTarget } from 'billd-utils';
 import { ref } from 'vue';
 
-// const showModal = ref(process.env.NODE_ENV === 'production');
 const showModal = ref(false);
 // const showModal = ref(true);
 // const showModal = ref(router.currentRoute.value.name === routerName.home);

+ 2 - 2
src/utils/cookie/loginEnv.ts

@@ -1,6 +1,6 @@
 import cookies from 'js-cookie';
 
-import { COOKIE_KEY } from '@/constant';
+import { COOKIE_DOMAIN, COOKIE_KEY } from '@/constant';
 
 export const getLoginInfo = () => {
   return cookies.get(COOKIE_KEY.loginInfo);
@@ -8,7 +8,7 @@ export const getLoginInfo = () => {
 
 export const setLoginInfo = (val) => {
   cookies.set(COOKIE_KEY.loginInfo, val, {
-    domain: process.env.NODE_ENV === 'development' ? undefined : '.hsslive.cn',
+    domain: COOKIE_DOMAIN,
   });
 };
 

+ 59 - 1
src/utils/index.ts

@@ -1,6 +1,64 @@
 // TIP: ctrl+cmd+t,生成函数注释
-
 import { getRangeRandom } from 'billd-utils';
+import sparkMD5 from 'spark-md5';
+
+export const getHostnameUrl = () => {
+  // window.location.host,包含了域名的一个DOMString,可能在该串最后带有一个":"并跟上 URL 的端口号。
+  const { protocol, hostname } = window.location;
+  return `${protocol}//${hostname}`;
+};
+
+/**
+ * 根据文件内容获取hash,同一个文件不管重命名还是改文件名后缀,hash都一样
+ * @param file
+ * @returns
+ */
+export const getHash = (file: File) => {
+  return new Promise<{
+    hash: string;
+    ext: string;
+    buffer: ArrayBuffer;
+  }>((resolve) => {
+    const reader = new FileReader();
+    reader.readAsArrayBuffer(file);
+    reader.onload = (e) => {
+      const spark = new sparkMD5.ArrayBuffer();
+      const buffer = e.target!.result as ArrayBuffer;
+      spark.append(buffer);
+      const hash = spark.end();
+      const arr = file.name.split('.');
+      const ext = arr[arr.length - 1];
+      resolve({ hash, ext, buffer });
+    };
+  });
+};
+
+// 文件切片
+export const splitFile = (file: File) => {
+  const chunkList: { chunk: Blob; chunkName: string }[] = [];
+  // 先以固定的切片大小1024*100
+  let max = 50 * 100;
+  let count = Math.ceil(file.size / max);
+  let index = 0;
+  // 限定最多100个切片
+  if (count > 100) {
+    max = Math.ceil(file.size / 100);
+    count = 100;
+  }
+  /**
+   * 0:0,max
+   * 1:max,2max
+   * 2:2max,3max
+   */
+  while (index < count) {
+    chunkList.push({
+      chunkName: `${index}`,
+      chunk: new File([file.slice(index * max, (index + 1) * max)], file.name),
+    });
+    index += 1;
+  }
+  return chunkList;
+};
 
 /**
  * 格式化倒计时

+ 2 - 4
src/utils/request.ts

@@ -1,5 +1,6 @@
 import axios, { Axios, AxiosRequestConfig } from 'axios';
 
+import { AXIOS_BASEURL } from '@/constant';
 import { useUserStore } from '@/store/user';
 import { getToken } from '@/utils/localStorage/user';
 
@@ -117,10 +118,7 @@ class MyAxios {
 }
 
 export default new MyAxios({
-  baseURL:
-    process.env.NODE_ENV === 'development'
-      ? '/api'
-      : 'https://live-api.hsslive.cn',
+  baseURL: AXIOS_BASEURL,
   // baseURL: '/prodapi',
   timeout: 1000 * 5,
 });

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

@@ -22,7 +22,7 @@
       <div v-else>
         <div>直播间名字:{{ userInfo?.live_rooms?.[0].name }}</div>
         <div>
-          直播间地址:https://live.hsslive.cn/pull/{{
+          直播间地址:{{ getHostnameUrl() }}/pull/{{
             userInfo?.live_rooms?.[0].id
           }}
         </div>
@@ -69,6 +69,7 @@ import { SRS_CB_URL_PARAMS } from '@/constant';
 import { loginTip } from '@/hooks/use-login';
 import { IUser, LiveRoomTypeEnum } from '@/interface';
 import { routerName } from '@/router';
+import { getHostnameUrl } from '@/utils';
 
 const newRtmpUrl = ref();
 const keyLoading = ref(false);

+ 3 - 4
src/views/group/index.vue

@@ -92,11 +92,9 @@
         1. 你想要私有化部署的,参考:
         <span
           class="link"
-          @click="
-            openToTarget('https://live.hsslive.cn/privatizationDeployment')
-          "
+          @click="openToTarget(`${getHostnameUrl()}/privatizationDeployment`)"
         >
-          https://live.hsslive.cn/privatizationDeployment
+          {{ `${getHostnameUrl()}/privatizationDeployment` }}
         </span>
       </p>
       <p>2. 闲聊勿扰。</p>
@@ -115,6 +113,7 @@
 import { openToTarget } from 'billd-utils';
 
 import { COMMON_URL } from '@/constant';
+import { getHostnameUrl } from '@/utils';
 </script>
 
 <style lang="scss" scoped>

+ 57 - 7
src/views/home/index.vue

@@ -1,7 +1,22 @@
 <template>
   <div class="home-wrap">
     <div class="play-container">
-      <div class="bg"></div>
+      <div
+        v-if="configBg !== ''"
+        class="bg-img"
+        :style="{ backgroundImage: `url(${configBg})` }"
+      ></div>
+      <video
+        v-if="configVideo !== ''"
+        class="bg-video"
+        :src="configVideo"
+        muted
+        autoplay
+      ></video>
+      <div
+        v-else
+        class="bg-img"
+      ></div>
       <div class="slider-wrap">
         <Slider
           v-if="interactionList.length"
@@ -169,6 +184,7 @@ import { onMounted, ref, watch } from 'vue';
 import { useRouter } from 'vue-router';
 
 import { fetchLiveList } from '@/api/live';
+import { fetchFindLiveConfigByKey } from '@/api/liveConfig';
 import { sliderList } from '@/constant';
 import { usePull } from '@/hooks/use-pull';
 import {
@@ -185,6 +201,11 @@ const appStore = useAppStore();
 const canvasRef = ref<Element>();
 const showJoinBtn = ref(false);
 const topNums = ref(6);
+const configBg = ref('');
+const configVideo = ref();
+// const configVideo = ref(
+//   'https://www.xdyun.com/resldmnqcom/ldq_website/all_ldy/cloudphone_xdyun_ldy_mobile/mobile/assets/xd-video-6c9bcd.mp4'
+// );
 const topLiveRoomList = ref<ILive[]>([]);
 const otherLiveRoomList = ref<ILive[]>([]);
 const currentLiveRoom = ref<ILive>();
@@ -200,6 +221,11 @@ const {
   handlePlay,
 } = usePull();
 
+onMounted(() => {
+  getLiveRoomList();
+  getBg();
+});
+
 watch(
   () => remoteVideo.value,
   (newVal) => {
@@ -212,6 +238,23 @@ watch(
   }
 );
 
+async function getBg() {
+  try {
+    const res = await fetchFindLiveConfigByKey('frontend_live_home_bg');
+    if (res.code === 200) {
+      const reg = /.+\.mp4$/g;
+      const url = res.data.value as string;
+      if (reg.exec(url)) {
+        configVideo.value = res.data.value;
+      } else {
+        configBg.value = res.data.value;
+      }
+    }
+  } catch (error) {
+    console.log(error);
+  }
+}
+
 function handleRefresh() {
   playLive(currentLiveRoom.value!);
 }
@@ -256,10 +299,6 @@ async function getLiveRoomList() {
   }
 }
 
-onMounted(() => {
-  getLiveRoomList();
-});
-
 function joinRoom(data: { roomId: number }) {
   router.push({
     name: routerName.pull,
@@ -274,7 +313,7 @@ function joinRoom(data: { roomId: number }) {
     position: relative;
     z-index: 1;
     padding-bottom: 20px;
-    .bg {
+    .bg-img {
       position: absolute;
       top: 0;
       right: 0;
@@ -282,10 +321,21 @@ function joinRoom(data: { roomId: number }) {
       z-index: -1;
       width: 100%;
       height: 100%;
-      background-color: papayawhip;
       background-position: center;
+      background-size: cover;
       background-repeat: no-repeat;
     }
+    .bg-video {
+      position: absolute;
+      top: 0;
+      right: 0;
+      left: 0;
+      z-index: -1;
+      width: 100%;
+      height: 100%;
+
+      object-fit: fill;
+    }
     .slider-wrap {
       padding: 2px 0 4px 0;
     }

+ 126 - 20
src/views/pull/index.vue

@@ -144,9 +144,29 @@
         >
           <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
             <span class="name">
-              {{ item.userInfo?.username || item.socket_id }}:
+              <span v-if="item.userInfo">
+                {{ item.userInfo.username }}[{{
+                  item.userInfo.roles?.map((v) => v.role_name).join()
+                }}]
+              </span>
+              <span v-else>{{ item.socket_id }}</span>
+              <span>:</span>
+            </span>
+            <span
+              class="msg"
+              v-if="!item.msgIsFile"
+            >
+              {{ item.msg }}
             </span>
-            <span class="msg">{{ item.msg }}</span>
+            <div
+              class="msg img"
+              v-else
+            >
+              <img
+                :src="item.msg"
+                alt=""
+              />
+            </div>
           </template>
           <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
             <span class="name system">系统通知:</span>
@@ -162,7 +182,29 @@
           </template>
         </div>
       </div>
-      <div class="send-msg">
+      <div
+        class="send-msg"
+        v-loading="msgLoading"
+      >
+        <div class="control">
+          <div
+            class="ico face"
+            title="表情"
+            @click="handleWait"
+          ></div>
+          <div
+            class="ico img"
+            title="图片"
+            @click="mockClick"
+          >
+            <input
+              ref="uploadRef"
+              type="file"
+              class="input-upload"
+              @change="uploadChange"
+            />
+          </div>
+        </div>
         <textarea
           v-model="danmuStr"
           class="ipt"
@@ -187,8 +229,10 @@
 import { onMounted, onUnmounted, ref, watch } from 'vue';
 
 import { fetchGoodsList } from '@/api/goods';
+import { QINIU_BLOG } from '@/constant';
 import { loginTip } from '@/hooks/use-login';
 import { usePull } from '@/hooks/use-pull';
+import { useUpload } from '@/hooks/use-upload';
 import { DanmuMsgTypeEnum, GoodsTypeEnum, IGoods } from '@/interface';
 import { useAppStore } from '@/store/app';
 import { useUserStore } from '@/store/user';
@@ -202,11 +246,13 @@ const giftGoodsList = ref<IGoods[]>([]);
 const height = ref(0);
 const giftLoading = ref(false);
 const showRecharge = ref(false);
+const msgLoading = ref(false);
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
 const danmuListRef = ref<HTMLDivElement>();
 const remoteVideoRef = ref<HTMLDivElement>();
 const containerRef = ref<HTMLDivElement>();
+const uploadRef = ref<HTMLInputElement>();
 const {
   initPull,
   closeWs,
@@ -214,6 +260,7 @@ const {
   keydownDanmu,
   sendDanmu,
   handlePlay,
+  msgIsFile,
   mySocketId,
   videoHeight,
   videoLoading,
@@ -225,6 +272,22 @@ const {
   anchorInfo,
 } = usePull();
 
+onMounted(() => {
+  setTimeout(() => {
+    scrollTo(0, 0);
+  }, 100);
+  appStore.setPlay(true);
+  getGoodsList();
+  if (topRef.value && bottomRef.value && containerRef.value) {
+    const res =
+      bottomRef.value.getBoundingClientRect().top -
+      (topRef.value.getBoundingClientRect().top +
+        topRef.value.getBoundingClientRect().height);
+    height.value = res;
+  }
+  initPull();
+});
+
 onUnmounted(() => {
   closeWs();
   closeRtc();
@@ -243,21 +306,36 @@ watch(
   }
 );
 
-onMounted(() => {
-  setTimeout(() => {
-    scrollTo(0, 0);
-  }, 100);
-  appStore.setPlay(true);
-  getGoodsList();
-  if (topRef.value && bottomRef.value && containerRef.value) {
-    const res =
-      bottomRef.value.getBoundingClientRect().top -
-      (topRef.value.getBoundingClientRect().top +
-        topRef.value.getBoundingClientRect().height);
-    height.value = res;
+function handleWait() {
+  window.$message.warning('敬请期待!');
+}
+
+function mockClick() {
+  uploadRef.value?.click();
+}
+
+async function uploadChange() {
+  const fileList = uploadRef.value?.files;
+  if (fileList?.length) {
+    try {
+      msgLoading.value = true;
+      msgIsFile.value = true;
+      const res = await useUpload({
+        prefix: QINIU_BLOG.prefix['image/'],
+        file: fileList[0],
+      });
+      if (res?.resultUrl) {
+        danmuStr.value = res.resultUrl || '错误图片';
+        sendDanmu();
+      }
+    } catch (error) {
+      console.log(error);
+    } finally {
+      msgIsFile.value = false;
+      msgLoading.value = false;
+    }
   }
-  initPull();
-});
+}
 
 function handlePay() {
   window.$message.info('敬请期待!');
@@ -590,7 +668,9 @@ watch(
       @extend %hideScrollbar;
       .item {
         margin-bottom: 10px;
-        font-size: 12px;
+        font-size: 13px;
+        word-wrap: break-word;
+        white-space: normal;
         .name {
           color: #9499a0;
           &.system {
@@ -599,6 +679,11 @@ watch(
         }
         .msg {
           color: #61666d;
+          &.img {
+            img {
+              width: 80%;
+            }
+          }
         }
       }
     }
@@ -608,6 +693,27 @@ watch(
       box-sizing: border-box;
       padding: 0 10px;
       width: 100%;
+      .control {
+        display: flex;
+        margin-bottom: 4px;
+        .ico {
+          margin-right: 4px;
+          width: 20px;
+          height: 20px;
+          cursor: pointer;
+          .input-upload {
+            width: 0;
+            height: 0;
+            opacity: 0;
+          }
+          &.face {
+            @include setBackground('@/assets/img/msg-face.png');
+          }
+          &.img {
+            @include setBackground('@/assets/img/msg-img.png');
+          }
+        }
+      }
       .ipt {
         display: block;
         box-sizing: border-box;
@@ -625,8 +731,8 @@ watch(
         box-sizing: border-box;
         margin-top: 10px;
         margin-left: auto;
-        padding: 5px;
-        width: 80px;
+        padding: 4px;
+        width: 70px;
         border-radius: 4px;
         background-color: $theme-color-gold;
         color: white;

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

@@ -565,7 +565,7 @@ function renderFrame() {
 }
 
 function handleMixedAudio() {
-  console.log('handleMixedAudio');
+  // console.log('handleMixedAudio');
   const allAudioTrack = appStore.allTrack.filter((item) => item.audio === 1);
   const audioCtx = new AudioContext();
   if (canvasVideoStream.value?.getAudioTracks()[0]) {
@@ -665,7 +665,6 @@ function autoCreateVideo({
   rect?: { left: number; top: number };
   muted?: boolean;
 }) {
-  console.warn('autoCreateVideo', id);
   const videoEl = createVideo({ appendChild: true });
   bodyAppendChildElArr.value.push(videoEl);
   videoEl.setAttribute('videoid', id);

+ 0 - 5
src/views/push/mediaModal/index.vue

@@ -328,11 +328,6 @@ async function init() {
     timeInfo.value = props.initData.timeInfo;
     txtInfo.value = props.initData.txtInfo;
     stopwatchInfo.value = props.initData.stopwatchInfo;
-    console.log(
-      props.initData.deviceId,
-      props.initData.type,
-      props.initData.mediaName
-    );
   }
 }
 </script>

+ 10 - 41
test/test.html

@@ -7,46 +7,15 @@
       content="width=device-width, initial-scale=1.0"
     />
     <title>Document</title>
+    <link
+      href="emoji.css"
+      rel="stylesheet"
+      type="text/css"
+    />
+    <script
+      src="emoji.js"
+      type="text/javascript"
+    ></script>
   </head>
-  <body>
-    <script>
-      /**
-       * @param {number} targetCount 不小于1的整数,表示经过targetCount帧之后返回结果
-       * @return {Promise<number>}
-       */
-      const getScreenFps = (() => {
-        // 先做一下兼容性处理
-        const nextFrame =
-          window.requestAnimationFrame ||
-          window.webkitRequestAnimationFrame ||
-          window.mozRequestAnimationFrame;
-        if (!nextFrame) {
-          console.error('requestAnimationFrame is not supported!');
-          return;
-        }
-        return (targetCount = 120) => {
-          // 判断参数是否合规
-          if (targetCount < 1)
-            throw new Error('targetCount cannot be less than 1.');
-          const beginDate = Date.now();
-          let count = 0;
-          return new Promise((resolve) => {
-            (function log() {
-              nextFrame(() => {
-                if (++count >= targetCount) {
-                  const diffDate = Date.now() - beginDate;
-                  const fps = (count / diffDate) * 1000;
-                  return resolve(fps);
-                }
-                log();
-              });
-            })();
-          });
-        };
-      })();
-      getScreenFps().then((res) => {
-        console.log(res);
-      });
-    </script>
-  </body>
+  <body></body>
 </html>

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است