Pārlūkot izejas kodu

feat: 商品模块

shuisheng 2 gadi atpakaļ
vecāks
revīzija
2375cbdbd3

+ 6 - 0
src/api/goods.ts

@@ -0,0 +1,6 @@
+import { IGoods, IList, IPaging } from '@/interface';
+import request from '@/utils/request';
+
+export function fetchGoodsList(params: IList<IGoods>) {
+  return request.get<IPaging<IGoods>>('/goods/list', { params });
+}

BIN
src/assets/img/ad/aliyun-1.png


BIN
src/assets/img/ad/aliyun-2.jpg


BIN
src/assets/img/ad/qiniu-1.jpg


BIN
src/assets/img/ad/qiniu-2.jpg


BIN
src/assets/img/ad/qiniu-3.jpg


BIN
src/assets/img/ad/qiniu-4.jpg


BIN
src/assets/img/ad/tencent-1.jpg


BIN
src/assets/img/ad/tencent-2.png


BIN
src/assets/img/ad/tencent-3.jpg


BIN
src/assets/img/qq-logo.webp


BIN
src/assets/img/wechat-group.webp


+ 111 - 0
src/components/Modal/index.vue

@@ -0,0 +1,111 @@
+<template>
+  <div
+    class="modal-wrap"
+    @click.self="maskClose"
+  >
+    <div class="container">
+      <span class="title">{{ props.title }}</span>
+      <i
+        v-if="!hiddenClose"
+        class="close"
+        @click="emits('close')"
+      ></i>
+      <div class="content">
+        <slot></slot>
+      </div>
+      <div class="footer">
+        <slot name="footer"></slot>
+
+        <div
+          v-if="!slots.footer"
+          class="btn"
+        >
+          返回首页
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { defineEmits, defineProps, useSlots } from 'vue';
+
+const slots = useSlots();
+const props = defineProps({
+  modelValue: Boolean,
+  hiddenClose: Boolean,
+  maskClosable: Boolean,
+  title: {
+    type: String,
+    default: '',
+  },
+});
+
+function maskClose() {
+  if (props.maskClosable) {
+    emits('close');
+  }
+}
+
+const emits = defineEmits(['close']);
+</script>
+
+<style lang="scss" scoped>
+.modal-wrap {
+  z-index: 1;
+  background-color: rgba(0, 0, 0, 0.7) !important;
+
+  @extend %maskBg;
+  .container {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    box-sizing: border-box;
+    padding: 20px;
+    width: 320px;
+    height: 200px;
+    border-radius: 10px;
+    background-color: #fff;
+    font-size: 14px;
+    transform: translate(-50%, -50%);
+    .title {
+      font-weight: 700;
+      font-size: 24px;
+    }
+    .close {
+      position: absolute;
+      top: 20px;
+      right: 20px;
+      width: 18px;
+      height: 18px;
+      cursor: pointer;
+
+      @include cross(#ccc, 3px);
+    }
+    .content {
+      margin-top: 10px;
+    }
+    .footer {
+      position: absolute;
+      right: 20px;
+      bottom: 20px;
+      left: 20px;
+
+      .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%);
+        color: white;
+        text-align: center;
+        font-weight: 600;
+        font-size: 16px;
+        line-height: 44px;
+        cursor: pointer;
+      }
+    }
+  }
+}
+</style>

+ 58 - 0
src/hooks/loginModal/index.vue

