shuisheng hace 2 años
padre
commit
874a540e66

+ 45 - 0
src/components/Dropdown/index.vue

@@ -0,0 +1,45 @@
+<template>
+  <div class="dropdown-wrap">
+    <div class="btn">
+      <slot name="btn"></slot>
+    </div>
+    <div class="container">
+      <div class="wrap">
+        <slot name="list"></slot>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup></script>
+
+<style lang="scss" scoped>
+.dropdown-wrap {
+  position: relative;
+  &:hover {
+    .container {
+      display: block;
+    }
+  }
+  .btn {
+    cursor: pointer;
+  }
+  .container {
+    position: absolute;
+    top: 100%;
+    right: 0;
+    z-index: 2;
+    display: none;
+    .wrap {
+      box-sizing: border-box;
+      margin-top: 5px;
+      padding: 10px 0;
+      border-radius: 5px;
+      background-color: #fff;
+      box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08);
+      color: black;
+      font-size: 13px;
+    }
+  }
+}
+</style>

+ 69 - 0
src/components/FullLoading/index.ts

@@ -0,0 +1,69 @@
+import { ComponentPublicInstance, StyleValue, createApp } from 'vue';
+
+import main from './main.vue';
+
+const initInstance = (option: IOption) => {
+  // 这里就是与vue2最大的区别了,在vue2的时候,我们只需instance.$mount()便能得到节点,现在不行
+  const app = createApp(main);
+  const container = document.createElement('div');
+  // @ts-ignore
+  const instance: ComponentPublicInstance<InstanceType<typeof main>> =
+    app.mount(container);
+  if (option.el) {
+    instance.isFixed = false;
+    option.el.appendChild(container);
+  } else {
+    instance.isFixed = true;
+    document.body.appendChild(container);
+  }
+  return instance;
+};
+
+interface IOption {
+  content?: string;
+  style?: StyleValue;
+  loading?: boolean;
+  showMask?: boolean;
+  el?: HTMLElement;
+}
+
+const defaultOption: IOption = {
+  content: '',
+  showMask: false,
+  style: {},
+};
+
+let globalLoading;
+
+// 直接导出该方法
+export const fullLoading = function (option: IOption): IOption {
+  const newOption = {
+    ...defaultOption,
+    ...option,
+  };
+  // 没有传el,代表是全局的loading,全局loading的话就使用单例
+  if (!newOption.el) {
+    if (!globalLoading) {
+      globalLoading = initInstance(newOption);
+    }
+    Object.keys(newOption).forEach((key) => {
+      globalLoading[key] = option?.[key] || newOption[key];
+    });
+    return globalLoading;
+  } else {
+    const cptLoading = initInstance(newOption);
+    Object.keys(newOption).forEach((key) => {
+      cptLoading[key] = option?.[key] || newOption[key];
+    });
+    return cptLoading;
+  }
+};
+
+// 不推荐。
+// export const useFullLoading = {
+//   install: (app: App) => {
+//     console.log('kkkkkk', app);
+//     // 挂载在根实例的全局配置上
+//     app.config.globalProperties['$fullLoading'] = FullLoading;
+//   },
+// };

+ 57 - 0
src/components/FullLoading/main.vue

@@ -0,0 +1,57 @@
+<template>
+  <div
+    v-show="loading"
+    :class="{ 'full-loading-wrap': 1, [isFixed ? 'fixed' : 'absolute']: 1 }"
+  >
+    <div :style="style">
+      <div
+        v-if="showMask"
+        class="mask"
+      ></div>
+      <div class="container"></div>
+      <div class="txt">{{ content }}</div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { StyleValue, defineComponent, ref } from 'vue';
+
+export default defineComponent({
+  setup() {
+    const isFixed = ref(false);
+    const loading = ref(false);
+    const showMask = ref(false);
+    const content = ref('');
+    const style = ref<StyleValue>();
+    return { content, style, loading, showMask, isFixed };
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+@import 'billd-scss/src/animate/loading-size.scss';
+
+.full-loading-wrap {
+  z-index: 10;
+
+  @extend %flexCenter;
+  &.fixed {
+    @include full(fixed);
+  }
+  &.absolute {
+    @include full(absolute);
+  }
+  .mask {
+    @extend %maskBg;
+  }
+  .container {
+    @include loadingSizeChange(30px, rgba($theme-color-gold, 0.5));
+  }
+  .txt {
+    margin-top: 10px;
+    font-size: 14px;
+    position: relative;
+  }
+}
+</style>

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

@@ -172,7 +172,7 @@ function getPayStatus(outTradeNo: string) {
     } catch (error) {
       console.log(error);
     }
-  }, 1000);
+  }, 2000);
 }
 </script>
 

+ 7 - 0
src/directives/index.ts

@@ -0,0 +1,7 @@
+import { App } from 'vue';
+
+import directiveLoading from './loading';
+
+export default function registerDirectives(app: App) {
+  app.directive('loading', directiveLoading);
+}

+ 51 - 0
src/directives/loading/index.ts

