Selaa lähdekoodia

feat: 完善支付

shuisheng 2 vuotta sitten
vanhempi
sitoutus
b9e153642d

+ 6 - 0
src/api/goods.ts

@@ -4,3 +4,9 @@ import request from '@/utils/request';
 export function fetchGoodsList(params: IList<IGoods>) {
   return request.get<IPaging<IGoods>>('/goods/list', { params });
 }
+
+export function fetchFindByTypeGoods(type) {
+  return request.get<IGoods>('/goods/find_by_type', {
+    params: { type },
+  });
+}

+ 12 - 0
src/api/liveRoom.ts

@@ -0,0 +1,12 @@
+import request from '@/utils/request';
+
+export function fetchLiveRoomList(params: {
+  orderName: string;
+  orderBy: string;
+}) {
+  return request.instance({
+    url: '/live_room/list',
+    method: 'get',
+    params,
+  });
+}

+ 3 - 3
src/api/order.ts

@@ -9,9 +9,9 @@ import request from '@/utils/request';
  * @returns
  */
 export function fetchAliPay(data: {
-  total_amount: string;
-  subject: string;
-  body: string;
+  goodsId: number;
+  liveRoomId: number;
+  money?: string;
 }) {
   return request.instance({
     url: '/order/pay',

+ 2 - 2
src/api/user.ts

@@ -8,6 +8,6 @@ export function fetchUserInfo() {
   });
 }
 
-export function fetchUserList() {
-  return request.get<IPaging<IUser>>('/user/list');
+export function fetchUserList(params: { orderName: string; orderBy: string }) {
+  return request.get<IPaging<IUser>>('/user/list', { params });
 }

+ 12 - 0
src/api/userLiveRoom.ts

@@ -0,0 +1,12 @@
+import { IUserLiveRoom } from '@/interface';
+import request from '@/utils/request';
+
+export function fetchUserHasLiveRoom(userId: number) {
+  return request.get<IUserLiveRoom>(`/user_live_room/find_by_userId/${userId}`);
+}
+export function fetchCreateUserLiveRoom() {
+  return request.instance({
+    url: `/user_live_room/create`,
+    method: 'post',
+  });
+}

+ 8 - 0
src/api/wallet.ts

@@ -0,0 +1,8 @@
+import request from '@/utils/request';
+
+export function fetchWalletList() {
+  return request.instance({
+    url: '/wallet/list',
+    method: 'get',
+  });
+}

BIN
src/assets/img/wallet.webp


+ 219 - 0
src/components/QrPay/index.vue

@@ -0,0 +1,219 @@
+<template>
+  <div class="qr-pay-wrap">
+    <div class="money">金额:{{ props.money }}元</div>
+    <div class="qrcode-wrap">
+      <img
+        v-if="aliPayBase64 !== ''"
+        class="qrcode"
+        :src="aliPayBase64"
+        alt=""
+      />
+      <template v-if="currentPayStatus !== PayStatusEnum.error">
+        <div class="mask">
+          <div class="txt">
+            {{
+              currentPayStatus === PayStatusEnum.TRADE_SUCCESS
+                ? '支付成功'
+                : '等待支付'
+            }}
+          </div>
+        </div>
+      </template>
+    </div>
+    <div v-if="aliPayBase64 !== ''">
+      <div class="bottom">
+        <div class="sao">打开支付宝扫一扫</div>
+        <div class="expr">有效期5分钟({{ formatDownTime(downTime) }})</div>
+      </div>
+    </div>
+
+    <h3
+      v-if="currentPayStatus === PayStatusEnum.WAIT_BUYER_PAY"
+      class="tip"
+    >
+      ps:支付宝标题显示:东圃牛杂档,是正常的~
+    </h3>
+
+    <div
+      v-if="payOk"
+      class="bottom"
+    >
+      <h2>支付成功!</h2>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { hrefToTarget, isMobile } from 'billd-utils';
+import QRCode from 'qrcode';
+import { defineProps, onMounted, onUnmounted, ref } from 'vue';
+
+import { fetchAliPay, fetchAliPayStatus } from '@/api/order';
+import { PayStatusEnum } from '@/interface';
+
+const payOk = ref(false);
+const aliPayBase64 = ref('');
+const payStatusTimer = ref();
+const downTimer = ref();
+const downTime = ref();
+const downTimeEnd = ref();
+
+const currentPayStatus = ref(PayStatusEnum.error);
+const props = defineProps({
+  money: { type: String, default: '0.00' },
+  goodsId: { type: Number, default: -1 },
+  liveRoomId: { type: Number, default: -1 },
+});
+
+onUnmounted(() => {
+  clearInterval(payStatusTimer.value);
+  clearInterval(downTimer.value);
+});
+
+onMounted(() => {
+  startPay({
+    goodsId: props.goodsId,
+    liveRoomId: props.liveRoomId,
+    money: props.money,
+  });
+});
+
+function formatDownTime(startTime: number) {
+  const time2 = downTimeEnd.value - startTime;
+  const ms = 1;
+  const second = ms * 1000;
+  const minute = second * 60;
+  const hour = minute * 60;
+  const day = hour * 24;
+  if (time2 > day) {
+    const res = (time2 / day).toFixed(4).split('.');
+    return `${res[0]}天${Math.ceil(Number(`0.${res[1]}`) * 24)}时`;
+  } else if (time2 > hour) {
+    const res = (time2 / hour).toFixed(4).split('.');
+    return `${res[0]}时${Math.ceil(Number(`0.${res[1]}`) * 60)}分`;
+  } else if (time2 > minute) {
+    const res = (time2 / minute).toFixed(4).split('.');
+    return `${res[0]}分${Math.ceil(Number(`0.${res[1]}`) * 60)}秒`;
+  } else {
+    const res = (time2 / second).toFixed(4).split('.');
+    return `${res[0]}秒`;
+  }
+}
+
+async function generateQR(text) {
+  let base64 = '';
+  try {
+    base64 = await QRCode.toDataURL(text, {
+      margin: 1,
+    });
+  } catch (err) {
+    console.error('生成二维码失败!', err);
+  }
+  return base64;
+}
+
+function handleDownTime() {
+  clearInterval(downTimer.value);
+  downTimeEnd.value = +new Date() + 1000 * 60 * 5;
+  downTime.value = +new Date();
+  downTimer.value = setInterval(() => {
+    downTime.value = +new Date();
+  }, 1000);
+}
+
+async function startPay(data: {
+  goodsId: number;
+  liveRoomId: number;
+  money?: string;
+}) {
+  currentPayStatus.value = PayStatusEnum.error;
+  payOk.value = false;
+  clearInterval(payStatusTimer.value);
+  clearInterval(downTimer.value);
+  try {
+    const res = await fetchAliPay({
+      money: data.money,
+      goodsId: data.goodsId,
+      liveRoomId: data.liveRoomId,
+    });
+    if (res.code === 200) {
+      if (isMobile()) {
+        hrefToTarget(res.data.qr_code);
+        return;
+      }
+      const base64 = await generateQR(res.data.qr_code);
+      aliPayBase64.value = base64;
+      getPayStatus(res.data.out_trade_no);
+      handleDownTime();
+    }
+  } catch (error) {
+    console.log(error);
+  }
+}
+
+function getPayStatus(outTradeNo: string) {
+  clearInterval(payStatusTimer.value);
+  payStatusTimer.value = setInterval(async () => {
+    try {
+      const res = await fetchAliPayStatus({
+        out_trade_no: outTradeNo,
+      });
+      if (res.data.tradeStatus === PayStatusEnum.WAIT_BUYER_PAY) {
+        currentPayStatus.value = PayStatusEnum.WAIT_BUYER_PAY;
+        console.log('等待支付');
+      }
+      if (res.data.tradeStatus === PayStatusEnum.TRADE_SUCCESS) {
+        currentPayStatus.value = PayStatusEnum.TRADE_SUCCESS;
+        clearInterval(downTimer.value);
+        clearInterval(payStatusTimer.value);
+        console.log('支付成功!');
+        payOk.value = true;
+      }
+    } catch (error) {
+      console.log(error);
+    }
+  }, 1000);
+}
+</script>
+
+<style lang="scss" scoped>
+.qr-pay-wrap {
+  padding: 10px;
+  .money {
+    text-align: center;
+    font-size: 20px;
+  }
+  .qrcode-wrap {
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    box-sizing: border-box;
+    margin: 0 auto;
+    width: 140px;
+    height: 140px;
+
+    .mask {
+      position: absolute !important;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      @extend %maskBg;
+      .txt {
+        color: white;
+        font-weight: bold;
+      }
+    }
+  }
+  .tip {
+    text-align: center;
+  }
+  .bottom {
+    margin-top: 2px;
+    width: 100%;
+    text-align: center;
+    font-size: 14px;
+  }
+}
+</style>

+ 6 - 2
src/hooks/loginModal/index.vue

@@ -13,7 +13,7 @@
           class="qq-logo"
           src="@/assets/img/qq-logo.webp"
           alt=""
-          @click="useQQLogin()"
+          @click="handleQQlogin"
         />
         <div>qq登录</div>
       </div>
@@ -35,11 +35,15 @@ export default defineComponent({
     const title = ref('登录');
     const show = ref(false);
     const maskClosable = ref(true);
+    function handleQQlogin() {
+      show.value = !show.value;
+      useQQLogin();
+    }
     return {
       title,
       show,
       maskClosable,
-      useQQLogin,
+      handleQQlogin,
     };
   },
 });

+ 1 - 0
src/hooks/modal/index.vue

@@ -78,6 +78,7 @@ export default defineComponent({
       border-radius: 100px;
       text-align: center;
       line-height: 44px;
+      cursor: pointer;
 
       user-select: none;
 

+ 5 - 3
src/hooks/use-login.ts

@@ -6,6 +6,7 @@ import { QQ_CLIENT_ID, QQ_OAUTH_URL, QQ_REDIRECT_URI } from '@/constant';
 import LoginModalCpt from '@/hooks/loginModal/index.vue';
 import { PlatformEnum } from '@/interface';
 import { useUserStore } from '@/store/user';
+import cache from '@/utils/cache';
 import { clearLoginInfo, setLoginInfo } from '@/utils/cookie';
 
 const app = createApp(LoginModalCpt);
@@ -43,9 +44,10 @@ export async function handleLogin(e) {
   }
 }
 
-export function loginTip() {
-  const userStore = useUserStore();
-  if (!userStore.userInfo) {
+export function loginTip(show = false) {
+  const token = cache.getStorageExp('token');
+  instance.show = show;
+  if (!token) {
     window.$message.warning('请先登录~');
     instance.show = true;
     return false;

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

@@ -1,5 +1,5 @@
 import { getRandomString } from 'billd-utils';
-import { Ref, nextTick, reactive, ref, watch } from 'vue';
+import { Ref, nextTick, onUnmounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { fetchRtcV1Play } from '@/api/srs';
@@ -45,6 +45,7 @@ export function usePull({
   const streamurl = ref('');
   const flvurl = ref('');
   const danmuStr = ref('');
+  const balance = ref('0.00');
   const damuList = ref<IDanmu[]>([]);
   const liveUserList = ref<ILiveUser[]>([]);
   const isDone = ref(false);
@@ -92,6 +93,10 @@ export function usePull({
     txt: string;
   }>();
 
+  onUnmounted(() => {
+    clearInterval(heartbeatTimer.value);
+  });
+
   /** 摄像头 */
   async function startGetUserMedia() {
     if (!localStream.value) {
@@ -135,6 +140,9 @@ export function usePull({
       () => networkStore.wsMap.get(roomId.value)?.socketIo?.connected,
     ],
     ([userInfo, connected]) => {
+      if (userInfo) {
+        balance.value = userInfo.wallet?.balance || '0.00';
+      }
       if (userInfo && connected) {
         const instance = networkStore.wsMap.get(roomId.value);
         if (!instance) return;
@@ -212,13 +220,10 @@ export function usePull({
   }
 
   function addTransceiver(socketId: string) {
-    console.log('333addTransceiver', localStream.value);
     if (!localStream.value) return;
     if (socketId !== getSocketId()) {
-      console.log(3333);
       localStream.value.getTracks().forEach((track) => {
         const rtc = networkStore.getRtcMap(`${roomId.value}___${socketId}`);
-        console.log(999999);
         rtc?.addTransceiver(track, localStream.value);
       });
     }
@@ -608,6 +613,7 @@ export function usePull({
     startGetDisplayMedia,
     addTrack,
     addVideo,
+    balance,
     roomName,
     roomNoLive,
     damuList,

+ 68 - 7
src/hooks/use-push.ts

@@ -1,8 +1,20 @@
-import { getRandomString } from 'billd-utils';
-import { Ref, nextTick, reactive, ref } from 'vue';
+import { getRandomString, windowReload } from 'billd-utils';
+import {
+  Ref,
+  nextTick,
+  onMounted,
+  onUnmounted,
+  reactive,
+  ref,
+  watch,
+} from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 
 import { fetchRtcV1Publish } from '@/api/srs';
+import {
+  fetchCreateUserLiveRoom,
+  fetchUserHasLiveRoom,
+} from '@/api/userLiveRoom';
 import {
   DanmuMsgTypeEnum,
   IAdminIn,
@@ -25,6 +37,7 @@ import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
 
 import { loginTip } from './use-login';
+import { useTip } from './use-tip';
 
 export function usePush({
   localVideoRef,
@@ -40,9 +53,9 @@ export function usePush({
   const userStore = useUserStore();
   const networkStore = useNetworkStore();
   const heartbeatTimer = ref();
-  const roomId = ref<string>(getRandomString(15));
+  const roomId = ref('-1');
+  const roomName = ref('');
   const danmuStr = ref('');
-  const roomName = ref(getRandomString(5));
   const isDone = ref(false);
   const joined = ref(false);
   const disabled = ref(false);
@@ -96,8 +109,58 @@ export function usePush({
     txt: string;
   }>();
 
-  function startLive() {
+  async function userHasLiveRoom() {
+    const res = await fetchUserHasLiveRoom(userStore.userInfo?.id!);
+    if (res.code === 200 && res.data) {
+      roomName.value = res.data.live_room?.roomName || '';
+      roomId.value = `${res.data.live_room?.id || -1}`;
+      router.push({ query: { ...route.query, roomId: roomId.value } });
+      return true;
+    }
+    return false;
+  }
+
+  async function handleCreateUserLiveRoom() {
+    try {
+      const res = await fetchCreateUserLiveRoom();
+      if (res.code === 200) {
+        window.$message.success('开通直播间成功!');
+        windowReload();
+      }
+    } catch (error) {
+      console.log(error);
+    }
+  }
+
+  watch(
+    () => userStore.userInfo,
+    async (newVal) => {
+      if (newVal) {
+        const res = await userHasLiveRoom();
+        if (!res) {
+          await useTip('你还没有直播间,是否立即开通?');
+          await handleCreateUserLiveRoom();
+        }
+      }
+    }
+  );
+
+  onMounted(() => {
+    if (!loginTip()) return;
+  });
+
+  onUnmounted(() => {
+    clearInterval(heartbeatTimer.value);
+  });
+
+  async function startLive() {
     if (!loginTip()) return;
+    const flag = await userHasLiveRoom();
+    if (!flag) {
+      await useTip('你还没有直播间,是否立即开通?');
+      await handleCreateUserLiveRoom();
+      return;
+    }
     if (!roomNameIsOk()) return;
     if (currMediaTypeList.value.length <= 0) {
       window.$message.warning('请选择一个素材!');
@@ -593,13 +656,11 @@ export function usePush({
   }
 
   async function initPush() {
-    router.push({ query: { ...route.query, roomId: roomId.value } });
     const all = await getAllMediaDevices();
     allMediaTypeList[MediaTypeEnum.camera] = {
       txt: all.find((item) => item.kind === 'videoinput')?.label || '摄像头',
       type: MediaTypeEnum.camera,
     };
-    console.log('initPush', localVideoRef);
     localVideoRef.value?.addEventListener('loadstart', () => {
       console.warn('视频流-loadstart');
       const rtc = networkStore.getRtcMap(roomId.value);

+ 41 - 0
src/interface.ts

@@ -6,6 +6,20 @@ export enum PayStatusEnum {
   TRADE_SUCCESS = 'TRADE_SUCCESS',
 }
 
+export enum RankTypeEnum {
+  liveRoom = 'liveRoom',
+  user = 'user',
+  sponsors = 'sponsors',
+  wallet = 'wallet',
+}
+export interface IWallet {
+  id?: number;
+  user_id?: number;
+  balance?: string;
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+}
 export type IList<T> = {
   nowPage?: string;
   pageSize?: string;
@@ -52,6 +66,7 @@ export enum GoodsTypeEnum {
   support = 'support',
   sponsors = 'sponsors',
   gift = 'gift',
+  recharge = 'recharge',
 }
 
 export interface IGoods {
@@ -72,6 +87,30 @@ export interface IGoods {
   deleted_at?: string;
 }
 
+export interface ILiveRoom {
+  id?: number;
+  /** 用户信息 */
+  user?: IUser;
+  /** 直播间名字 */
+  roomName?: string;
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+}
+
+export interface IUserLiveRoom {
+  id?: number;
+  user_id?: number;
+  live_room_id?: number;
+  /** 用户信息 */
+  user?: IUser;
+  /** 直播间信息 */
+  live_room?: ILiveRoom;
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+}
+
 export enum liveTypeEnum {
   webrtcPull = 'webrtcPull',
   srsWebrtcPull = 'srsWebrtcPull',
@@ -127,6 +166,7 @@ export interface IUser {
   id?: number;
   username?: string;
   password?: string;
+  email?: string;
   status?: number;
   avatar?: string;
   desc?: string;
@@ -136,6 +176,7 @@ export interface IUser {
   updated_at?: string;
   deleted_at?: string;
   qq_users?: IQqUser[];
+  wallet?: IWallet;
 }
 
 export interface IQqUser {

+ 8 - 28
src/layout/head/index.vue

@@ -41,22 +41,22 @@
         <a
           :class="{
             item: 1,
-            active: router.currentRoute.value.name === routerName.about,
+            active: router.currentRoute.value.name === routerName.ad,
           }"
-          href="/about"
-          @click.prevent="router.push({ name: routerName.about })"
+          href="/ad"
+          @click.prevent="router.push({ name: routerName.ad })"
         >
-          关于
+          广告
         </a>
         <a
           :class="{
             item: 1,
-            active: router.currentRoute.value.name === routerName.ad,
+            active: router.currentRoute.value.name === routerName.about,
           }"
-          href="/ad"
-          @click.prevent="router.push({ name: routerName.ad })"
+          href="/about"
+          @click.prevent="router.push({ name: routerName.about })"
         >
-          广告
+          关于
         </a>
         <div
           v-for="(item, index) in navLeftList.filter(
@@ -106,26 +106,6 @@
         ></div>
       </n-dropdown>
 
-      <!-- <a
-        :class="{
-          sponsors: 1,
-          active: router.currentRoute.value.name === routerName.sponsors,
-        }"
-        href="/sponsors"
-        @click.prevent="router.push({ name: routerName.sponsors })"
-      >
-        赞助
-      </a>
-      <a
-        :class="{
-          about: 1,
-          active: router.currentRoute.value.name === routerName.about,
-        }"
-        href="/about"
-        @click.prevent="router.push({ name: routerName.about })"
-      >
-        关于
-      </a> -->
       <a
         class="bilibili"
         target="_blank"

+ 0 - 5
src/store/network/index.ts

@@ -6,7 +6,6 @@ import { WebSocketClass } from '@/network/webSocket';
 type NetworkRootState = {
   wsMap: Map<string, WebSocketClass>;
   rtcMap: Map<string, WebRTCClass>;
-  fromUserMap: Map<string, string>;
 };
 
 export const useNetworkStore = defineStore('network', {
@@ -14,7 +13,6 @@ export const useNetworkStore = defineStore('network', {
     return {
       wsMap: new Map(),
       rtcMap: new Map(),
-      fromUserMap: new Map(),
     };
   },
   actions: {
@@ -34,9 +32,6 @@ export const useNetworkStore = defineStore('network', {
         this.rtcMap.set(roomId, arg);
       }
     },
-    updateFromUserMap(socketId: string, data) {
-      this.fromUserMap.set(socketId, data);
-    },
     getRtcMap(roomId: string) {
       return this.rtcMap.get(roomId);
     },

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

@@ -1,29 +1,11 @@
 import { defineStore } from 'pinia';
 
 import { fetchUserInfo } from '@/api/user';
-import { IRole } from '@/interface';
+import { IRole, IUser } from '@/interface';
 import cache from '@/utils/cache';
 
 type RootState = {
-  userInfo?: {
-    id: number;
-    username: string;
-    status: number;
-    avatar: string;
-    title: string;
-    created_at: string;
-    updated_at: string;
-    deleted_at: any;
-    send_comments_total: number;
-    receive_comments_total: number;
-    send_stars_total: number;
-    receive_stars_total: number;
-    articles_total: number;
-    qq_users: any[];
-    github_users: any[];
-    email_users: any[];
-    roles: IRole[];
-  };
+  userInfo?: IUser;
   token?: string;
   roles?: IRole[];
 };

+ 5 - 4
src/utils/request.ts

@@ -7,8 +7,7 @@ export interface MyAxiosPromise<T = any>
   extends Promise<{
     code: number;
     data: T;
-    msg: string;
-    message?: string;
+    message: string;
   }> {}
 
 interface MyAxiosInstance extends Axios {
@@ -54,8 +53,6 @@ class MyAxios {
         if (error.message.indexOf('timeout') !== -1) {
           console.error(error.message);
           window.$message.error('请求超时,请重试');
-        } else {
-          window.$message.error('网络错误,请重试');
         }
         const statusCode = error.response.status as number;
         const errorResponseData = error.response.data;
@@ -75,20 +72,24 @@ class MyAxios {
           }
           if (statusCode === 400) {
             console.error(errorResponseData.message);
+            window.$message.error(errorResponseData.message);
             return Promise.reject(errorResponseData);
           }
           if (statusCode === 401) {
             console.error(errorResponseData.message);
+            window.$message.error(errorResponseData.message);
             const userStore = useUserStore();
             userStore.logout();
             return Promise.reject(errorResponseData);
           }
           if (statusCode === 403) {
             console.error(errorResponseData.message);
+            window.$message.error(errorResponseData.message);
             return Promise.reject(errorResponseData);
           }
           if (statusCode === 404) {
             console.error(errorResponseData.message);
+            window.$message.error(errorResponseData.message);
             return Promise.reject(errorResponseData);
           }
         } else {

+ 8 - 1
src/views/about/index.vue

@@ -4,7 +4,14 @@
     <h3>1. 原生webrtc一对多直播(DONE)</h3>
     <h3>2. srs-webrtc一对多直播(DONE)</h3>
     <h3>3. 原生webrtc多对多直播(DONE)</h3>
-
+    <h1>PR</h1>
+    <h2>
+      billd-live目前只有作者一人开发,难免有不足的地方,欢迎大家共同开发。
+    </h2>
+    <h3>
+      ps:我希望的是强强联合,如果你啥都不会,欢迎
+      <a href="/support">付费</a>向我提问。
+    </h3>
     <h1>微信交流群 & 我的微信</h1>
     <img
       src="@/assets/img/wechat-group.webp"

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

@@ -39,7 +39,7 @@
             </div>
           </div>
           <div
-            v-if="showJoin"
+            v-if="showSidebar"
             class="sidebar"
           >
             <div
@@ -63,6 +63,7 @@
             </div>
 
             <div
+              v-if="showJoin"
               class="join"
               @click="handleJoin()"
             >
@@ -95,6 +96,14 @@
             <div class="name">{{ item.name }}</div>
             <div class="price">¥{{ item.price }}</div>
           </div>
+          <div
+            class="item"
+            @click="handleRecharge"
+          >
+            <div class="ico wallet"></div>
+            <div class="name">余额:{{ balance }}</div>
+            <div class="price">立即充值</div>
+          </div>
         </div>
       </div>
       <div class="right">
@@ -170,6 +179,7 @@
           </div>
         </div>
       </div>
+      <RechargeCpt v-if="showRecharge"></RechargeCpt>
     </template>
   </div>
 </template>
@@ -189,12 +199,16 @@ import {
 import { useAppStore } from '@/store/app';
 import { useUserStore } from '@/store/user';
 
+import RechargeCpt from './recharge/index.vue';
+
 const route = useRoute();
 const userStore = useUserStore();
 const appStore = useAppStore();
 
 const giftGoodsList = ref<IGoods[]>([]);
+const showRecharge = ref(false);
 const showJoin = ref(true);
+const showSidebar = ref(true);
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
 const containerRef = ref<HTMLDivElement>();
@@ -208,14 +222,20 @@ const {
   getSocketId,
   keydownDanmu,
   sendDanmu,
+  batchSendOffer,
   startGetUserMedia,
+  startGetDisplayMedia,
+  addTrack,
   addVideo,
+  balance,
   roomName,
   roomNoLive,
   damuList,
   giftList,
   liveUserList,
   danmuStr,
+  localStream,
+  sender,
   sidebarList,
 } = usePull({
   localVideoRef,
@@ -235,6 +255,10 @@ async function getGoodsList() {
   }
 }
 
+function handleRecharge() {
+  showRecharge.value = !showRecharge.value;
+}
+
 function handleJoin() {
   showJoin.value = !showJoin.value;
   nextTick(async () => {
@@ -255,7 +279,7 @@ onMounted(() => {
       route.query.liveType as liveTypeEnum
     )
   ) {
-    showJoin.value = false;
+    showSidebar.value = false;
   }
   if (topRef.value && bottomRef.value && containerRef.value) {
     const res =
@@ -288,7 +312,7 @@ onMounted(() => {
     .head {
       display: flex;
       justify-content: space-between;
-      padding: 20px;
+      padding: 10px 20px;
       .tag {
         display: inline-block;
         margin-right: 5px;
@@ -392,12 +416,21 @@ onMounted(() => {
       display: flex;
       align-items: center;
       justify-content: space-around;
+      box-sizing: border-box;
       height: 120px;
       .item {
-        margin-right: 10px;
+        display: flex;
+        align-items: center;
+        flex-direction: column;
+        justify-content: center;
+        box-sizing: border-box;
+        width: 110px;
+        height: 110px;
         text-align: center;
         cursor: pointer;
-
+        &:hover {
+          background-color: #ebe0ce;
+        }
         .ico {
           position: relative;
           width: 50px;
@@ -405,20 +438,24 @@ onMounted(() => {
           background-position: center center;
           background-size: cover;
           background-repeat: no-repeat;
+          &.wallet {
+            background-image: url('@/assets/img/wallet.webp');
+          }
           .badge {
             position: absolute;
-            top: -10px;
+            top: -8px;
             right: -10px;
             display: flex;
             align-items: center;
             justify-content: center;
-            border-radius: 2px;
             padding: 2px;
+            border-radius: 2px;
             color: white;
             .txt {
               display: inline-block;
-              transform-origin: center !important;
               line-height: 1;
+              transform-origin: center !important;
+
               @include minFont(10);
             }
           }

+ 78 - 0
src/views/pull/recharge/index.vue

@@ -0,0 +1,78 @@
+<template>
+  <div class="recharge-wrap">
+    <n-modal
+      v-model:show="showModal"
+      style="width: 600px"
+      title="充值"
+      preset="card"
+      class="container"
+    >
+      <div>
+        充值金额(最低充值{{ minMoney }}元,最高充值{{ maxMoney }}元)
+        <n-input-number
+          v-model:value="money"
+          :precision="2"
+          :min="minMoney"
+          :max="maxMoney"
+          clearable
+        >
+          <template #prefix>¥</template>
+        </n-input-number>
+      </div>
+      <n-button
+        type="primary"
+        @click="startPay"
+      >
+        确定充值
+      </n-button>
+      <QrPayCpt
+        v-if="showQrPay"
+        :money="goodsInfo.money"
+        :goods-id="goodsInfo.goodsId"
+        :live-room-id="goodsInfo.liveRoomId"
+      ></QrPayCpt>
+    </n-modal>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { nextTick, reactive, ref } from 'vue';
+
+import { fetchFindByTypeGoods } from '@/api/goods';
+import QrPayCpt from '@/components/QrPay/index.vue';
+import { GoodsTypeEnum } from '@/interface';
+
+const showModal = ref(true);
+const maxMoney = 200;
+const minMoney = 0.1;
+const money = ref(minMoney);
+
+const showQrPay = ref(false);
+const goodsInfo = reactive({
+  money: '0.00',
+  goodsId: -1,
+  liveRoomId: -1,
+});
+
+async function startPay() {
+  console.log(money.value, minMoney);
+  if (money.value < minMoney) {
+    window.$message.warning(`最少充值${minMoney}元`);
+    return;
+  }
+  const res = await fetchFindByTypeGoods(GoodsTypeEnum.recharge);
+  if (res.code === 200) {
+    showQrPay.value = false;
+    nextTick(() => {
+      goodsInfo.money = `${money.value}`;
+      goodsInfo.goodsId = res.data.id!;
+      showQrPay.value = true;
+    });
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.recharge-wrap {
+}
+</style>

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

@@ -242,13 +242,13 @@ onUnmounted(() => {
 });
 
 onMounted(() => {
-  initPush();
   if (topRef.value && bottomRef.value && containerRef.value) {
     const res =
       bottomRef.value.getBoundingClientRect().top -
       topRef.value.getBoundingClientRect().top;
     containerRef.value.style.height = `${res}px`;
   }
+  initPush();
 });
 </script>
 

+ 105 - 18
src/views/rank/index.vue

@@ -2,10 +2,10 @@
   <div class="rank-wrap">
     <div class="type-list">
       <div
-        v-for="(item, index) in rankType"
+        v-for="(item, index) in rankTypeList"
         :key="index"
         :class="{ item: 1, active: item.type === currRankType }"
-        @click="currRankType = item.type"
+        @click="changeCurrRankType(item.type)"
       >
         {{ item.label }}
       </div>
@@ -32,6 +32,7 @@
           <div class="rank">
             <i>0{{ item.rank }}</i>
           </div>
+          <div v-if="item.balance">余额:{{ item.balance }}</div>
         </div>
       </div>
       <div class="top50-list">
@@ -61,36 +62,126 @@
 <script lang="ts" setup>
 import { onMounted, ref } from 'vue';
 
+import { fetchLiveRoomList } from '@/api/liveRoom';
 import { fetchUserList } from '@/api/user';
+import { fetchWalletList } from '@/api/wallet';
+import { RankTypeEnum } from '@/interface';
 
-const rankType = ref([
+export interface IRankType {
+  type: RankTypeEnum;
+  label: string;
+}
+
+const rankTypeList = ref<IRankType[]>([
   {
-    type: 1,
+    type: RankTypeEnum.liveRoom,
     label: '主播榜',
   },
   {
-    type: 2,
-    label: '打赏榜',
+    type: RankTypeEnum.user,
+    label: '用户榜',
   },
   {
-    type: 3,
-    label: '等级榜',
+    type: RankTypeEnum.wallet,
+    label: '土豪榜',
   },
 ]);
 
-const currRankType = ref(1);
+const currRankType = ref(RankTypeEnum.liveRoom);
 
 const mockRank = [
-  { username: '-', avatar: '', rank: 1, level: -1, score: -1 },
-  { username: '-', avatar: '', rank: 2, level: -1, score: -1 },
-  { username: '-', avatar: '', rank: 3, level: -1, score: -1 },
-  { username: '-', avatar: '', rank: 4, level: -1, score: -1 },
+  {
+    username: '待上榜',
+    avatar: '',
+    rank: 1,
+    level: -1,
+    score: -1,
+  },
+  {
+    username: '待上榜',
+    avatar: '',
+    rank: 2,
+    level: -1,
+    score: -1,
+  },
+  {
+    username: '待上榜',
+    avatar: '',
+    rank: 3,
+    level: -1,
+    score: -1,
+  },
 ];
 const rankList = ref(mockRank);
 
+async function getWalletList() {
+  const res = await fetchWalletList();
+  if (res.code === 200) {
+    const length = res.data.rows.length;
+    rankList.value = res.data.rows.map((item, index) => {
+      return {
+        username: item.user_username!,
+        avatar: item.user_avatar!,
+        rank: index + 1,
+        level: 1,
+        score: 1,
+        balance: item.balance,
+      };
+    });
+    if (length < 3) {
+      rankList.value.push(...mockRank.slice(length));
+    }
+  }
+}
+async function getLiveRoomList() {
+  const res = await fetchLiveRoomList({
+    orderName: 'updated_at',
+    orderBy: 'desc',
+  });
+  if (res.code === 200) {
+    const length = res.data.rows.length;
+    rankList.value = res.data.rows.map((item, index) => {
+      return {
+        username: item.user_username!,
+        avatar: item.user_avatar!,
+        rank: index + 1,
+        level: 1,
+        score: 1,
+      };
+    });
+    if (length < 3) {
+      rankList.value.push(...mockRank.slice(length));
+    }
+  }
+}
+
+function changeCurrRankType(type: RankTypeEnum) {
+  currRankType.value = type;
+  switch (type) {
+    case RankTypeEnum.liveRoom:
+      getLiveRoomList();
+      break;
+    case RankTypeEnum.user:
+      getUserList();
+      break;
+    case RankTypeEnum.wallet:
+      getWalletList();
+      break;
+    default:
+      break;
+  }
+}
+
+onMounted(() => {
+  changeCurrRankType(currRankType.value);
+});
+
 async function getUserList() {
   try {
-    const res = await fetchUserList();
+    const res = await fetchUserList({
+      orderName: 'updated_at',
+      orderBy: 'desc',
+    });
     if (res.code === 200) {
       const length = res.data.rows.length;
       rankList.value = res.data.rows.map((item, index) => {
@@ -110,10 +201,6 @@ async function getUserList() {
     console.log(error);
   }
 }
-
-onMounted(() => {
-  getUserList();
-});
 </script>
 
 <style lang="scss" scoped>

+ 22 - 167
src/views/sponsors/index.vue

@@ -47,67 +47,35 @@
         {{ item.name }}({{ item.price }}元)
       </div>
     </div>
-    <div class="qrcode-wrap">
-      <img
-        v-if="aliPayBase64 !== ''"
-        class="qrcode"
-        :src="aliPayBase64"
-        alt=""
-      />
-      <template v-if="currentPayStatus !== PayStatusEnum.error">
-        <div class="mask">
-          <div class="txt">
-            {{
-              currentPayStatus === PayStatusEnum.TRADE_SUCCESS
-                ? '支付成功'
-                : '等待支付'
-            }}
-          </div>
-        </div>
-      </template>
-    </div>
-    <div v-if="aliPayBase64 !== ''">
-      <div class="bottom">
-        <div class="sao">打开支付宝扫一扫</div>
-        <div class="expr">有效期5分钟({{ formatDownTime(downTime) }})</div>
-      </div>
-    </div>
-
-    <h3 v-if="currentPayStatus === PayStatusEnum.WAIT_BUYER_PAY">
-      ps:支付宝标题显示:东圃牛杂档,是正常的~
-    </h3>
-
-    <div
-      v-if="payOk"
-      class="bottom"
-    >
-      <h2>感谢您的赞助~</h2>
-    </div>
+    <QrPayCpt
+      v-if="showQrPay"
+      :money="goodsInfo.money"
+      :goods-id="goodsInfo.goodsId"
+      :live-room-id="goodsInfo.liveRoomId"
+    ></QrPayCpt>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { hrefToTarget, isMobile } from 'billd-utils';
-import QRCode from 'qrcode';
-import { onMounted, onUnmounted, ref } from 'vue';
+import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
 
 import { fetchGoodsList } from '@/api/goods';
-import { fetchAliPay, fetchAliPayStatus, fetchOrderList } from '@/api/order';
+import { fetchOrderList } from '@/api/order';
+import QrPayCpt from '@/components/QrPay/index.vue';
 import { GoodsTypeEnum, IGoods, IOrder, PayStatusEnum } from '@/interface';
 
-const payOk = ref(false);
 const onMountedTime = ref('');
-const aliPayBase64 = ref('');
 const payStatusTimer = ref();
 const downTimer = ref();
 const receiveMoney = ref(0);
-const downTime = ref();
-const downTimeEnd = ref();
+const showQrPay = ref(false);
+const goodsInfo = reactive({
+  money: '0.00',
+  goodsId: -1,
+  liveRoomId: -1,
+});
 
 const payList = ref<IOrder[]>([]);
-
-const currentPayStatus = ref(PayStatusEnum.error);
-
 const sponsorsGoodsList = ref<IGoods[]>([]);
 
 onUnmounted(() => {
@@ -121,49 +89,6 @@ onMounted(() => {
   getGoodsList();
 });
 
-function formatDownTime(startTime: number) {
-  const time2 = downTimeEnd.value - startTime;
-  const ms = 1;
-  const second = ms * 1000;
-  const minute = second * 60;
-  const hour = minute * 60;
-  const day = hour * 24;
-  if (time2 > day) {
-    const res = (time2 / day).toFixed(4).split('.');
-    return `${res[0]}天${Math.ceil(Number(`0.${res[1]}`) * 24)}时`;
-  } else if (time2 > hour) {
-    const res = (time2 / hour).toFixed(4).split('.');
-    return `${res[0]}时${Math.ceil(Number(`0.${res[1]}`) * 60)}分`;
-  } else if (time2 > minute) {
-    const res = (time2 / minute).toFixed(4).split('.');
-    return `${res[0]}分${Math.ceil(Number(`0.${res[1]}`) * 60)}秒`;
-  } else {
-    const res = (time2 / second).toFixed(4).split('.');
-    return `${res[0]}秒`;
-  }
-}
-
-async function generateQR(text) {
-  let base64 = '';
-  try {
-    base64 = await QRCode.toDataURL(text, {
-      margin: 1,
-    });
-  } catch (err) {
-    console.error('生成二维码失败!', err);
-  }
-  return base64;
-}
-
-function handleDownTime() {
-  clearInterval(downTimer.value);
-  downTimeEnd.value = +new Date() + 1000 * 60 * 5;
-  downTime.value = +new Date();
-  downTimer.value = setInterval(() => {
-    downTime.value = +new Date();
-  }, 1000);
-}
-
 async function getGoodsList() {
   const res = await fetchGoodsList({
     type: GoodsTypeEnum.sponsors,
@@ -191,55 +116,14 @@ async function getPayList() {
     console.log(error);
   }
 }
-async function startPay(item: IGoods) {
-  currentPayStatus.value = PayStatusEnum.error;
-  payOk.value = false;
-  clearInterval(payStatusTimer.value);
-  clearInterval(downTimer.value);
-  try {
-    const res = await fetchAliPay({
-      total_amount: item.price!,
-      subject: item.name!,
-      body: item.name!,
-    });
-    if (res.code === 200) {
-      if (isMobile()) {
-        hrefToTarget(res.data.qr_code);
-        return;
-      }
-      const base64 = await generateQR(res.data.qr_code);
-      aliPayBase64.value = base64;
-      getPayStatus(res.data.out_trade_no);
-      handleDownTime();
-    }
-  } catch (error) {
-    console.log(error);
-  }
-}
 
-function getPayStatus(outTradeNo: string) {
-  clearInterval(payStatusTimer.value);
-  payStatusTimer.value = setInterval(async () => {
-    try {
-      const res = await fetchAliPayStatus({
-        out_trade_no: outTradeNo,
-      });
-      if (res.data.tradeStatus === PayStatusEnum.WAIT_BUYER_PAY) {
-        currentPayStatus.value = PayStatusEnum.WAIT_BUYER_PAY;
-        console.log('等待支付');
-      }
-      if (res.data.tradeStatus === PayStatusEnum.TRADE_SUCCESS) {
-        currentPayStatus.value = PayStatusEnum.TRADE_SUCCESS;
-        clearInterval(downTimer.value);
-        clearInterval(payStatusTimer.value);
-        console.log('支付成功!');
-        payOk.value = true;
-        getPayList();
-      }
-    } catch (error) {
-      console.log(error);
-    }
-  }, 1000);
+function startPay(item: IGoods) {
+  showQrPay.value = false;
+  nextTick(() => {
+    goodsInfo.money = item.price!;
+    goodsInfo.goodsId = item.id!;
+    showQrPay.value = true;
+  });
 }
 </script>
 
@@ -308,34 +192,5 @@ function getPayStatus(outTradeNo: string) {
       cursor: pointer;
     }
   }
-  .qrcode-wrap {
-    position: relative;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    box-sizing: border-box;
-    margin: 20px auto 0;
-    width: 140px;
-    height: 140px;
-
-    .mask {
-      position: absolute !important;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-
-      @extend %maskBg;
-      .txt {
-        color: white;
-        font-weight: bold;
-      }
-    }
-  }
-  .bottom {
-    margin-top: 2px;
-    width: 100%;
-    text-align: center;
-    font-size: 14px;
-  }
 }
 </style>

+ 24 - 4
src/views/support/index.vue

@@ -5,7 +5,7 @@
         v-for="(item, index) in list"
         :key="index"
         class="item"
-        @click="handleClick"
+        @click="startPay(item)"
       >
         <div
           class="left"
@@ -35,16 +35,29 @@
         </div>
       </div>
     </div>
+    <QrPayCpt
+      v-if="showQrPay"
+      :money="goodsInfo.money"
+      :goods-id="goodsInfo.goodsId"
+      :live-room-id="goodsInfo.liveRoomId"
+    ></QrPayCpt>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref } from 'vue';
+import { nextTick, onMounted, reactive, ref } from 'vue';
 
 import { fetchGoodsList } from '@/api/goods';
+import QrPayCpt from '@/components/QrPay/index.vue';
 import { GoodsTypeEnum, IGoods } from '@/interface';
 
 const list = ref<IGoods[]>([]);
+const showQrPay = ref(false);
+const goodsInfo = reactive({
+  money: '0.00',
+  goodsId: -1,
+  liveRoomId: -1,
+});
 
 async function getGoodsList() {
   const res = await fetchGoodsList({
@@ -57,11 +70,18 @@ async function getGoodsList() {
     list.value = res.data.rows;
   }
 }
+
 onMounted(() => {
   getGoodsList();
 });
-function handleClick() {
-  window.$message.info('即将推出,敬请期待~');
+
+function startPay(item: IGoods) {
+  showQrPay.value = false;
+  nextTick(() => {
+    goodsInfo.money = item.price!;
+    goodsInfo.goodsId = item.id!;
+    showQrPay.value = true;
+  });
 }
 </script>