@@ -0,0 +1,58 @@
+<template>
+  <div
+    v-if="show"
+    class="useTip-wrap"
+  >
+    <ModalCpt
+      :title="title"
+      :mask-closable="maskClosable"
+      @close="show = !show"
+    >
+      <div class="container">
+        <img
+          class="qq-logo"
+          src="@/assets/img/qq-logo.webp"
+          alt=""
+          @click="useQQLogin()"
+        />
+        <div>qq登录</div>
+      </div>
+
+      <template #footer></template>
+    </ModalCpt>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue';
+
+import ModalCpt from '@/components/Modal/index.vue';
+import { useQQLogin } from '@/hooks/use-login';
+
+export default defineComponent({
+  components: { ModalCpt },
+  setup() {
+    const title = ref('登录');
+    const show = ref(false);
+    const maskClosable = ref(true);
+    return {
+      title,
+      show,
+      maskClosable,
+      useQQLogin,
+    };
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.useTip-wrap {
+  .container {
+    text-align: center;
+    .qq-logo {
+      cursor: pointer;
+      width: 60px;
+    }
+  }
+}
+</style>

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

@@ -0,0 +1,104 @@
+<template>
+  <div
+    v-if="show"
+    class="useTip-wrap"
+  >
+    <ModalCpt
+      :title="title"
+      :mask-closable="maskClosable"
+      @close="handleCancel()"
+    >
+      {{ msg }}
+      <template #footer>
+        <div class="footer">
+          <div
+            v-if="!hiddenCancel"
+            class="btn return"
+            @click="handleCancel()"
+          >
+            取消
+          </div>
+          <div
+            :class="{ btn: 1, next: 1, hiddenCancel }"
+            @click="handleOk()"
+          >
+            确认
+          </div>
+        </div>
+      </template>
+    </ModalCpt>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue';
+
+import ModalCpt from '@/components/Modal/index.vue';
+
+export default defineComponent({
+  components: { ModalCpt },
+  emits: ['ok', 'cancel'],
+  setup() {
+    const title = ref('提示');
+    const msg = ref('');
+    const show = ref(false);
+    const hiddenCancel = ref(false);
+    const hiddenClose = ref(false);
+    const maskClosable = ref(true);
+    function handleCancel(cb?) {
+      cb?.();
+    }
+    function handleOk(cb?) {
+      cb?.();
+    }
+    return {
+      title,
+      msg,
+      show,
+      hiddenCancel,
+      hiddenClose,
+      maskClosable,
+      handleCancel,
+      handleOk,
+    };
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.useTip-wrap {
+  .footer {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    .btn {
+      box-sizing: border-box;
+      width: 130px;
+      height: 44px;
+      border-radius: 100px;
+      text-align: center;
+      line-height: 44px;
+
+      user-select: none;
+
+      &.return {
+        border: 1px solid rgba(153, 153, 153, 0.3);
+        background: #ffffff;
+        color: #666;
+        font-size: 14px;
+      }
+      &.next {
+        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%);
+        color: white;
+        font-weight: 700;
+        font-size: 16px;
+        &.hiddenCancel {
+          width: 100%;
+        }
+      }
+    }
+  }
+}
+</style>

+ 26 - 6
src/hooks/use-login.ts

@@ -1,14 +1,24 @@
 import { hrefToTarget, isMobile } from 'billd-utils';
+import { createApp } from 'vue';
 
 import { fetchQQLogin } from '@/api/qqUser';
 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 { clearLoginInfo, setLoginInfo } from '@/utils/cookie';
 
+const app = createApp(LoginModalCpt);
+const container = document.createElement('div');
+// @ts-ignore
+const instance: ComponentPublicInstance<InstanceType<typeof LoginModalCpt>> =
+  app.mount(container);
+
+document.body.appendChild(container);
+
 const POSTMESSAGE_TYPE = [PlatformEnum.qqLogin];
 
-export const handleLogin = async (e) => {
+export async function handleLogin(e) {
   const { type, data } = e.data;
   if (!POSTMESSAGE_TYPE.includes(type)) return;
   console.log('收到消息', type, data);
@@ -31,13 +41,23 @@ export const handleLogin = async (e) => {
   } finally {
     clearLoginInfo();
   }
-};
+}
+
+export function loginTip() {
+  const userStore = useUserStore();
+  if (!userStore.userInfo) {
+    window.$message.warning('请先登录~');
+    instance.show = true;
+    return false;
+  }
+  return true;
+}
 
-export const loginMessage = () => {
+export function loginMessage() {
   window.addEventListener('message', handleLogin);
-};
+}
 
-export const useQQLogin = () => {
+export function useQQLogin() {
   const url = (state: string) =>
     `${QQ_OAUTH_URL}/authorize?response_type=code&client_id=${QQ_CLIENT_ID}&redirect_uri=${QQ_REDIRECT_URI}&scope=get_user_info,get_vip_info,get_vip_rich_info&state=${state}`;
   let loginInfo = JSON.stringify({
@@ -58,4 +78,4 @@ export const useQQLogin = () => {
       'toolbar=yes,location=no,directories=no,status=no,menubar=no,scrollbars=no,titlebar=no,toolbar=no,resizable=no,copyhistory=yes, width=918, height=609,top=250,left=400'
     );
   }
-};
+}

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

@@ -24,6 +24,8 @@ import {
 import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
 
+import { loginTip } from './use-login';
+
 export function usePush({
   localVideoRef,
   remoteVideoRef,
@@ -95,14 +97,14 @@ export function usePush({
   }>();
 
   function startLive() {
+    if (!loginTip()) return;
     if (!roomNameIsOk()) return;
     if (currMediaTypeList.value.length <= 0) {
       window.$message.warning('请选择一个素材!');
       return;
     }
     disabled.value = true;
-
-    const ws = new WebSocketClass({
+    const instance = new WebSocketClass({
       roomId: roomId.value,
       url:
         process.env.NODE_ENV === 'development'
@@ -110,7 +112,7 @@ export function usePush({
           : 'wss://live.hsslive.cn',
       isAdmin: true,
     });
-    ws.update();
+    instance.update();
     initReceive();
   }
 

+ 32 - 0
src/hooks/use-tip.ts

@@ -0,0 +1,32 @@
+import { ComponentPublicInstance, createApp } from 'vue';
+
+import ModalCpt from './modal/index.vue';
+
+const app = createApp(ModalCpt);
+const container = document.createElement('div');
+// @ts-ignore
+const instance: ComponentPublicInstance<InstanceType<typeof ModalCpt>> =
+  app.mount(container);
+
+document.body.appendChild(container);
+
+export function useTip(
+  msg: string,
+  hiddenCancel?: boolean,
+  hiddenClose?: boolean
+) {
+  instance.show = true;
+  instance.msg = msg;
+  instance.hiddenCancel = !!hiddenCancel;
+  instance.hiddenClose = !!hiddenClose;
+  return new Promise((resolve, reject) => {
+    instance.handleOk = () => {
+      instance.show = false;
+      resolve('ok');
+    };
+    instance.handleCancel = () => {
+      instance.show = false;
+      reject('cancel');
+    };
+  });
+}

+ 25 - 1
src/interface.ts

@@ -27,8 +27,8 @@ export interface IPaging<T> {
 
 export interface IOrder {
   id?: number;
+  user?: any; // 用户信息
   billd_live_user_id?: number;
-  user: IUser;
   out_trade_no?: string;
   total_amount?: string;
   subject?: string;
@@ -48,6 +48,30 @@ export interface IOrder {
   deleted_at?: string;
 }
 
+export enum GoodsTypeEnum {
+  support = 'support',
+  sponsors = 'sponsors',
+  gift = 'gift',
+}
+
+export interface IGoods {
+  id?: number;
+  type?: GoodsTypeEnum;
+  name?: string;
+  desc?: string;
+  short_desc?: string;
+  cover?: string;
+  price?: string;
+  original_price?: string;
+  nums?: number;
+  badge?: string;
+  badge_bg?: string;
+  remark?: string;
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+}
+
 export enum liveTypeEnum {
   webrtcPull = 'webrtcPull',
   srsWebrtcPull = 'srsWebrtcPull',

+ 49 - 14
src/layout/head/index.vue

@@ -28,6 +28,36 @@
         >
           付费支持
         </a>
+        <a
+          :class="{
+            item: 1,
+            active: router.currentRoute.value.name === routerName.sponsors,
+          }"
+          href="/sponsors"
+          @click.prevent="router.push({ name: routerName.sponsors })"
+        >
+          赞助
+        </a>
+        <a
+          :class="{
+            item: 1,
+            active: router.currentRoute.value.name === routerName.about,
+          }"
+          href="/about"
+          @click.prevent="router.push({ name: routerName.about })"
+        >
+          关于
+        </a>
+        <a
+          :class="{
+            item: 1,
+            active: router.currentRoute.value.name === routerName.ad,
+          }"
+          href="/ad"
+          @click.prevent="router.push({ name: routerName.ad })"
+        >
+          广告
+        </a>
         <div
           v-for="(item, index) in navLeftList.filter(
             (item) => router.currentRoute.value.query.liveType === item.liveType
@@ -76,20 +106,26 @@
         ></div>
       </n-dropdown>
 
-      <a
-        class="sponsors"
+      <!-- <a
+        :class="{
+          sponsors: 1,
+          active: router.currentRoute.value.name === routerName.sponsors,
+        }"
         href="/sponsors"
         @click.prevent="router.push({ name: routerName.sponsors })"
       >
         赞助
       </a>
       <a
-        class="about"
+        :class="{
+          about: 1,
+          active: router.currentRoute.value.name === routerName.about,
+        }"
         href="/about"
         @click.prevent="router.push({ name: routerName.about })"
       >
         关于
-      </a>
+      </a> -->
       <a
         class="bilibili"
         target="_blank"
@@ -126,7 +162,7 @@ import { openToTarget } from 'billd-utils';
 import { onMounted, ref } from 'vue';
 import { useRouter } from 'vue-router';
 
-import { useQQLogin } from '@/hooks/use-login';
+import { loginTip, useQQLogin } from '@/hooks/use-login';
 import { liveTypeEnum } from '@/interface';
 import { routerName } from '@/router';
 import { useUserStore } from '@/store/user';
@@ -195,13 +231,14 @@ function handleUserSelect(key) {
   }
 }
 function handlePushSelect(key) {
-  if (key === liveTypeEnum.webrtcPush || key === liveTypeEnum.srsPush) {
-    const url = router.resolve({
-      name: routerName.push,
-      query: { liveType: key },
-    });
-    openToTarget(url.href);
+  if (!loginTip()) {
+    return;
   }
+  const url = router.resolve({
+    name: routerName.push,
+    query: { liveType: key },
+  });
+  openToTarget(url.href);
 }
 
 function goPushPage(routerName: string) {
@@ -290,9 +327,7 @@ function goPushPage(routerName: string) {
       cursor: pointer;
     }
 
-    .sponsors,
     .bilibili,
-    .about,
     .github {
       position: relative;
       margin-right: 15px;
@@ -306,7 +341,7 @@ function goPushPage(routerName: string) {
           position: absolute;
           bottom: -6px;
           left: 50%;
-          width: 40%;
+          width: 40% !important;
           height: 2px;
           background-color: red;
           content: '';

+ 1 - 0
src/main.scss

@@ -1,4 +1,5 @@
 body {
   padding: 0;
   margin: 0;
+  font-family: PingFang SC;
 }

+ 6 - 0
src/router/index.ts

@@ -10,6 +10,7 @@ export const routerName = {
   rank: 'rank',
   sponsors: 'sponsors',
   support: 'support',
+  ad: 'ad',
   oauth: 'oauth',
   notFound: 'notFound',
 
@@ -48,6 +49,11 @@ export const defaultRoutes: RouteRecordRaw[] = [
         path: '/support',
         component: () => import('@/views/support/index.vue'),
       },
+      {
+        name: routerName.ad,
+        path: '/ad',
+        component: () => import('@/views/ad/index.vue'),
+      },
       {
         name: routerName.pull,
         path: '/pull/:roomId',

+ 2 - 2
src/views/about/index.vue

@@ -1,9 +1,9 @@
 <template>
   <div class="about-wrap">
-    <h2>目前实现</h2>
+    <h2>billd-live目前实现</h2>
     <h3>1. 原生webrtc一对多直播(DONE)</h3>
     <h3>2. srs-webrtc一对多直播(DONE)</h3>
-    <h3>3. 原生webrtc多对多直播(TODO)</h3>
+    <h3>3. 原生webrtc多对多直播(DONE)</h3>
 
     <h1>微信交流群 & 我的微信</h1>
     <img

+ 111 - 0
src/views/ad/index.vue

@@ -0,0 +1,111 @@
+<template>
+  <div class="ad-wrap">
+    <div class="ad-list">
+      <div
+        v-for="(item, index) in list"
+        :key="index"
+        class="item"
+        @click="openToTarget(item.url)"
+      >
+        <div>
+          <img
+            class="cover"
+            :src="item.cover"
+            alt=""
+          />
+        </div>
+        <div>{{ item.name }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { openToTarget } from 'billd-utils';
+import { ref } from 'vue';
+
+const list = ref([
+  {
+    // eslint-disable-next-line
+    cover: require('@/assets/img/ad/tencent-1.jpg'),
+    name: '【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中',
+    url: 'https://cloud.tencent.com/act/cps/redirect?redirect=2446&cps_key=ebe55ad4d940d688bcde548b101dff5f&from=console',
+  },
+  {
+    // eslint-disable-next-line
+    cover: require('@/assets/img/ad/tencent-2.png'),
+    name: '【腾讯云】爆品抢先购,预热专属折上折券限时领!',
+    url: 'https://cloud.tencent.com/act/cps/redirect?redirect=undefined&cps_key=ebe55ad4d940d688bcde548b101dff5f&from=console',
+  },
+  {
+    // eslint-disable-next-line
+    cover: require('@/assets/img/ad/tencent-3.jpg'),
+    name: '【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。',
+    url: 'https://cloud.tencent.com/act/cps/redirect?redirect=1040&cps_key=ebe55ad4d940d688bcde548b101dff5f&from=console',
+  },
+  {
+    // eslint-disable-next-line
+    cover: require('@/assets/img/ad/aliyun-1.png'),
+    name: '云小站特惠超底价',
+    url: 'https://www.aliyun.com/minisite/goods?userCode=41m2k6bt',
+  },
+  {
+    // eslint-disable-next-line
+    cover: require('@/assets/img/ad/aliyun-2.jpg'),
+    name: '云服务器t6 2核2G低至10.14元/月',
+    url: 'https://www.aliyun.com/daily-act/ecs/activity_share?userCode=41m2k6bt',
+  },
+  {
+    // eslint-disable-next-line
+    cover: require('@/assets/img/ad/qiniu-1.jpg'),
+    name: '新人免费 CDN(全球2000+节点无盲区覆盖,注册即可免费使用)',
+    url: 'https://s.qiniu.com/Q7zqeq',
+  },
+  {
+    // eslint-disable-next-line
+    cover: require('@/assets/img/ad/qiniu-2.jpg'),
+    name: '对象存储新人好礼(10年专注云存储,注册即可免费使用)',
+    url: 'https://s.qiniu.com/iQR7Jz',
+  },
+  {
+    // eslint-disable-next-line
+    cover: require('@/assets/img/ad/qiniu-3.jpg'),
+    name: '新人免费试用(多款云产品长期免费使用,注册即享超值赠送)',
+    url: 'https://s.qiniu.com/JnIbyq',
+  },
+  {
+    // eslint-disable-next-line
+    cover: require('@/assets/img/ad/qiniu-4.jpg'),
+    name: '新人免费云主机(注册免费领取 4核8G 云服务器,享免费数据迁移服务)',
+    url: 'https://s.qiniu.com/B3i63y',
+  },
+]);
+</script>
+
+<style lang="scss" scoped>
+.ad-wrap {
+  .ad-list {
+    padding: 20px;
+
+    .item {
+      display: flex;
+      align-items: center;
+      margin-right: 20px;
+      padding: 10px;
+      border-radius: 10px;
+      box-shadow: 0 4px 30px 0 rgba(238, 242, 245, 0.8);
+      cursor: pointer;
+      transition: box-shadow 0.2s linear;
+      margin-top: 10px;
+      margin-bottom: 10px;
+      &:hover {
+        box-shadow: 4px 4px 20px 0 rgba(205, 216, 228, 0.6);
+      }
+      .cover {
+        width: 300px;
+        margin-right: 10px;
+      }
+    }
+  }
+}
+</style>

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

@@ -231,6 +231,7 @@ function joinFlvRoom() {
         cursor: pointer;
         &:hover {
           background-color: rgba($color: skyblue, $alpha: 0.5);
+          color: white;
         }
         &.webrtc {
           margin-right: 10px;

+ 62 - 17
src/views/pull/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="srs-webrtc-pull-wrap">
+  <div class="pull-wrap">
     <template v-if="roomNoLive">当前房间没在直播~</template>
     <template v-else>
       <div class="left">
@@ -73,16 +73,27 @@
 
         <div
           ref="bottomRef"
-          class="gift"
+          class="gift-list"
         >
           <div
-            v-for="(item, index) in giftList"
+            v-for="(item, index) in giftGoodsList"
             :key="index"
             class="item"
           >
-            <div class="ico"></div>
+            <div
+              class="ico"
+              :style="{ backgroundImage: `url(${item.cover})` }"
+            >
+              <div
+                v-if="item.badge"
+                class="badge"
+                :style="{ backgroundColor: item.badge_bg }"
+              >
+                <span class="txt">{{ item.badge }}</span>
+              </div>
+            </div>
             <div class="name">{{ item.name }}</div>
-            <div class="price">{{ item.price }}</div>
+            <div class="price">{{ item.price }}</div>
           </div>
         </div>
       </div>
@@ -167,8 +178,14 @@
 import { nextTick, onMounted, onUnmounted, ref } from 'vue';
 import { useRoute } from 'vue-router';
 
+import { fetchGoodsList } from '@/api/goods';
 import { usePull } from '@/hooks/use-pull';
-import { DanmuMsgTypeEnum, liveTypeEnum } from '@/interface';
+import {
+  DanmuMsgTypeEnum,
+  GoodsTypeEnum,
+  IGoods,
+  liveTypeEnum,
+} from '@/interface';
 import { useAppStore } from '@/store/app';
 import { useUserStore } from '@/store/user';
 
@@ -176,6 +193,7 @@ const route = useRoute();
 const userStore = useUserStore();
 const appStore = useAppStore();
 
+const giftGoodsList = ref<IGoods[]>([]);
 const showJoin = ref(true);
 const topRef = ref<HTMLDivElement>();
 const bottomRef = ref<HTMLDivElement>();
@@ -190,10 +208,7 @@ const {
   getSocketId,
   keydownDanmu,
   sendDanmu,
-  batchSendOffer,
   startGetUserMedia,
-  startGetDisplayMedia,
-  addTrack,
   addVideo,
   roomName,
   roomNoLive,
@@ -201,7 +216,6 @@ const {
   giftList,
   liveUserList,
   danmuStr,
-  localStream,
   sidebarList,
 } = usePull({
   localVideoRef,
@@ -210,10 +224,20 @@ const {
   isSRS: route.query.liveType === liveTypeEnum.srsWebrtcPull,
 });
 
+async function getGoodsList() {
+  const res = await fetchGoodsList({
+    type: GoodsTypeEnum.gift,
+    orderName: 'created_at',
+    orderBy: 'desc',
+  });
+  if (res.code === 200) {
+    giftGoodsList.value = res.data.rows;
+  }
+}
+
 function handleJoin() {
   showJoin.value = !showJoin.value;
   nextTick(async () => {
-    // await startGetDisplayMedia();
     await startGetUserMedia();
     addVideo();
   });
@@ -225,6 +249,7 @@ onUnmounted(() => {
 });
 
 onMounted(() => {
+  getGoodsList();
   if (
     [liveTypeEnum.srsFlvPull, liveTypeEnum.srsWebrtcPull].includes(
       route.query.liveType as liveTypeEnum
@@ -244,7 +269,7 @@ onMounted(() => {
 </script>
 
 <style lang="scss" scoped>
-.srs-webrtc-pull-wrap {
+.pull-wrap {
   margin: 20px auto 0;
   min-width: $large-width;
   height: 700px;
@@ -314,7 +339,6 @@ onMounted(() => {
               width: 10px;
               height: 10px;
               border-radius: 50%;
-              background-color: skyblue;
             }
           }
         }
@@ -360,7 +384,7 @@ onMounted(() => {
       }
     }
 
-    .gift {
+    .gift-list {
       position: absolute;
       right: 0;
       bottom: 0;
@@ -368,15 +392,36 @@ onMounted(() => {
       display: flex;
       align-items: center;
       justify-content: space-around;
-      height: 100px;
+      height: 120px;
       .item {
         margin-right: 10px;
         text-align: center;
+        cursor: pointer;
 
         .ico {
+          position: relative;
           width: 50px;
           height: 50px;
-          background-color: skyblue;
+          background-position: center center;
+          background-size: cover;
+          background-repeat: no-repeat;
+          .badge {
+            position: absolute;
+            top: -10px;
+            right: -10px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            border-radius: 2px;
+            padding: 2px;
+            color: white;
+            .txt {
+              display: inline-block;
+              transform-origin: center !important;
+              line-height: 1;
+              @include minFont(10);
+            }
+          }
         }
         .name {
           color: #18191c;
@@ -490,7 +535,7 @@ onMounted(() => {
 
 // 屏幕宽度小于$large-width的时候
 @media screen and (max-width: $large-width) {
-  .srs-webrtc-pull-wrap {
+  .pull-wrap {
     .left {
       width: $medium-left-width;
     }

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

@@ -1,5 +1,5 @@
 <template>
-  <div class="webrtc-push-wrap">
+  <div class="push-wrap">
     <div
       ref="topRef"
       class="left"
@@ -253,7 +253,7 @@ onMounted(() => {
 </script>
 
 <style lang="scss" scoped>
-.webrtc-push-wrap {
+.push-wrap {
   margin: 20px auto 0;
   min-width: $large-width;
   height: 700px;
@@ -476,7 +476,7 @@ onMounted(() => {
 }
 // 屏幕宽度小于$large-width的时候
 @media screen and (max-width: $large-width) {
-  .webrtc-push-wrap {
+  .push-wrap {
     .left {
       width: $medium-left-width;
     }

+ 21 - 39
src/views/sponsors/index.vue

@@ -39,12 +39,12 @@
     <h2>开始赞助(支付宝)</h2>
     <div class="gift-list">
       <div
-        v-for="(item, index) in giftList"
+        v-for="(item, index) in sponsorsGoodsList"
         :key="index"
         class="item"
         @click="startPay(item)"
       >
-        {{ item.subject }}({{ item.total_amount }}元)
+        {{ item.name }}({{ item.price }}元)
       </div>
     </div>
     <div class="qrcode-wrap">
@@ -91,8 +91,9 @@ import { hrefToTarget, isMobile } from 'billd-utils';
 import QRCode from 'qrcode';
 import { onMounted, onUnmounted, ref } from 'vue';
 
+import { fetchGoodsList } from '@/api/goods';
 import { fetchAliPay, fetchAliPayStatus, fetchOrderList } from '@/api/order';
-import { IOrder, PayStatusEnum } from '@/interface';
+import { GoodsTypeEnum, IGoods, IOrder, PayStatusEnum } from '@/interface';
 
 const payOk = ref(false);
 const onMountedTime = ref('');
@@ -107,38 +108,7 @@ const payList = ref<IOrder[]>([]);
 
 const currentPayStatus = ref(PayStatusEnum.error);
 
-const giftList = ref([
-  {
-    total_amount: '0.10',
-    subject: '一根辣条',
-    body: 'aaa',
-  },
-  {
-    total_amount: '1.00',
-    subject: '一根烤肠',
-    body: 'bbb',
-  },
-  {
-    total_amount: '5.00',
-    subject: '一瓶奶茶',
-    body: 'ccc',
-  },
-  {
-    total_amount: '10.00',
-    subject: '一杯咖啡',
-    body: 'ddd',
-  },
-  {
-    total_amount: '50.00',
-    subject: '一顿麦当劳',
-    body: 'eee',
-  },
-  {
-    total_amount: '100.00',
-    subject: '一顿海底捞',
-    body: 'fff',
-  },
-]);
+const sponsorsGoodsList = ref<IGoods[]>([]);
 
 onUnmounted(() => {
   clearInterval(payStatusTimer.value);
@@ -148,6 +118,7 @@ onUnmounted(() => {
 onMounted(() => {
   onMountedTime.value = new Date().toLocaleString();
   getPayList();
+  getGoodsList();
 });
 
 function formatDownTime(startTime: number) {
@@ -193,6 +164,17 @@ function handleDownTime() {
   }, 1000);
 }
 
+async function getGoodsList() {
+  const res = await fetchGoodsList({
+    type: GoodsTypeEnum.sponsors,
+    orderName: 'created_at',
+    orderBy: 'desc',
+  });
+  if (res.code === 200) {
+    sponsorsGoodsList.value = res.data.rows;
+  }
+}
+
 async function getPayList() {
   try {
     const res = await fetchOrderList({
@@ -209,16 +191,16 @@ async function getPayList() {
     console.log(error);
   }
 }
-async function startPay(item) {
+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.total_amount,
-      subject: item.subject,
-      body: item.body,
+      total_amount: item.price!,
+      subject: item.name!,
+      body: item.name!,
     });
     if (res.code === 200) {
       if (isMobile()) {

+ 49 - 23
src/views/support/index.vue

@@ -9,8 +9,16 @@
       >
         <div
           class="left"
-          :style="{ backgroundImage: `url(${item.img})` }"
-        ></div>
+          :style="{ backgroundImage: `url(${item.cover})` }"
+        >
+          <div
+            v-if="item.badge"
+            class="badge"
+            :style="{ backgroundColor: item.badge_bg }"
+          >
+            <div class="txt">{{ item.badge }}</div>
+          </div>
+        </div>
         <div class="right">
           <div class="title">{{ item.name }}</div>
           <div class="info">100%好评</div>
@@ -18,10 +26,10 @@
           <div class="price-wrap">
             <span class="price">¥{{ item.price }}</span>
             <del
-              v-if="item.price !== item.originalPrice"
+              v-if="item.price !== item.original_price"
               class="original-price"
             >
-              {{ item.originalPrice }}
+              {{ item.original_price }}
             </del>
           </div>
         </div>
@@ -31,27 +39,27 @@
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { onMounted, ref } from 'vue';
+
+import { fetchGoodsList } from '@/api/goods';
+import { GoodsTypeEnum, IGoods } from '@/interface';
 
-const list = ref([
-  {
-    // eslint-disable-next-line
-    img: require('@/assets/img/billd.webp'),
-    name: '1对1解答(一小时)',
-    price: '50.00',
-    originalPrice: '50.00',
-    desc: '包括但不限于billd-live相关的任何问题。',
-  },
-  {
-    // eslint-disable-next-line
-    img: require('@/assets/img/billd2.webp'),
-    name: '1对1解答(三小时)',
-    price: '120.00',
-    originalPrice: '150.00',
-    desc: '包括但不限于billd-live相关的任何问题。',
-  },
-]);
+const list = ref<IGoods[]>([]);
 
+async function getGoodsList() {
+  const res = await fetchGoodsList({
+    type: GoodsTypeEnum.support,
+    orderName: 'created_at',
+    orderBy: 'desc',
+  });
+  if (res.code === 200) {
+    console.log(res.data);
+    list.value = res.data.rows;
+  }
+}
+onMounted(() => {
+  getGoodsList();
+});
 function handleClick() {
   window.$message.info('即将推出,敬请期待~');
 }
@@ -75,12 +83,30 @@ function handleClick() {
         box-shadow: 4px 4px 20px 0 rgba(205, 216, 228, 0.6);
       }
       .left {
+        position: relative;
         margin-right: 10px;
         width: 100px;
         height: 100px;
         background-position: center center;
         background-size: cover;
         background-repeat: no-repeat;
+        .badge {
+          position: absolute;
+          top: -10px;
+          right: -10px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          border-radius: 2px;
+          padding: 2px;
+          color: white;
+          .txt {
+            display: inline-block;
+            transform-origin: center !important;
+            line-height: 1;
+            @include minFont(10);
+          }
+        }
       }
       .right {
         .title {