@@ -0,0 +1,51 @@
+import { App, ComponentPublicInstance, Directive, createApp } from 'vue';
+
+import main from '@/components/FullLoading/main.vue';
+
+const map = new Map<
+  string,
+  {
+    app: App<Element>;
+    instance: ComponentPublicInstance<InstanceType<typeof main>>;
+  }
+>();
+
+export default <Directive>{
+  // 在绑定元素的 attribute 前
+  // 或事件监听器应用前调用
+  created() {},
+  // 在元素被插入到 DOM 前调用
+  beforeMount() {},
+  // 在绑定元素的父组件
+  // 及他自己的所有子节点都挂载完成后调用
+  mounted(el, binding, vnode) {
+    const { value } = binding;
+    const app = createApp(main);
+    const container = document.createElement('div');
+    // @ts-ignore
+    const instance: ComponentPublicInstance<InstanceType<typeof main>> =
+      app.mount(container);
+    el.appendChild(container);
+    instance.loading = value;
+    vnode.scopeId && map.set(vnode.scopeId, { app, instance });
+    return instance;
+  },
+  // 绑定元素的父组件更新前调用
+  beforeUpdate() {},
+  // 在绑定元素的父组件及他自己的所有子节点都更新后调用
+  updated(el, binding, vnode) {
+    const { value } = binding;
+    if (vnode.scopeId) {
+      const res = map.get(vnode.scopeId);
+      if (res) {
+        res.instance.loading = value;
+      }
+    }
+  },
+  // 绑定元素的父组件卸载前调用
+  beforeUnmount() {},
+  // 绑定元素的父组件卸载后调用
+  unmounted(el, binding, vnode) {
+    vnode.scopeId && map.get(vnode.scopeId)?.app.unmount();
+  },
+};

+ 10 - 0
src/hooks/use-login.ts

@@ -2,6 +2,7 @@ import { hrefToTarget, isMobile } from 'billd-utils';
 import { createApp } from 'vue';
 
 import { fetchQQLogin } from '@/api/qqUser';
+import { fullLoading } from '@/components/FullLoading';
 import { QQ_CLIENT_ID, QQ_OAUTH_URL, QQ_REDIRECT_URI } from '@/constant';
 import LoginModalCpt from '@/hooks/loginModal/index.vue';
 import { PlatformEnum } from '@/interface';
@@ -31,6 +32,9 @@ export async function handleLogin(e) {
         const res = await fetchQQLogin(data);
         if (res.code === 200) {
           window.$message.success('登录成功!');
+          fullLoading({
+            loading: false,
+          });
         }
         userStore.setToken(res.data);
         userStore.getUserInfo();
@@ -60,6 +64,12 @@ export function loginMessage() {
 }
 
 export function useQQLogin() {
+  fullLoading({
+    loading: true,
+    showMask: true,
+    content: 'qq登录...',
+    style: { color: 'white' },
+  });
   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({

+ 229 - 201
src/layout/head/index.vue

@@ -23,22 +23,22 @@
         <a
           class="item"
           :class="{
-            active: router.currentRoute.value.name === routerName.rank,
+            active: router.currentRoute.value.name === routerName.shop,
           }"
-          href="/rank"
-          @click.prevent="router.push({ name: routerName.rank })"
+          href="/shop"
+          @click.prevent="router.push({ name: routerName.shop })"
         >
-          排行榜
+          商店
         </a>
         <a
           class="item"
           :class="{
-            active: router.currentRoute.value.name === routerName.support,
+            active: router.currentRoute.value.name === routerName.order,
           }"
-          href="/support"
-          @click.prevent="router.push({ name: routerName.support })"
+          href="/order"
+          @click.prevent="router.push({ name: routerName.order })"
         >
-          付费支持
+          订单
         </a>
         <a
           class="item"
@@ -50,67 +50,89 @@
         >
           广告
         </a>
+        <a
+          class="item"
+          :class="{
+            active: router.currentRoute.value.name === routerName.rank,
+          }"
+          href="/rank"
+          @click.prevent="router.push({ name: routerName.rank })"
+        >
+          排行榜
+        </a>
       </div>
     </div>
     <div class="right">
-      <div class="ecosystem">
-        <div class="txt">生态系统</div>
-        <VPIconChevronDown class="icon"></VPIconChevronDown>
-        <div class="list">
-          <div class="title">资源</div>
-          <a
-            v-for="(item, index) in resource"
-            :key="index"
-            :href="item.url"
-            class="item"
-            @click="handleJump(item)"
-          >
-            <div class="txt">{{ item.label }}</div>
-            <VPIconExternalLink
-              v-if="item.url"
-              class="icon"
-            ></VPIconExternalLink>
-          </a>
-          <div class="hr"></div>
-          <div class="title">官方库</div>
-          <a
-            v-for="(item, index) in plugins"
-            :key="index"
-            class="item"
-            :href="item.url"
-            @click="handleJump(item)"
-          >
-            <div class="txt">{{ item.label }}</div>
-            <VPIconExternalLink
-              v-if="item.url"
-              class="icon"
-            ></VPIconExternalLink>
-          </a>
-        </div>
-      </div>
-      <div class="about">
-        <div class="txt">关于</div>
-        <VPIconChevronDown class="icon"></VPIconChevronDown>
-        <div class="list">
-          <a
-            v-for="(item, index) in about"
-            :key="index"
-            class="item"
-            :href="item.url"
-            @click.prevent="
-              item.routerName
-                ? router.push({ name: item.routerName })
-                : openToTarget(item.url)
-            "
-          >
-            <div class="txt">{{ item.label }}</div>
-            <VPIconExternalLink
-              v-if="item.url"
-              class="icon"
-            ></VPIconExternalLink>
-          </a>
-        </div>
-      </div>
+      <Dropdown class="ecosystem">
+        <template #btn>
+          <div class="btn">
+            生态系统<VPIconChevronDown class="icon"></VPIconChevronDown>
+          </div>
+        </template>
+        <template #list>
+          <div class="list">
+            <div class="title">资源</div>
+            <a
+              v-for="(item, index) in resource"
+              :key="index"
+              :href="item.url"
+              class="item"
+              @click="handleJump(item)"
+            >
+              <div class="txt">{{ item.label }}</div>
+              <VPIconExternalLink
+                v-if="item.url"
+                class="icon"
+              ></VPIconExternalLink>
+            </a>
+            <div class="hr"></div>
+            <div class="title">官方库</div>
+            <a
+              v-for="(item, index) in plugins"
+              :key="index"
+              class="item"
+              :href="item.url"
+              @click="handleJump(item)"
+            >
+              <div class="txt">{{ item.label }}</div>
+              <VPIconExternalLink
+                v-if="item.url"
+                class="icon"
+              ></VPIconExternalLink>
+            </a>
+          </div>
+        </template>
+      </Dropdown>
+
+      <Dropdown class="about">
+        <template #btn>
+          <div class="btn">
+            关于<VPIconChevronDown class="icon"></VPIconChevronDown>
+          </div>
+        </template>
+        <template #list>
+          <div class="list">
+            <a
+              v-for="(item, index) in about"
+              :key="index"
+              class="item"
+              :href="item.url"
+              @click.prevent="
+                item.routerName
+                  ? router.push({ name: item.routerName })
+                  : openToTarget(item.url)
+              "
+            >
+              <div class="txt">{{ item.label }}</div>
+              <VPIconExternalLink
+                v-if="item.url"
+                class="icon"
+              ></VPIconExternalLink>
+            </a>
+          </div>
+        </template>
+      </Dropdown>
+
       <a
         class="sponsors"
         :class="{
@@ -121,6 +143,7 @@
       >
         赞助
       </a>
+
       <a
         class="github"
         target="_blank"
@@ -130,36 +153,59 @@
           :src="githubStar"
           alt=""
         />
-        <!-- Github -->
       </a>
-      <n-dropdown
-        v-if="router.currentRoute.value.name !== routerName.push"
-        trigger="hover"
-        :options="options"
-        placement="bottom-end"
-        @select="handlePushSelect"
-      >
-        <div class="start-live">我要开播</div>
-      </n-dropdown>
+
+      <Dropdown class="start-live">
+        <template #btn>
+          <div class="btn">我要开播</div>
+        </template>
+        <template #list>
+          <div class="list">
+            <a
+              class="item"
+              @click.prevent="handleStartLive(liveTypeEnum.webrtcPush)"
+            >
+              <div class="txt">webrtc开播</div>
+            </a>
+            <a
+              class="item"
+              @click.prevent="handleStartLive(liveTypeEnum.srsPush)"
+            >
+              <div class="txt">srs-webrtc开播</div>
+            </a>
+          </div>
+        </template>
+      </Dropdown>
+
       <div
         v-if="!userStore.userInfo"
         class="qqlogin"
         @click="useQQLogin()"
       >
-        登录
+        <div class="btn">登录</div>
       </div>
-      <n-dropdown
+      <Dropdown
         v-else
-        trigger="hover"
-        :options="userOptions"
-        @select="handleUserSelect"
+        class="qqlogin"
       >
-        <div
-          class="qqlogin"
-          :style="{ backgroundImage: `url(${userStore.userInfo.avatar})` }"
-          @click="useQQLogin()"
-        ></div>
-      </n-dropdown>
+        <template #btn>
+          <div
+            class="btn"
+            :style="{ backgroundImage: `url(${userStore.userInfo.avatar})` }"
+            @click="useQQLogin()"
+          ></div>
+        </template>
+        <template #list>
+          <div class="list">
+            <a
+              class="item"
+              @click.prevent="userStore.logout()"
+            >
+              <div class="txt">退出</div>
+            </a>
+          </div>
+        </template>
+      </Dropdown>
     </div>
   </div>
 </template>
@@ -169,6 +215,7 @@ import { openToTarget } from 'billd-utils';
 import { onMounted, ref } from 'vue';
 import { useRouter } from 'vue-router';
 
+import Dropdown from '@/components/Dropdown/index.vue';
 import VPIconChevronDown from '@/components/icons/VPIconChevronDown.vue';
 import VPIconExternalLink from '@/components/icons/VPIconExternalLink.vue';
 import { loginTip, useQQLogin } from '@/hooks/use-login';
@@ -180,13 +227,6 @@ const router = useRouter();
 const userStore = useUserStore();
 const githubStar = ref('');
 
-const userOptions = ref([
-  {
-    label: '退出',
-    key: '1',
-  },
-]);
-
 const about = ref([
   {
     label: '常见问题',
@@ -249,29 +289,12 @@ function handleJump(item) {
   }
 }
 
-const options = ref([
-  {
-    label: 'webrtc开播',
-    key: liveTypeEnum.webrtcPush,
-  },
-  {
-    label: 'srs-webrtc开播',
-    key: liveTypeEnum.srsPush,
-  },
-]);
-
 onMounted(() => {
   githubStar.value =
     'https://img.shields.io/github/stars/galaxy-s10/billd-live?label=Star&logo=GitHub&labelColor=white&logoColor=black&style=social&cacheSeconds=3600';
 });
 
-function handleUserSelect(key) {
-  if (key === '1') {
-    userStore.logout();
-  }
-}
-
-function handlePushSelect(key) {
+function handleStartLive(key) {
   if (!loginTip()) {
     return;
   }
@@ -288,7 +311,7 @@ function handlePushSelect(key) {
   display: flex;
   align-items: center;
   justify-content: space-between;
-  padding: 0 20px;
+  padding: 0 30px;
   min-width: $medium-width;
   height: 64px;
   background-color: #fff;
@@ -351,7 +374,6 @@ function handlePushSelect(key) {
           &::after {
             position: absolute;
             top: calc(50% - 8px);
-            transform: translateY(-100%);
             right: -5px;
             width: 5px;
             height: 5px;
@@ -359,6 +381,7 @@ function handlePushSelect(key) {
             background-color: $theme-color-gold;
             content: '';
             transition: all 0.1s ease;
+            transform: translateY(-100%);
           }
         }
         &:hover {
@@ -367,132 +390,137 @@ function handlePushSelect(key) {
       }
     }
   }
+
   .right {
     display: flex;
     align-items: center;
     height: 100%;
 
-    .qqlogin {
-      box-sizing: border-box;
-      margin-right: 15px;
-      width: 35px;
-      height: 35px;
-      border-radius: 50%;
-      background-color: papayawhip;
-      text-align: center;
-      font-size: 13px;
-      line-height: 35px;
-      cursor: pointer;
-
-      @extend %containBg;
-    }
-
     .about,
-    .ecosystem,
-    .sponsors,
-    .github {
-      position: relative;
-      display: flex;
-      align-items: center;
+    .ecosystem {
       margin-right: 20px;
-      height: 100%;
-      border-radius: 6px;
-      color: black;
-      text-decoration: none;
-      font-size: 13px;
-      cursor: pointer;
-      .icon {
-        fill: currentColor;
-      }
-      a {
-        color: black;
-        text-decoration: none;
-        font-size: 13px;
-      }
       &:hover {
         color: $theme-color-gold;
+      }
+      .btn {
+        display: flex;
+        align-items: center;
         .icon {
-          color: $theme-color-gold;
+          margin-left: 5px;
+          width: 13px;
+
+          fill: currentColor;
         }
-      }
-    }
-    .about,
-    .ecosystem {
-      &:hover {
-        .list {
-          display: block;
-          .item {
-            &:hover {
-              color: $theme-color-gold;
-              a {
-                color: $theme-color-gold;
-              }
-            }
-          }
+        &:hover {
+          color: $theme-color-gold;
         }
       }
-      .icon {
-        margin-left: 5px;
-        width: 13px;
-      }
+
       .list {
-        position: absolute;
-        top: 80%;
-        right: 0;
-        z-index: 2;
-        display: none;
-        box-sizing: border-box;
-        padding: 10px 0;
-        border-radius: 5px;
-        background-color: #fff;
-        box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1),
-          0 2px 6px rgba(0, 0, 0, 0.08);
+        width: 120px;
         .item {
           display: flex;
           align-items: center;
           margin-bottom: 5px;
-          padding: 0 20px;
+          padding: 0 15px;
           color: black;
+          text-decoration: none;
+          cursor: pointer;
+          &:hover {
+            color: $theme-color-gold;
+          }
           .icon {
+            margin-left: 5px;
             width: 13px;
             color: #3c3c4354;
+
+            fill: currentColor;
           }
         }
-        .title {
-          margin: 10px 0 5px;
-          padding: 0 20px;
-          color: rgba(60, 60, 60, 0.33);
-        }
-        .title:first-child {
-          margin-top: 0;
-        }
       }
     }
     .ecosystem {
       .list {
         width: 220px;
+        .title {
+          margin: 10px 0 5px;
+          padding: 0 15px;
+          color: rgba(60, 60, 60, 0.33);
+
+          &:first-child {
+            margin-top: 0;
+          }
+        }
       }
     }
-    .about {
-      .list {
-        width: 120px;
-      }
-    }
-    .github {
+
+    .github,
+    .sponsors {
       display: flex;
       align-items: center;
+      margin-right: 20px;
+      color: black;
+      text-decoration: none;
+      &:hover {
+        color: $theme-color-gold;
+      }
       .txt {
         margin-right: 5px;
       }
     }
+
     .start-live {
       margin-right: 20px;
-      padding: 5px 15px;
-      border-radius: 6px;
-      background-color: $theme-color-gold;
-      color: white;
-      font-size: 13px;
-      cursor: pointer;
+
+      .btn {
+        padding: 5px 15px;
+        border-radius: 6px;
+        background-color: $theme-color-gold;
+        color: white;
+        font-size: 13px;
+        cursor: pointer;
+      }
+      .list {
+        width: 150px;
+        .item {
+          display: flex;
+          align-items: center;
+          margin-bottom: 5px;
+          padding: 0 15px;
+          cursor: pointer;
+          &:hover {
+            color: $theme-color-gold;
+          }
+        }
+      }
+    }
+    .qqlogin {
+      .btn {
+        box-sizing: border-box;
+        width: 35px;
+        height: 35px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        border-radius: 50%;
+        background-color: papayawhip;
+        font-size: 13px;
+        cursor: pointer;
+
+        @extend %containBg;
+      }
+      .list {
+        width: 70px;
+        .item {
+          display: flex;
+          justify-content: center;
+          padding: 0 15px;
+          cursor: pointer;
+          &:hover {
+            color: $theme-color-gold;
+          }
+        }
+      }
     }
   }
 }

+ 2 - 1
src/main.ts

@@ -6,6 +6,7 @@ import { createApp } from 'vue';
 import adapter from 'webrtc-adapter';
 
 import Message from '@/components/Message/index.vue';
+import registerDirectives from '@/directives';
 import router from '@/router/index';
 import store from '@/store/index';
 
@@ -14,7 +15,7 @@ import App from './App.vue';
 console.log('webrtc-adapter', adapter.browserDetails);
 
 const app = createApp(App);
-
+registerDirectives(app);
 app.use(store);
 app.use(router);
 

+ 12 - 0
src/router/index.ts

@@ -10,6 +10,8 @@ export const routerName = {
   rank: 'rank',
   sponsors: 'sponsors',
   support: 'support',
+  order: 'order',
+  shop: 'shop',
   link: 'link',
   ad: 'ad',
   faq: 'faq',
@@ -65,6 +67,11 @@ export const defaultRoutes: RouteRecordRaw[] = [
         path: '/rank',
         component: () => import('@/views/rank/index.vue'),
       },
+      {
+        name: routerName.shop,
+        path: '/shop',
+        component: () => import('@/views/shop/index.vue'),
+      },
       {
         name: routerName.sponsors,
         path: '/sponsors',
@@ -75,6 +82,11 @@ export const defaultRoutes: RouteRecordRaw[] = [
         path: '/support',
         component: () => import('@/views/support/index.vue'),
       },
+      {
+        name: routerName.order,
+        path: '/order',
+        component: () => import('@/views/order/index.vue'),
+      },
       {
         name: routerName.ad,
         path: '/ad',

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

@@ -1,5 +1,6 @@
 <template>
   <div class="ad-wrap">
+    <h1 class="title">广告位招租</h1>
     <div class="ad-list">
       <div
         v-for="(item, index) in list"
@@ -84,6 +85,9 @@ const list = ref([
 
 <style lang="scss" scoped>
 .ad-wrap {
+  .title {
+    text-align: center;
+  }
   .ad-list {
     padding: 20px;
 

+ 73 - 47
src/views/sponsors copy/index.vue → src/views/order copy/index.vue

@@ -4,12 +4,22 @@
       截止至{{ onMountedTime }},已收到:{{ receiveMoney / 100 }}元赞助~
     </h1>
     <div class="pay-list">
+      <div class="head-wrap">
+        <div
+          v-for="(item, index) in headList"
+          :key="index"
+          class="head"
+        >
+          <div>{{ item.label }}</div>
+        </div>
+      </div>
+
       <div
         v-for="(item, index) in payList"
         :key="index"
         class="item"
       >
-        <div class="time">发起时间:{{ item.created_at }},</div>
+        <div class="time">{{ item.created_at }}</div>
         <div class="user">
           <template v-if="item.user">
             <img
@@ -19,25 +29,24 @@
             />
             <span class="username">{{ item.user.username }}</span>
           </template>
-          <span v-else>游客</span>
+          <span v-else>游客</span>
         </div>
 
-        <div class="account">支付宝账号:{{ item.buyer_logon_id }},</div>
-        <div class="gift">
-          赞助了:{{ item.subject }}({{ item.total_amount }}元),
-        </div>
+        <div class="account">{{ item.buyer_logon_id }}</div>
+        <div class="gift">{{ item.subject }}</div>
+        <div class="gift">{{ item.total_amount }}元</div>
         <div class="status">
-          状态:{{
+          {{
             item.trade_status === PayStatusEnum.WAIT_BUYER_PAY
               ? '支付中'
               : '已支付'
-          }}
+          }}
         </div>
-        <div class="time">支付时间:{{ item.send_pay_date || '-' }}</div>
+        <div class="time">{{ item.send_pay_date || '-' }}</div>
       </div>
     </div>
     <h2>开始赞助(支付宝)</h2>
-    <div class="gift-list">
+    <div class="goods-list">
       <div
         v-for="(item, index) in sponsorsGoodsList"
         :key="index"
@@ -78,6 +87,37 @@ const goodsInfo = reactive({
 const payList = ref<IOrder[]>([]);
 const sponsorsGoodsList = ref<IGoods[]>([]);
 
+const headList = ref([
+  {
+    label: '创建时间',
+    key: 'created_at',
+  },
+  {
+    label: '用户',
+    key: 'avatar',
+  },
+  {
+    label: '支付宝账号',
+    key: 'buyer_logon_id',
+  },
+  {
+    label: '商品',
+    key: 'subject',
+  },
+  {
+    label: '价格',
+    key: 'total_amount',
+  },
+  {
+    label: '状态',
+    key: 'trade_status',
+  },
+  {
+    label: '完成时间',
+    key: 'send_pay_date',
+  },
+]);
+
 onUnmounted(() => {
   clearInterval(payStatusTimer.value);
   clearInterval(downTimer.value);
@@ -131,55 +171,41 @@ function startPay(item: IGoods) {
 .sponsors-wrap {
   text-align: center;
   .pay-list {
-    display: flex;
-    overflow: scroll;
-    align-items: center;
-    flex-direction: column;
-    box-sizing: border-box;
-    margin-bottom: 20px;
-    padding: 10px;
-    width: 100%;
-    height: 200px;
-    background-color: papayawhip;
+    .head-wrap {
+      display: flex;
+      align-items: center;
+      .head {
+        flex: 1;
+        box-sizing: border-box;
+        margin-bottom: 5px;
+      }
+    }
     .item {
-      display: inline-flex;
-      flex-wrap: wrap;
-      justify-content: center;
-      margin-bottom: 4px;
-      width: 100%;
-      text-align: left;
-
-      .user {
-        width: 120px;
-        padding: 0 10px;
+      display: flex;
+      align-items: center;
+      height: 40px;
+      color: #666;
+      > div {
         display: flex;
         align-items: center;
+        flex: 1;
         justify-content: center;
+      }
+      &:nth-child(2n) {
+        background-color: #fafbfc;
+      }
+      .time {
+      }
+      .user {
         .avatar {
           width: 30px;
           height: 30px;
           border-radius: 50%;
         }
-        .username {
-          @extend %singleEllipsis;
-        }
-      }
-      .account {
-        width: 250px;
-      }
-      .gift {
-        width: 260px;
-      }
-      .status {
-        width: 120px;
-        text-align: left;
-      }
-      .time {
-        width: 280px;
       }
     }
   }
-  .gift-list {
+  .goods-list {
     display: flex;
     align-items: center;
     flex-wrap: wrap;

+ 152 - 0
src/views/order/index.vue

@@ -0,0 +1,152 @@
+<template>
+  <div class="order-wrap">
+    <div class="list">
+      <div class="head-wrap">
+        <div
+          v-for="(item, index) in headList"
+          :key="index"
+          class="head"
+        >
+          <div>{{ item.label }}</div>
+        </div>
+      </div>
+
+      <div
+        v-for="(item, index) in payList"
+        :key="index"
+        class="item"
+      >
+        <div class="time">{{ item.created_at }}</div>
+        <div class="user">
+          <template v-if="item.user">
+            <img
+              :src="item.user.avatar"
+              class="avatar"
+              alt=""
+            />
+            <span class="username">{{ item.user.username }}</span>
+          </template>
+          <span v-else>游客</span>
+        </div>
+
+        <div class="account">{{ item.buyer_logon_id }}</div>
+        <div class="gift">{{ item.subject }}</div>
+        <div class="gift">{{ item.total_amount }}元</div>
+        <div class="status">
+          {{
+            item.trade_status === PayStatusEnum.WAIT_BUYER_PAY
+              ? '支付中'
+              : '已支付'
+          }}
+        </div>
+        <div class="time">{{ item.send_pay_date || '-' }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+
+import { fetchOrderList } from '@/api/order';
+import { fullLoading } from '@/components/FullLoading';
+import { IOrder, PayStatusEnum } from '@/interface';
+
+const payList = ref<IOrder[]>([]);
+
+const headList = ref([
+  {
+    label: '创建时间',
+    key: 'created_at',
+  },
+  {
+    label: '用户',
+    key: 'avatar',
+  },
+  {
+    label: '支付宝账号',
+    key: 'buyer_logon_id',
+  },
+  {
+    label: '商品',
+    key: 'subject',
+  },
+  {
+    label: '价格',
+    key: 'total_amount',
+  },
+  {
+    label: '状态',
+    key: 'trade_status',
+  },
+  {
+    label: '完成时间',
+    key: 'send_pay_date',
+  },
+]);
+
+onUnmounted(() => {});
+
+onMounted(() => {
+  getPayList();
+});
+
+async function getPayList() {
+  try {
+    fullLoading({ loading: true });
+    const res = await fetchOrderList({
+      trade_status: PayStatusEnum.TRADE_SUCCESS,
+    });
+    if (res.code === 200) {
+      payList.value = res.data.rows;
+    }
+  } catch (error) {
+    console.log(error);
+  } finally {
+    fullLoading({ loading: false });
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.order-wrap {
+  padding: 50px 30px 0;
+  .list {
+    text-align: center;
+
+    .head-wrap {
+      display: flex;
+      align-items: center;
+      .head {
+        flex: 1;
+        box-sizing: border-box;
+        margin-bottom: 5px;
+      }
+    }
+    .item {
+      display: flex;
+      align-items: center;
+      height: 40px;
+      color: #666;
+      > div {
+        display: flex;
+        align-items: center;
+        flex: 1;
+        justify-content: center;
+      }
+      &:nth-child(2n) {
+        background-color: #fafbfc;
+      }
+      .time {
+      }
+      .user {
+        .avatar {
+          width: 30px;
+          height: 30px;
+          border-radius: 50%;
+        }
+      }
+    }
+  }
+}
+</style>

+ 16 - 7
src/views/pull/index.vue

@@ -74,6 +74,7 @@
 
         <div
           ref="bottomRef"
+          v-loading="giftLoading"
           class="gift-list"
         >
           <div
@@ -207,6 +208,7 @@ const userStore = useUserStore();
 const appStore = useAppStore();
 
 const giftGoodsList = ref<IGoods[]>([]);
+const giftLoading = ref(false);
 const showRecharge = ref(false);
 const showJoin = ref(true);
 const showSidebar = ref(true);
@@ -246,13 +248,20 @@ const {
 });
 
 async function getGoodsList() {
-  const res = await fetchGoodsList({
-    type: GoodsTypeEnum.gift,
-    orderName: 'created_at',
-    orderBy: 'desc',
-  });
-  if (res.code === 200) {
-    giftGoodsList.value = res.data.rows;
+  try {
+    giftLoading.value = true;
+    const res = await fetchGoodsList({
+      type: GoodsTypeEnum.gift,
+      orderName: 'created_at',
+      orderBy: 'desc',
+    });
+    if (res.code === 200) {
+      giftGoodsList.value = res.data.rows;
+    }
+  } catch (error) {
+    console.log(error);
+  } finally {
+    giftLoading.value = false;
   }
 }
 

+ 49 - 31
src/views/rank/index.vue

@@ -65,6 +65,7 @@ import { onMounted, ref } from 'vue';
 import { fetchLiveRoomList } from '@/api/liveRoom';
 import { fetchUserList } from '@/api/user';
 import { fetchWalletList } from '@/api/wallet';
+import { fullLoading } from '@/components/FullLoading';
 import { RankTypeEnum } from '@/interface';
 
 export interface IRankType {
@@ -115,43 +116,57 @@ const mockRank = [
 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));
+  try {
+    fullLoading({ loading: true });
+    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));
+      }
     }
+  } catch (error) {
+    console.log(error);
+  } finally {
+    fullLoading({ loading: false });
   }
 }
 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,
-      };
+  try {
+    fullLoading({ loading: true });
+    const res = await fetchLiveRoomList({
+      orderName: 'updated_at',
+      orderBy: 'desc',
     });
-    if (length < 3) {
-      rankList.value.push(...mockRank.slice(length));
+    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));
+      }
     }
+  } catch (error) {
+    console.log(error);
+  } finally {
+    fullLoading({ loading: false });
   }
 }
 
@@ -178,6 +193,7 @@ onMounted(() => {
 
 async function getUserList() {
   try {
+    fullLoading({ loading: true });
     const res = await fetchUserList({
       orderName: 'updated_at',
       orderBy: 'desc',
@@ -199,6 +215,8 @@ async function getUserList() {
     }
   } catch (error) {
     console.log(error);
+  } finally {
+    fullLoading({ loading: false });
   }
 }
 </script>

+ 235 - 0
src/views/shop/index.vue

@@ -0,0 +1,235 @@
+<template>
+  <div class="shop-wrap">
+    <div class="tab-list">
+      <div
+        v-for="(item, index) in tabList"
+        :key="index"
+        class="tab"
+        :class="{ active: item.key === currTab }"
+        @click="changeTab(item.key)"
+      >
+        {{ item.label }}
+      </div>
+    </div>
+    <div
+      v-loading="loading"
+      class="goods-list"
+    >
+      <div
+        v-for="(item, index) in list"
+        :key="index"
+        class="goods"
+        @click="startPay(item)"
+      >
+        <div
+          class="left"
+          :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>
+          <div class="desc">{{ item.desc }}</div>
+          <div class="price-wrap">
+            <span class="price">¥{{ item.price }}</span>
+            <del
+              v-if="item.price !== item.original_price"
+              class="original-price"
+            >
+              {{ item.original_price }}
+            </del>
+          </div>
+        </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 { nextTick, onMounted, reactive, ref } from 'vue';
+import { useRoute } from 'vue-router';
+
+import { fetchGoodsList } from '@/api/goods';
+import QrPayCpt from '@/components/QrPay/index.vue';
+import { GoodsTypeEnum, IGoods } from '@/interface';
+import router from '@/router';
+
+const route = useRoute();
+const list = ref<IGoods[]>([]);
+const loading = ref(false);
+const showQrPay = ref(false);
+const goodsInfo = reactive({
+  money: '0.00',
+  goodsId: -1,
+  liveRoomId: -1,
+});
+
+const tabList = ref([
+  { label: '礼物', key: GoodsTypeEnum.gift },
+  { label: '赞助', key: GoodsTypeEnum.sponsors },
+  { label: '服务', key: GoodsTypeEnum.support },
+]);
+
+const currTab = ref(tabList.value[0].key);
+
+async function getGoodsList(type: GoodsTypeEnum) {
+  try {
+    loading.value = true;
+    const res = await fetchGoodsList({
+      type,
+      orderName: 'created_at',
+      orderBy: 'desc',
+    });
+    if (res.code === 200) {
+      console.log(res.data);
+      list.value = res.data.rows;
+    }
+  } catch (error) {
+    console.log(error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+onMounted(() => {
+  const key = route.query.goodsType as GoodsTypeEnum;
+  if (GoodsTypeEnum[key]) {
+    currTab.value = key;
+  } else {
+    router.push({ query: {} });
+  }
+  getGoodsList(currTab.value);
+});
+
+function changeTab(type: GoodsTypeEnum) {
+  showQrPay.value = false;
+  currTab.value = type;
+  getGoodsList(currTab.value);
+}
+
+function startPay(item: IGoods) {
+  if (Number(Number(item.price).toFixed(0)) <= 0) {
+    window.$message.info('该商品是免费的,不需要购买!');
+    return;
+  }
+  showQrPay.value = false;
+  nextTick(() => {
+    goodsInfo.money = item.price!;
+    goodsInfo.goodsId = item.id!;
+    showQrPay.value = true;
+  });
+}
+</script>
+
+<style lang="scss" scoped>
+.shop-wrap {
+  padding: 20px 30px;
+  .tab-list {
+    display: flex;
+    align-items: center;
+    margin-bottom: 20px;
+    height: 40px;
+    font-size: 14px;
+
+    user-select: none;
+    .tab {
+      position: relative;
+      margin-right: 20px;
+      cursor: pointer;
+      &.active {
+        color: $theme-color-gold;
+        font-size: 16px;
+
+        &::after {
+          position: absolute;
+          bottom: -6px;
+          left: 50%;
+          width: 20px;
+          height: 2px;
+          border-radius: 10px;
+          background-color: $theme-color-gold;
+          content: '';
+          transform: translateX(-50%);
+        }
+      }
+    }
+  }
+  .goods-list {
+    display: flex;
+    flex-wrap: wrap;
+    .goods {
+      display: flex;
+      box-sizing: border-box;
+      margin-right: 20px;
+      margin-bottom: 20px;
+      padding: 18px 10px 10px;
+      width: 400px;
+      border-radius: 6px;
+      box-shadow: 0 4px 30px 0 rgba(238, 242, 245, 0.8);
+      cursor: pointer;
+      transition: box-shadow 0.2s linear;
+      &:hover {
+        box-shadow: 4px 4px 20px 0 rgba(205, 216, 228, 0.6);
+      }
+      .left {
+        position: relative;
+        margin-right: 20px;
+        width: 100px;
+        height: 100px;
+        @extend %containBg;
+        .badge {
+          position: absolute;
+          top: -10px;
+          right: -10px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          padding: 2px;
+          border-radius: 2px;
+          color: white;
+          .txt {
+            display: inline-block;
+            line-height: 1;
+            transform-origin: center !important;
+
+            @include minFont(10);
+          }
+        }
+      }
+      .right {
+        .title {
+          font-size: 22px;
+        }
+        .info {
+        }
+        .price-wrap {
+          display: flex;
+          align-items: center;
+          .price {
+            color: gold;
+            font-size: 22px;
+          }
+          .original-price {
+            margin-left: 5px;
+            color: #666;
+            font-size: 14px;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 9 - 144
src/views/support/index.vue

@@ -1,154 +1,19 @@
 <template>
-  <div class="support-wrap">
-    <div class="list">
-      <div
-        v-for="(item, index) in list"
-        :key="index"
-        class="item"
-        @click="startPay(item)"
-      >
-        <div
-          class="left"
-          :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>
-          <div class="desc">{{ item.desc }}</div>
-          <div class="price-wrap">
-            <span class="price">¥{{ item.price }}</span>
-            <del
-              v-if="item.price !== item.original_price"
-              class="original-price"
-            >
-              {{ item.original_price }}
-            </del>
-          </div>
-        </div>
-      </div>
-    </div>
-    <QrPayCpt
-      v-if="showQrPay"
-      :money="goodsInfo.money"
-      :goods-id="goodsInfo.goodsId"
-      :live-room-id="goodsInfo.liveRoomId"
-    ></QrPayCpt>
-  </div>
+  <div></div>
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onMounted, reactive, ref } from 'vue';
+import { onMounted } 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({
-    type: GoodsTypeEnum.support,
-    orderName: 'created_at',
-    orderBy: 'desc',
-  });
-  if (res.code === 200) {
-    console.log(res.data);
-    list.value = res.data.rows;
-  }
-}
+import { GoodsTypeEnum } from '@/interface';
+import router, { routerName } from '@/router';
 
 onMounted(() => {
-  getGoodsList();
-});
-
-function startPay(item: IGoods) {
-  showQrPay.value = false;
-  nextTick(() => {
-    goodsInfo.money = item.price!;
-    goodsInfo.goodsId = item.id!;
-    showQrPay.value = true;
+  router.push({
+    name: routerName.shop,
+    query: { goodsType: GoodsTypeEnum.sponsors },
   });
-}
+});
 </script>
 
-<style lang="scss" scoped>
-.support-wrap {
-  margin-top: 30px;
-  padding: 0 20px;
-  .list {
-    display: flex;
-    .item {
-      display: flex;
-      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;
-      &:hover {
-        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 {
-          font-size: 22px;
-        }
-        .info {
-        }
-        .price-wrap {
-          display: flex;
-          align-items: center;
-          .price {
-            color: gold;
-            font-size: 22px;
-          }
-          .original-price {
-            margin-left: 5px;
-            color: #666;
-            font-size: 14px;
-          }
-        }
-      }
-    }
-  }
-}
-</style>
+<style lang="scss" scoped></style>

+ 6 - 8
src/views/team/index.vue

@@ -44,7 +44,7 @@
                   </a>
                 </div>
               </div>
-              <div class="desc country">
+              <div class="desc">
                 <n-icon size="18">
                   <LocationOutline></LocationOutline>
                 </n-icon>
@@ -85,7 +85,10 @@
                 :href="iten.github"
                 @click.prevent="openToTarget(iten.github)"
               >
-                <n-icon size="22">
+                <n-icon
+                  size="22"
+                  class="ico"
+                >
                   <LogoGithub></LogoGithub>
                 </n-icon>
               </a>
@@ -160,7 +163,6 @@ const list = ref([
   }
   .desc {
     margin: 0;
-    width: 500px;
     color: #3c3c3cb3;
     font-size: 16px;
     line-height: 1.8;
@@ -189,24 +191,20 @@ const list = ref([
       }
     }
     .members {
-      flex-grow: 1;
-
+      flex-grow: 0.8;
       .item {
         position: relative;
         display: flex;
-        margin-bottom: 30px;
         padding: 30px 0;
         border-radius: 10px;
         background-color: #f9f9f9;
         .avatar {
-          flex-shrink: 0;
           margin-left: 30px;
           width: 80px;
           height: 80px;
           border-radius: 50%;
         }
         .data {
-          flex-shrink: 0;
           margin-left: 30px;
           .name {
             margin-bottom: 2px;