Procházet zdrojové kódy

feat: 全面优化

shuisheng před 2 roky
rodič
revize
37e908066a

binární
public/favicon.ico


+ 1 - 0
src/assets/constant.scss

@@ -15,3 +15,4 @@ $small-width: 800px;
 $large-left-width: 1100px;
 $medium-left-width: 900px;
 $small-left-width: 600px;
+$theme-color-gold: gold;

binární
src/assets/img/logo.webp


+ 12 - 0
src/components/icons/VPIconChevronDown.vue

@@ -0,0 +1,12 @@
+<template>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    aria-hidden="true"
+    focusable="false"
+    viewBox="0 0 24 24"
+  >
+    <path
+      d="M12,16c-0.3,0-0.5-0.1-0.7-0.3l-6-6c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l5.3,5.3l5.3-5.3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-6,6C12.5,15.9,12.3,16,12,16z"
+    />
+  </svg>
+</template>

+ 16 - 0
src/components/icons/VPIconExternalLink.vue

@@ -0,0 +1,16 @@
+<template>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    aria-hidden="true"
+    focusable="false"
+    height="24px"
+    viewBox="0 0 24 24"
+    width="24px"
+  >
+    <path
+      d="M0 0h24v24H0V0z"
+      fill="none"
+    />
+    <path d="M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z" />
+  </svg>
+</template>

+ 2 - 1
src/constant.ts

@@ -2,7 +2,8 @@ export const QQ_CLIENT_ID = '101958191';
 export const QQ_OAUTH_URL = 'https://graph.qq.com/oauth2.0';
 export const QQ_REDIRECT_URI = 'https://live.hsslive.cn/oauth/qq_login';
 
-export const LIVE_CLIENT_URL = 'https://www.hsslive.cn';
+export const AUTHOR_GITHUB = 'https://github.com/galaxy-s10';
+export const LIVE_CLIENT_URL = 'https://live.hsslive.cn';
 
 // 全局的cookie的key
 export const COOKIE_KEY = {

+ 1 - 1
src/hooks/use-login.ts

@@ -48,7 +48,7 @@ export function loginTip(show = false) {
   const token = cache.getStorageExp('token');
   instance.show = show;
   if (!token) {
-    window.$message.warning('请先登录~');
+    window.$message.warning('请先登录');
     instance.show = true;
     return false;
   }

+ 1 - 1
src/hooks/use-push.ts

@@ -612,7 +612,7 @@ export function usePush({
     }
     const instance = networkStore.wsMap.get(roomId.value);
     if (!instance) {
-      window.$message.error('还没开播,不能发送弹幕');
+      window.$message.error('还没开播,不能发送弹幕');
       return;
     }
     instance.send({

+ 288 - 209
src/layout/head/index.vue

@@ -2,15 +2,27 @@
   <div class="head-wrap">
     <div class="left">
       <div
-        class="logo"
+        class="logo-wrap"
         @click="router.push('/')"
       >
-        Billd直播
+        <!-- <div class="logo"></div> -->
+        <div class="txt">Billd直播</div>
       </div>
+
       <div class="nav">
         <a
+          class="item"
+          :class="{
+            active: router.currentRoute.value.path === '/',
+          }"
+          href="/"
+          @click.prevent="router.push('/')"
+        >
+          首页
+        </a>
+        <a
+          class="item"
           :class="{
-            item: 1,
             active: router.currentRoute.value.name === routerName.rank,
           }"
           href="/rank"
@@ -19,8 +31,8 @@
           排行榜
         </a>
         <a
+          class="item"
           :class="{
-            item: 1,
             active: router.currentRoute.value.name === routerName.support,
           }"
           href="/support"
@@ -29,18 +41,8 @@
           付费支持
         </a>
         <a
+          class="item"
           :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.ad,
           }"
           href="/ad"
@@ -48,118 +50,127 @@
         >
           广告
         </a>
-        <a
-          :class="{
-            item: 1,
-            active: router.currentRoute.value.name === routerName.about,
-          }"
-          href="/about"
-          @click.prevent="router.push({ name: routerName.about })"
-        >
-          关于
-          <div class="list">
-            <div class="item">
-              <div class="txt">常见问题</div>
-            </div>
-            <div class="item">
-              <div class="txt">团队</div>
-            </div>
-            <div class="item">
-              <div class="txt">b站视频</div>
-              <n-icon
-                size="20"
-                color="#bfbfbf"
-              >
-                <TrendingUp></TrendingUp>
-              </n-icon>
-            </div>
-          </div>
-        </a>
-        <div
-          v-for="(item, index) in navLeftList.filter(
-            (item) => router.currentRoute.value.query.liveType === item.liveType
-          )"
-          :key="index"
-          :class="{
-            item: 1,
-            active: router.currentRoute.value.query.liveType === item.liveType,
-          }"
-          @click="goPushPage(item.routerName)"
-        >
-          {{ item.title }}
-        </div>
-        <div
-          v-for="(item, index) in pullList.filter(
-            (item) => router.currentRoute.value.query.liveType === item.liveType
-          )"
-          :key="index"
-          :class="{
-            item: 1,
-            active: router.currentRoute.value.query.liveType === item.liveType,
-          }"
-        >
-          {{ item.title }}
-        </div>
       </div>
     </div>
     <div class="right">
-      <div
-        v-if="!userStore.userInfo"
-        class="qqlogin"
-        @click="useQQLogin()"
-      >
-        登录
+      <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>
-      <n-dropdown
-        v-else
-        trigger="hover"
-        :options="userOptions"
-        @select="handleUserSelect"
-      >
-        <div
-          class="qqlogin"
-          :style="{ backgroundImage: `url(${userStore.userInfo.avatar})` }"
-          @click="useQQLogin()"
-        ></div>
-      </n-dropdown>
-
       <a
-        class="bilibili"
-        target="_blank"
-        href="https://space.bilibili.com/381307133/channel/seriesdetail?sid=3285689"
+        class="sponsors"
+        :class="{
+          active: router.currentRoute.value.name === routerName.sponsors,
+        }"
+        href="/sponsors"
+        @click.prevent="router.push({ name: routerName.sponsors })"
       >
-        b站视频
+        赞助
       </a>
       <a
         class="github"
         target="_blank"
         href="https://github.com/galaxy-s10/billd-live"
       >
-        <span class="txt">github</span>
         <img
           :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>
+      <div
+        v-if="!userStore.userInfo"
+        class="qqlogin"
+        @click="useQQLogin()"
+      >
+        登录
+      </div>
+      <n-dropdown
+        v-else
+        trigger="hover"
+        :options="userOptions"
+        @select="handleUserSelect"
+      >
+        <div
+          class="qqlogin"
+          :style="{ backgroundImage: `url(${userStore.userInfo.avatar})` }"
+          @click="useQQLogin()"
+        ></div>
+      </n-dropdown>
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { TrendingUp } from '@vicons/ionicons5';
 import { openToTarget } from 'billd-utils';
 import { onMounted, ref } from 'vue';
 import { useRouter } from 'vue-router';
 
+import VPIconChevronDown from '@/components/icons/VPIconChevronDown.vue';
+import VPIconExternalLink from '@/components/icons/VPIconExternalLink.vue';
 import { loginTip, useQQLogin } from '@/hooks/use-login';
 import { liveTypeEnum } from '@/interface';
 import { routerName } from '@/router';
@@ -169,65 +180,75 @@ const router = useRouter();
 const userStore = useUserStore();
 const githubStar = ref('');
 
-const navLeftList = ref([
-  {
-    title: 'Webrtc Push',
-    routerName: routerName.push,
-    liveType: liveTypeEnum.webrtcPush,
-  },
+const userOptions = ref([
   {
-    title: 'SRS WebRTC Push',
-    routerName: routerName.push,
-    liveType: liveTypeEnum.srsPush,
+    label: '退出',
+    key: '1',
   },
 ]);
 
-const pullList = ref([
+const about = ref([
   {
-    title: 'Webrtc Pull',
-    routerName: routerName.pull,
-    liveType: liveTypeEnum.webrtcPull,
+    label: '常见问题',
+    routerName: routerName.faq,
   },
   {
-    title: 'SRS WebRTC Pull Flv',
-    routerName: routerName.pull,
-    liveType: liveTypeEnum.srsFlvPull,
+    label: '团队',
+    routerName: routerName.team,
   },
   {
-    title: 'SRS WebRTC Pull',
-    routerName: routerName.pull,
-    liveType: liveTypeEnum.srsWebrtcPull,
+    label: '官方群',
+    routerName: routerName.group,
+  },
+  {
+    label: '版本发布',
+    routerName: routerName.release,
+  },
+  {
+    label: 'b站视频',
+    url: 'https://space.bilibili.com/381307133/channel/seriesdetail?sid=3285689',
   },
 ]);
-
-const userOptions = ref([
+const resource = ref([
   {
-    label: '退出',
-    key: '1',
+    label: 'billd-live-server',
+    url: 'https://github.com/galaxy-s10/billd-live-server',
+  },
+  {
+    label: 'billd-live-admin',
   },
 ]);
-
-enum OptionEnum {
-  problem,
-  team,
-  bilibili,
-}
-
-const aboutOptions = ref([
+const plugins = ref([
   {
-    label: '常见问题',
-    key: OptionEnum.problem,
+    label: 'billd-ui',
+    url: 'https://github.com/galaxy-s10/billd-ui',
   },
   {
-    label: '团队',
-    key: OptionEnum.team,
+    label: 'billd-cli',
+    url: 'https://github.com/galaxy-s10/billd-cli',
   },
   {
-    label: 'b站视频',
-    key: OptionEnum.bilibili,
+    label: 'billd-utils',
+    url: 'https://github.com/galaxy-s10/billd-utils',
+  },
+  {
+    label: 'billd-scss',
+    url: 'https://github.com/galaxy-s10/billd-scss',
+  },
+  {
+    label: 'billd-html-webpack-plugin',
+    url: 'https://github.com/galaxy-s10/billd-html-webpack-plugin',
   },
 ]);
 
+function handleJump(item) {
+  if (item.url) {
+    openToTarget(item.url);
+  } else {
+    window.$message.info('敬请期待!');
+  }
+}
+
 const options = ref([
   {
     label: 'webrtc开播',
@@ -244,12 +265,12 @@ onMounted(() => {
     'https://img.shields.io/github/stars/galaxy-s10/billd-live?label=Star&logo=GitHub&labelColor=white&logoColor=black&style=social&cacheSeconds=3600';
 });
 
-function handleSelect(key) {}
 function handleUserSelect(key) {
   if (key === '1') {
     userStore.logout();
   }
 }
+
 function handlePushSelect(key) {
   if (!loginTip()) {
     return;
@@ -260,11 +281,6 @@ function handlePushSelect(key) {
   });
   openToTarget(url.href);
 }
-
-function goPushPage(routerName: string) {
-  const url = router.resolve({ name: routerName });
-  openToTarget(url.href);
-}
 </script>
 
 <style lang="scss" scoped>
@@ -272,74 +288,81 @@ function goPushPage(routerName: string) {
   display: flex;
   align-items: center;
   justify-content: space-between;
+  padding: 0 20px;
   min-width: $medium-width;
   height: 64px;
   background-color: #fff;
   box-shadow: inset 0 -1px #f1f2f3 !important;
+  .hr {
+    width: 100%;
+    height: 1px;
+    background-color: #e7e7e7;
+  }
   .left {
     display: flex;
     align-items: center;
-    .logo {
-      margin: 0 20px;
-      width: 100px;
-      height: 40px;
-      background-color: skyblue;
-      color: white;
-      text-align: center;
-      line-height: 40px;
+    height: 100%;
+    .logo-wrap {
+      display: flex;
+      align-items: center;
+      margin-right: 20px;
       cursor: pointer;
+
+      // .logo {
+      //   margin-right: 5px;
+      //   width: 35px;
+      //   height: 35px;
+      //   border-radius: 50%;
+      //   font-size: 12px;
+      //   // animation: rotate 3s linear infinite;
+
+      //   @include setBackground('@/assets/img/logo.webp');
+      //   @keyframes rotate {
+      //     0% {
+      //       transform: rotate(0deg);
+      //     }
+      //     100% {
+      //       transform: rotate(360deg);
+      //     }
+      //   }
+      // }
+      .txt {
+        color: $theme-color-gold;
+        font-weight: 500;
+        font-size: 18px;
+      }
     }
+
     .nav {
       display: flex;
       align-items: center;
+      height: 100%;
       .item {
         position: relative;
-        padding: 0 10px;
+        display: flex;
+        align-items: center;
+        margin-right: 20px;
+        height: 100%;
         color: black;
         text-decoration: none;
         cursor: pointer;
 
-        .list {
-          position: absolute;
-          top: 100%;
-          right: 0;
-          padding: 10px;
-          width: 100px;
-          background-color: #fff;
-          box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1),
-            0 2px 6px rgba(0, 0, 0, 0.08);
-          .item {
-            display: flex;
-            align-items: center;
-            margin-bottom: 5px;
-            .txt {
-              margin-right: 5px;
-            }
-          }
-        }
-
         &.active {
           &::after {
             position: absolute;
-            bottom: -6px;
-            left: 50%;
-            width: 40% !important;
-            height: 2px;
-            background-color: red;
+            top: calc(50% - 8px);
+            transform: translateY(-100%);
+            right: -5px;
+            width: 5px;
+            height: 5px;
+            border-radius: 50%;
+            background-color: $theme-color-gold;
             content: '';
             transition: all 0.1s ease;
-            transform: translateX(-50%);
           }
         }
-        &::after {
-          width: 0px !important;
-
-          @extend .active;
-        }
         &:hover {
-          &::after {
-            width: 40% !important;
-          }
+          color: $theme-color-gold;
         }
       }
     }
@@ -347,7 +370,7 @@ function goPushPage(routerName: string) {
   .right {
     display: flex;
     align-items: center;
-    margin-right: 20px;
+    height: 100%;
 
     .qqlogin {
       box-sizing: border-box;
@@ -355,50 +378,106 @@ function goPushPage(routerName: string) {
       width: 35px;
       height: 35px;
       border-radius: 50%;
-      background-color: skyblue;
-      background-position: center;
-      background-size: cover;
-      background-repeat: no-repeat;
-      color: white;
+      background-color: papayawhip;
       text-align: center;
       font-size: 13px;
       line-height: 35px;
       cursor: pointer;
+
+      @extend %containBg;
     }
 
-    .bilibili,
+    .about,
+    .ecosystem,
+    .sponsors,
     .github {
       position: relative;
-      margin-right: 15px;
+      display: flex;
+      align-items: center;
+      margin-right: 20px;
+      height: 100%;
       border-radius: 6px;
       color: black;
       text-decoration: none;
-      font-size: 14px;
+      font-size: 13px;
       cursor: pointer;
-      &.active {
-        &::after {
-          position: absolute;
-          bottom: -6px;
-          left: 50%;
-          width: 40% !important;
-          height: 2px;
-          background-color: red;
-          content: '';
-          transition: all 0.1s ease;
-          transform: translateX(-50%);
-        }
+      .icon {
+        fill: currentColor;
       }
-      &::after {
-        width: 0px !important;
-
-        @extend .active;
+      a {
+        color: black;
+        text-decoration: none;
+        font-size: 13px;
       }
       &:hover {
-        &::after {
-          width: 40% !important;
+        color: $theme-color-gold;
+        .icon {
+          color: $theme-color-gold;
         }
       }
     }
+    .about,
+    .ecosystem {
+      &:hover {
+        .list {
+          display: block;
+          .item {
+            &:hover {
+              color: $theme-color-gold;
+              a {
+                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);
+        .item {
+          display: flex;
+          align-items: center;
+          margin-bottom: 5px;
+          padding: 0 20px;
+          color: black;
+          .icon {
+            width: 13px;
+            color: #3c3c4354;
+          }
+        }
+        .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;
+      }
+    }
+    .about {
+      .list {
+        width: 120px;
+      }
+    }
     .github {
       display: flex;
       align-items: center;
@@ -407,12 +486,12 @@ function goPushPage(routerName: string) {
       }
     }
     .start-live {
-      margin-right: 10px;
-      padding: 5px 10px;
+      margin-right: 20px;
+      padding: 5px 15px;
       border-radius: 6px;
-      background-color: skyblue;
+      background-color: $theme-color-gold;
       color: white;
-      font-size: 14px;
+      font-size: 13px;
       cursor: pointer;
     }
   }

+ 0 - 2
src/layout/index.vue

@@ -4,13 +4,11 @@
     <router-view v-slot="{ Component }">
       <component :is="Component"></component>
     </router-view>
-    <FooterCpt></FooterCpt>
     <ModalCpt></ModalCpt>
   </div>
 </template>
 
 <script lang="ts" setup>
-import FooterCpt from './footer/index.vue';
 import HeadCpt from './head/index.vue';
 import ModalCpt from './modal/index.vue';
 </script>

+ 3 - 1
src/main.scss

@@ -1,5 +1,7 @@
 body {
   padding: 0;
   margin: 0;
-  font-family: PingFang SC;
+  font-family: Quotes, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
+    Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+    sans-serif;
 }

+ 28 - 1
src/router/index.ts

@@ -10,9 +10,14 @@ export const routerName = {
   rank: 'rank',
   sponsors: 'sponsors',
   support: 'support',
+  link: 'link',
   ad: 'ad',
+  faq: 'faq',
+  team: 'team',
   oauth: 'oauth',
+  release: 'release',
   notFound: 'notFound',
+  group: 'group',
 
   pull: 'pull',
   push: 'push',
@@ -32,7 +37,28 @@ export const defaultRoutes: RouteRecordRaw[] = [
       {
         name: routerName.about,
         path: '/about',
-        component: () => import('@/views/about/index.vue'),
+        children: [
+          {
+            name: routerName.group,
+            path: 'group',
+            component: () => import('@/views/group/index.vue'),
+          },
+          {
+            name: routerName.faq,
+            path: 'faq',
+            component: () => import('@/views/faq/index.vue'),
+          },
+          {
+            name: routerName.team,
+            path: 'team',
+            component: () => import('@/views/team/index.vue'),
+          },
+          {
+            name: routerName.release,
+            path: 'release',
+            component: () => import('@/views/release/index.vue'),
+          },
+        ],
       },
       {
         name: routerName.rank,
@@ -54,6 +80,7 @@ export const defaultRoutes: RouteRecordRaw[] = [
         path: '/ad',
         component: () => import('@/views/ad/index.vue'),
       },
+
       {
         name: routerName.pull,
         path: '/pull/:roomId',

+ 0 - 0
src/views/about/index.vue → src/views/about copy/index.vue


+ 124 - 0
src/views/faq/index.vue

@@ -0,0 +1,124 @@
+<template>
+  <div class="faq-wrap">
+    <div class="content">
+      <h1 class="title">常见问题</h1>
+      <div class="hr"></div>
+      <div class="list">
+        <div class="item">
+          <h2>billd-live是什么?</h2>
+          <p>
+            billd-live 是一个web端的直播平台,目前支持使用WebRTC或SRS进行直播。
+          </p>
+        </div>
+        <div class="item">
+          <h2>谁在维护billd-live?</h2>
+          <p>
+            billd-live 是由
+            <a
+              :href="AUTHOR_GITHUB"
+              target="_blank"
+              class="link"
+            >
+              galaxy-s10
+            </a>
+            在 2023 年作为其个人项目创建的,目前只有作者一人维护。
+          </p>
+        </div>
+        <div class="hr"></div>
+        <div class="item">
+          <h2>billd-live使用了什么技术栈?</h2>
+          <p>billd-live是一个前端全栈项目,几乎所有技术栈都是前端相关。</p>
+          <p>前端相关:Vue3、WebRTC、Typescript</p>
+          <p>
+            后端相关:Node、Koa2、MySQL、Redis、Socket.io、
+            <a
+              target="_blank"
+              class="link"
+              href="https://ossrs.net"
+            >
+              SRS
+            </a>
+            、Typescript
+          </p>
+          <p>部署相关:Docker、Jenkins</p>
+        </div>
+        <div class="hr"></div>
+        <div class="item">
+          <h2>如何参与贡献?</h2>
+          <p>非常欢迎!具体TODO</p>
+        </div>
+      </div>
+    </div>
+    <div class="aside">
+      <div class="title">本页目录</div>
+      <div class="item">billd-live是什么?</div>
+      <div class="item">谁在维护billd-live?</div>
+      <div class="item">billd-live使用了什么技术栈?</div>
+      <div class="item">如何参与贡献?</div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+import { AUTHOR_GITHUB } from '@/constant';
+
+const list = ref([]);
+</script>
+
+<style lang="scss" scoped>
+.faq-wrap {
+  display: flex;
+  box-sizing: border-box;
+  margin: 0 auto;
+  padding-top: 50px;
+  width: 960px;
+  color: rgb(33, 53, 71);
+
+  .content {
+    flex: 1;
+    .title {
+      margin: 0;
+      font-weight: 500;
+      font-size: 40px;
+      margin-bottom: 60px;
+    }
+    .hr {
+      margin: 60px 0 20px 0;
+      width: 100%;
+      height: 1px;
+      background-color: #e7e7e7;
+    }
+
+    .link {
+      color: $theme-color-gold;
+      text-decoration: none;
+      font-weight: 500;
+    }
+    .list {
+      h2 {
+        font-weight: 600;
+      }
+      .item {
+        font-size: 16px;
+      }
+    }
+  }
+  .aside {
+    padding-left: 90px;
+    .title {
+      color: rgb(33, 53, 71);
+      font-weight: 700;
+      font-size: 12px;
+      margin-bottom: 8px;
+    }
+    .item {
+      margin-bottom: 8px;
+      color: rgba(60, 60, 60, 0.7);
+      font-size: 13px;
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 29 - 0
src/views/group/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="group-wrap">
+    <h1>微信交流群 & 我的微信</h1>
+    <img
+      src="@/assets/img/wechat-group.webp"
+      alt=""
+      class="wechat-group"
+    />
+    <img
+      src="@/assets/img/my-wechat.webp"
+      alt=""
+      class="my-wechat"
+    />
+  </div>
+</template>
+
+<script lang="ts" setup></script>
+
+<style lang="scss" scoped>
+.group-wrap {
+  text-align: center;
+  .wechat-group {
+    height: 500px;
+  }
+  .my-wechat {
+    height: 500px;
+  }
+}
+</style>

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

@@ -171,7 +171,7 @@ function joinFlvRoom() {
   padding: 20px 0;
   min-width: $large-width;
   height: 610px;
-  background-color: skyblue;
+  background-color: papayawhip;
   text-align: center;
 
   .left {
@@ -223,14 +223,14 @@ function joinFlvRoom() {
 
       .btn {
         padding: 14px 26px;
-        border: 2px solid rgba($color: skyblue, $alpha: 0.5);
+        border: 2px solid rgba($color: papayawhip, $alpha: 0.5);
         border-radius: 6px;
         background-color: rgba(0, 0, 0, 0.3);
-        color: skyblue;
+        color: papayawhip;
         font-size: 16px;
         cursor: pointer;
         &:hover {
-          background-color: rgba($color: skyblue, $alpha: 0.5);
+          background-color: rgba($color: papayawhip, $alpha: 0.5);
           color: white;
         }
         &.webrtc {
@@ -275,7 +275,7 @@ function joinFlvRoom() {
           bottom: 0;
           left: 0;
           z-index: 1;
-          border: 2px solid skyblue;
+          border: 2px solid papayawhip;
           border-radius: 4px;
         }
         .triangle {
@@ -284,7 +284,7 @@ function joinFlvRoom() {
           left: 0;
           display: inline-block;
           border: 5px solid transparent;
-          border-right-color: skyblue;
+          border-right-color: papayawhip;
           transform: translate(-100%, -50%);
         }
         &.active {
@@ -327,6 +327,7 @@ function joinFlvRoom() {
       }
     }
     .none {
+      width: 200px;
       color: white;
       font-size: 14px;
     }

+ 5 - 3
src/views/pull/index.vue

@@ -189,6 +189,7 @@ import { nextTick, onMounted, onUnmounted, ref } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { fetchGoodsList } from '@/api/goods';
+import { loginTip } from '@/hooks/use-login';
 import { usePull } from '@/hooks/use-pull';
 import {
   DanmuMsgTypeEnum,
@@ -256,6 +257,7 @@ async function getGoodsList() {
 }
 
 function handleRecharge() {
+  if (!loginTip()) return;
   showRecharge.value = !showRecharge.value;
 }
 
@@ -333,7 +335,7 @@ onMounted(() => {
           width: 64px;
           height: 64px;
           border-radius: 50%;
-          background-color: skyblue;
+          background-color: $theme-color-gold;
         }
         .detail {
           .top {
@@ -507,7 +509,7 @@ onMounted(() => {
             width: 25px;
             height: 25px;
             border-radius: 50%;
-            background-color: skyblue;
+            background-color: $theme-color-gold;
           }
           .username {
             color: black;
@@ -560,7 +562,7 @@ onMounted(() => {
         padding: 5px;
         width: 80px;
         border-radius: 4px;
-        background-color: skyblue;
+        background-color: $theme-color-gold;
         color: white;
         text-align: center;
         font-size: 12px;

+ 1 - 1
src/views/pull/recharge/index.vue

@@ -57,7 +57,7 @@ const goodsInfo = reactive({
 async function startPay() {
   console.log(money.value, minMoney);
   if (money.value < minMoney) {
-    window.$message.warning(`最少充值${minMoney}元`);
+    window.$message.warning(`最少充值${minMoney}元`);
     return;
   }
   const res = await fetchFindByTypeGoods(GoodsTypeEnum.recharge);

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

@@ -109,7 +109,7 @@
           <div class="top">
             <span class="item">
               <i class="ico"></i>
-              <span>正在观看人数:{{ liveUserList.length }}</span>
+              <span>正在观看:{{ liveUserList.length }}</span>
             </span>
           </div>
           <div class="bottom">

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

@@ -219,7 +219,7 @@ async function getUserList() {
       margin: 0 10px;
       height: 40px;
       border-radius: 10px;
-      background-color: skyblue;
+      background-color: $theme-color-gold;
       color: white;
       text-align: center;
       font-weight: bold;

+ 62 - 0
src/views/release/index.vue

@@ -0,0 +1,62 @@
+<template>
+  <div class="release-wrap">
+    <div class="content">
+      <h1 class="title">版本发布</h1>
+      <p>最近更新:{{ billd.lastBuildDate }}</p>
+      <p>提交哈希:{{ billd.commitHash }}</p>
+    </div>
+    <div class="aside">
+      <div class="title">本页目录</div>
+      <div class="item">TODO</div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { BilldHtmlWebpackPluginLog } from '@/interface';
+
+// @ts-ignore
+const billd: BilldHtmlWebpackPluginLog = process.env.BilldHtmlWebpackPlugin;
+</script>
+
+<style lang="scss" scoped>
+.release-wrap {
+  display: flex;
+  box-sizing: border-box;
+  margin: 0 auto;
+  padding-top: 50px;
+  width: 960px;
+  color: rgb(33, 53, 71);
+
+  .content {
+    flex: 1;
+    .title {
+      margin: 0;
+      font-weight: 500;
+      font-size: 40px;
+      margin-bottom: 20px;
+    }
+    .hr {
+      margin: 60px 0 20px 0;
+      width: 100%;
+      height: 1px;
+      background-color: #e7e7e7;
+    }
+  }
+  .aside {
+    padding-left: 90px;
+    .title {
+      color: rgb(33, 53, 71);
+      font-weight: 700;
+      font-size: 12px;
+      margin-bottom: 8px;
+    }
+    .item {
+      margin-bottom: 8px;
+      color: rgba(60, 60, 60, 0.7);
+      font-size: 13px;
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 196 - 0
src/views/sponsors copy/index.vue

@@ -0,0 +1,196 @@
+<template>
+  <div class="sponsors-wrap">
+    <h1 class="txt">
+      截止至{{ onMountedTime }},已收到:{{ receiveMoney / 100 }}元赞助~
+    </h1>
+    <div class="pay-list">
+      <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 }}({{ 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>
+    <h2>开始赞助(支付宝)</h2>
+    <div class="gift-list">
+      <div
+        v-for="(item, index) in sponsorsGoodsList"
+        :key="index"
+        class="item"
+        @click="startPay(item)"
+      >
+        {{ item.name }}({{ item.price }}元)
+      </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, onUnmounted, reactive, ref } from 'vue';
+
+import { fetchGoodsList } from '@/api/goods';
+import { fetchOrderList } from '@/api/order';
+import QrPayCpt from '@/components/QrPay/index.vue';
+import { GoodsTypeEnum, IGoods, IOrder, PayStatusEnum } from '@/interface';
+
+const onMountedTime = ref('');
+const payStatusTimer = ref();
+const downTimer = ref();
+const receiveMoney = ref(0);
+const showQrPay = ref(false);
+const goodsInfo = reactive({
+  money: '0.00',
+  goodsId: -1,
+  liveRoomId: -1,
+});
+
+const payList = ref<IOrder[]>([]);
+const sponsorsGoodsList = ref<IGoods[]>([]);
+
+onUnmounted(() => {
+  clearInterval(payStatusTimer.value);
+  clearInterval(downTimer.value);
+});
+
+onMounted(() => {
+  onMountedTime.value = new Date().toLocaleString();
+  getPayList();
+  getGoodsList();
+});
+
+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({
+      trade_status: PayStatusEnum.TRADE_SUCCESS,
+    });
+    if (res.code === 200) {
+      payList.value = res.data.rows;
+      receiveMoney.value = payList.value.reduce(
+        (pre, item) => pre + Number(item.total_amount) * 100,
+        0
+      );
+    }
+  } catch (error) {
+    console.log(error);
+  }
+}
+
+function startPay(item: IGoods) {
+  showQrPay.value = false;
+  nextTick(() => {
+    goodsInfo.money = item.price!;
+    goodsInfo.goodsId = item.id!;
+    showQrPay.value = true;
+  });
+}
+</script>
+
+<style lang="scss" scoped>
+.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;
+    .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;
+        justify-content: center;
+        .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 {
+    display: flex;
+    align-items: center;
+    flex-wrap: wrap;
+    justify-content: center;
+    .item {
+      margin: 5px;
+      padding: 5px 10px;
+      border-radius: 4px;
+      background-color: skyblue;
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 225 - 165
src/views/sponsors/index.vue

@@ -1,194 +1,254 @@
 <template>
   <div class="sponsors-wrap">
-    <h1 class="txt">
-      截止至{{ onMountedTime }},已收到:{{ receiveMoney / 100 }}元赞助~
-    </h1>
-    <div class="pay-list">
-      <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 class="content">
+      <h1 class="title">成为billd-live的赞助者</h1>
+      <div class="desc">
+        目前billd-live仅仅是作者业余时间开发以及维护,需要投入非常多时间以及精力,
+        你的赞助将会为billd-live提供经济支持。
+      </div>
+      <div class="hr"></div>
+      <div class="list">
+        <div class="item">
+          <h2>以企业名义赞助billd-live</h2>
+          <p>如果你是企业用户,并且从billd-live中受益,请考虑捐赠以示感谢。</p>
         </div>
-
-        <div class="account">支付宝账号:{{ item.buyer_logon_id }},</div>
-        <div class="gift">
-          赞助了:{{ item.subject }}({{ item.total_amount }}元),
+        <div class="hr"></div>
+        <div class="item">
+          <h2>以个人名义赞助billd-live</h2>
+          <p>
+            如果你是个人用户,并且从billd-live中受益,请考虑捐赠以示感谢——就当是偶尔请我们喝杯咖啡。
+          </p>
         </div>
-        <div class="status">
-          状态:{{
-            item.trade_status === PayStatusEnum.WAIT_BUYER_PAY
-              ? '支付中'
-              : '已支付'
-          }},
+        <div class="hr"></div>
+        <div class="item sponsors">
+          <h2>当前赞助商</h2>
+          <h3>铂金赞助商</h3>
+          <div class="hr"></div>
+          <div class="list platinum-list">
+            <a
+              v-for="(item, index) in platinumList"
+              :key="index"
+              class="bg platinum"
+              :href="item.url"
+              @click.prevent="openToTarget(item.url)"
+            >
+              <img
+                :src="item.logo"
+                alt=""
+              />
+            </a>
+          </div>
+          <h3>金牌赞助商</h3>
+          <div class="hr"></div>
+          <div class="list gold-list">
+            <a
+              v-for="(item, index) in goldList"
+              :key="index"
+              class="bg gold"
+              :href="item.url"
+              @click.prevent="openToTarget(item.url)"
+            >
+              <img
+                :src="item.logo"
+                alt=""
+              />
+            </a>
+          </div>
+          <h3>银牌赞助商</h3>
+          <div class="hr"></div>
+          <div class="list silver-list">
+            <a
+              v-for="(item, index) in silverList"
+              :key="index"
+              class="bg silver"
+              :href="item.url"
+              @click.prevent="openToTarget(item.url)"
+            >
+              <img
+                :src="item.logo"
+                alt=""
+              />
+            </a>
+          </div>
         </div>
-        <div class="time">支付时间:{{ item.send_pay_date || '-' }}</div>
       </div>
     </div>
-    <h2>开始赞助(支付宝)</h2>
-    <div class="gift-list">
-      <div
-        v-for="(item, index) in sponsorsGoodsList"
-        :key="index"
-        class="item"
-        @click="startPay(item)"
-      >
-        {{ item.name }}({{ item.price }}元)
-      </div>
+    <div class="aside">
+      <div class="title">本页目录</div>
+      <div class="item">以个人名义赞助billd-live</div>
+      <div class="item">以企业名义赞助billd-live</div>
+      <div class="item">当前赞助商</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, onUnmounted, reactive, ref } from 'vue';
-
-import { fetchGoodsList } from '@/api/goods';
-import { fetchOrderList } from '@/api/order';
-import QrPayCpt from '@/components/QrPay/index.vue';
-import { GoodsTypeEnum, IGoods, IOrder, PayStatusEnum } from '@/interface';
-
-const onMountedTime = ref('');
-const payStatusTimer = ref();
-const downTimer = ref();
-const receiveMoney = ref(0);
-const showQrPay = ref(false);
-const goodsInfo = reactive({
-  money: '0.00',
-  goodsId: -1,
-  liveRoomId: -1,
-});
-
-const payList = ref<IOrder[]>([]);
-const sponsorsGoodsList = ref<IGoods[]>([]);
-
-onUnmounted(() => {
-  clearInterval(payStatusTimer.value);
-  clearInterval(downTimer.value);
-});
+import { openToTarget } from 'billd-utils';
+import { ref } from 'vue';
 
-onMounted(() => {
-  onMountedTime.value = new Date().toLocaleString();
-  getPayList();
-  getGoodsList();
-});
+// 铂金赞助
+const platinumList = ref([
+  {
+    logo: 'https://sponsors.vuejs.org/images/line_corporation.avif',
+    url: 'aaa',
+  },
+  {
+    logo: 'https://sponsors.vuejs.org/images/vueschool.avif',
+    url: 'bbb',
+  },
+]);
 
-async function getGoodsList() {
-  const res = await fetchGoodsList({
-    type: GoodsTypeEnum.sponsors,
-    orderName: 'created_at',
-    orderBy: 'desc',
-  });
-  if (res.code === 200) {
-    sponsorsGoodsList.value = res.data.rows;
-  }
-}
+// 金牌赞助
+const goldList = ref([
+  {
+    logo: 'https://sponsors.vuejs.org/images/famous_fonts.avif',
+    url: '',
+  },
+  {
+    logo: 'https://sponsors.vuejs.org/images/herodevs.avif',
+    url: '',
+  },
+  {
+    logo: 'https://sponsors.vuejs.org/images/herodevs.avif',
+    url: '',
+  },
+]);
 
-async function getPayList() {
-  try {
-    const res = await fetchOrderList({
-      trade_status: PayStatusEnum.TRADE_SUCCESS,
-    });
-    if (res.code === 200) {
-      payList.value = res.data.rows;
-      receiveMoney.value = payList.value.reduce(
-        (pre, item) => pre + Number(item.total_amount) * 100,
-        0
-      );
-    }
-  } catch (error) {
-    console.log(error);
-  }
-}
-
-function startPay(item: IGoods) {
-  showQrPay.value = false;
-  nextTick(() => {
-    goodsInfo.money = item.price!;
-    goodsInfo.goodsId = item.id!;
-    showQrPay.value = true;
-  });
-}
+// 银牌赞助
+const silverList = ref([
+  {
+    logo: 'https://sponsors.vuejs.org/images/vuemastery.avif',
+    url: '',
+  },
+  {
+    logo: 'https://sponsors.vuejs.org/images/herodevs.avif',
+    url: '',
+  },
+  {
+    logo: 'https://sponsors.vuejs.org/images/herodevs.avif',
+    url: '',
+  },
+]);
 </script>
 
 <style lang="scss" scoped>
 .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;
-    .item {
-      display: inline-flex;
-      flex-wrap: wrap;
-      justify-content: center;
-      margin-bottom: 4px;
+  display: flex;
+  box-sizing: border-box;
+  margin: 0 auto;
+  padding-top: 50px;
+  width: 960px;
+  color: rgb(33, 53, 71);
+
+  .content {
+    flex: 1;
+    font-size: 16px;
+    .title {
+      margin: 0;
+      margin-bottom: 60px;
+      font-weight: 500;
+      font-size: 40px;
+    }
+    .hr {
+      margin: 60px 0 20px 0;
       width: 100%;
-      text-align: left;
+      height: 1px;
+      background-color: #e7e7e7;
+    }
 
-      .user {
-        width: 120px;
-        padding: 0 10px;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        .avatar {
-          width: 30px;
-          height: 30px;
-          border-radius: 50%;
-        }
-        .username {
-          @extend %singleEllipsis;
-        }
-      }
-      .account {
-        width: 250px;
-      }
-      .gift {
-        width: 260px;
-      }
-      .status {
-        width: 120px;
-        text-align: left;
+    .link {
+      color: $theme-color-gold;
+      text-decoration: none;
+      font-weight: 500;
+    }
+    .list {
+      h2 {
+        font-weight: 600;
       }
-      .time {
-        width: 280px;
+
+      .item {
+        font-size: 16px;
+        &.sponsors {
+          h3 {
+            margin-top: 50px;
+            text-align: center;
+          }
+          .list {
+            display: flex;
+            flex-wrap: wrap;
+            justify-content: space-evenly;
+          }
+          .bg {
+            display: flex;
+            align-items: center;
+            flex-shrink: 0;
+            justify-content: center;
+            box-sizing: border-box;
+            margin-bottom: 5px;
+            border-radius: 4px;
+            background-color: #f9f9f9;
+            cursor: pointer;
+            &.platinum {
+              width: 100%;
+              height: 160px;
+              img {
+                width: 300px;
+              }
+            }
+            &.gold {
+              width: 49%;
+              height: 120px;
+              img {
+                width: 200px;
+              }
+              // 最后一行是2个元素,则将最后一个元素添加一个30%
+              &:last-child:nth-child(2n-1) {
+                margin-right: calc(((100% - (49% * 2)) * 1 / 2) + (49% * 1));
+              }
+              // 最后一行是1个元素
+              &:last-child:nth-child(2n-2) {
+                margin-right: calc(((100% - (49% * 2)) * 2 / 2) + (49% * 2));
+              }
+            }
+            &.silver {
+              width: 32%;
+              height: 90px;
+              img {
+                width: 150px;
+              }
+              // 最后一行是3个元素,则将最后一行3个元素都添加底部外边距
+              &:last-child:nth-child(3n) {
+              }
+              // 最后一行是2个元素,则将最后一个元素添加一个30%
+              &:last-child:nth-child(3n-1) {
+                margin-right: calc(((100% - (32% * 3)) * 1 / 4) + (32% * 1));
+              }
+              // 最后一行是1个元素
+              &:last-child:nth-child(3n-2) {
+                margin-right: calc(((100% - (32% * 3)) * 2 / 4) + (32% * 2));
+              }
+            }
+            img {
+              max-width: 100%;
+              max-height: 100%;
+            }
+          }
+        }
       }
     }
   }
-  .gift-list {
-    display: flex;
-    align-items: center;
-    flex-wrap: wrap;
-    justify-content: center;
+  .aside {
+    padding-left: 90px;
+    .title {
+      margin-bottom: 8px;
+      color: rgb(33, 53, 71);
+      font-weight: 700;
+      font-size: 12px;
+    }
     .item {
-      margin: 5px;
-      padding: 5px 10px;
-      border-radius: 4px;
-      background-color: skyblue;
+      margin-bottom: 8px;
+      color: rgba(60, 60, 60, 0.7);
+      font-size: 13px;
       cursor: pointer;
     }
   }

+ 275 - 0
src/views/team/index.vue

@@ -0,0 +1,275 @@
+<template>
+  <div class="team-wrap">
+    <h1 class="title">认识团队</h1>
+    <p class="desc">
+      billd-live目前是个人开发,暂时没有贡献者,但以后可能会有,以下是目前部分团队成员的个人信息。
+    </p>
+    <div class="hr"></div>
+    <div class="core-team">
+      <div class="info">
+        <h2 class="title">核心团队成员</h2>
+        <div class="desc">
+          核心团队成员是那些积极参与维护一个或多个核心项目的人。他们对billd-live
+          的生态系统做出了重大贡献,并对项目及其用户的成功做出了长期的承诺。
+        </div>
+      </div>
+      <div class="members">
+        <div
+          v-for="(item, index) in list"
+          :key="index"
+          class="item"
+        >
+          <img
+            class="avatar"
+            src="https://www.github.com/galaxy-s10.png"
+            alt=""
+          />
+          <div class="data">
+            <div class="name">{{ item.name }}</div>
+            <div class="org">{{ item.org }}</div>
+            <div class="profiles">
+              <div class="desc skills">
+                <n-icon size="18">
+                  <CodeOutline></CodeOutline>
+                </n-icon>
+                <div class="txt">
+                  <a
+                    v-for="(iten, indey) in item.skill"
+                    :key="'skill-' + indey"
+                    class="skill link"
+                    :href="iten.github"
+                    @click.prevent="openToTarget(iten.github)"
+                  >
+                    {{ iten.label }}
+                  </a>
+                </div>
+              </div>
+              <div class="desc country">
+                <n-icon size="18">
+                  <LocationOutline></LocationOutline>
+                </n-icon>
+                <div class="txt">{{ item.country }}</div>
+              </div>
+              <div class="desc langues">
+                <n-icon size="18">
+                  <GlobeOutline></GlobeOutline>
+                </n-icon>
+                <div class="txt">
+                  <span
+                    v-for="(iten, indey) in item.langues"
+                    :key="'lang-' + indey"
+                    class="langue"
+                  >
+                    {{ iten }}
+                  </span>
+                </div>
+              </div>
+              <div class="desc">
+                <n-icon size="18">
+                  <Link></Link>
+                </n-icon>
+                <a
+                  :href="item.website"
+                  class="txt link"
+                  @click.prevent="openToTarget(item.website)"
+                >
+                  {{ item.website }}
+                </a>
+              </div>
+            </div>
+            <div class="social-list">
+              <a
+                v-for="(iten, indez) in item.social"
+                :key="'social-' + indez"
+                class="social link"
+                :href="iten.github"
+                @click.prevent="openToTarget(iten.github)"
+              >
+                <n-icon size="22">
+                  <LogoGithub></LogoGithub>
+                </n-icon>
+              </a>
+            </div>
+          </div>
+          <div class="sponsor">
+            <n-icon
+              size="18"
+              class="ico"
+            >
+              <HeartOutline></HeartOutline>
+            </n-icon>
+            赞助
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {
+  CodeOutline,
+  GlobeOutline,
+  HeartOutline,
+  Link,
+  LocationOutline,
+  LogoGithub,
+} from '@vicons/ionicons5';
+import { openToTarget } from 'billd-utils';
+import { ref } from 'vue';
+
+const list = ref([
+  {
+    avatar: 'https://www.github.com/galaxy-s10.png',
+    name: 'galaxy-s10',
+    org: 'Creator @billd-live',
+    country: 'Guangzho, China',
+    langues: ['中文'],
+    skill: [
+      {
+        label: 'billd-live',
+        github: 'https://github.com/galaxy-s10/billd-live',
+      },
+      {
+        label: 'billd-live-server',
+        github: 'https://github.com/galaxy-s10/billd-live-server',
+      },
+    ],
+    social: [{ github: 'https://www.github.com/galaxy-s10' }],
+    website: 'https://www.hsslive.cn',
+  },
+]);
+</script>
+
+<style lang="scss" scoped>
+.team-wrap {
+  box-sizing: border-box;
+  margin: 0 auto;
+  padding-top: 50px;
+  width: 960px;
+  .link {
+    color: $theme-color-gold;
+    text-decoration: none;
+    cursor: pointer;
+  }
+
+  .title {
+    margin: 0;
+    font-weight: 500;
+    font-size: 40px;
+  }
+  .desc {
+    margin: 0;
+    width: 500px;
+    color: #3c3c3cb3;
+    font-size: 16px;
+    line-height: 1.8;
+  }
+  .hr {
+    margin: 60px 0 20px 0;
+    width: 100%;
+    height: 1px;
+    background-color: #e7e7e7;
+  }
+  .core-team {
+    display: flex;
+    justify-content: space-between;
+    padding-bottom: 30px;
+    .info {
+      .title {
+        font-size: 20px;
+      }
+      .desc {
+        box-sizing: border-box;
+        margin-top: 3px;
+        padding-right: 60px;
+        width: 300px;
+        font-size: 14px;
+        line-height: 1.8;
+      }
+    }
+    .members {
+      flex-grow: 1;
+
+      .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;
+            font-size: 20px;
+          }
+          .org {
+            margin-bottom: 10px;
+            font-size: 14px;
+          }
+          .profiles {
+            margin-bottom: 10px;
+
+            .desc {
+              display: flex;
+              align-items: center;
+              margin-bottom: 7px;
+              .txt {
+                margin-left: 10px;
+              }
+              &.langues,
+              &.skills {
+                .langue:not(:last-child),
+                .skill:not(:last-child) {
+                  &:after {
+                    margin: 0 7px;
+                    color: #3c3c3c54;
+                    content: '•';
+                    font-size: 14px;
+                  }
+                }
+              }
+            }
+          }
+          .social-list {
+            padding-top: 2px;
+            .social {
+              color: #747474;
+              cursor: pointer;
+            }
+          }
+        }
+        .sponsor {
+          position: absolute;
+          top: 20px;
+          right: 20px;
+          display: flex;
+          align-items: center;
+          padding: 4px 8px;
+          border: 1px solid $theme-color-gold;
+          border-radius: 4px;
+          color: $theme-color-gold;
+          cursor: pointer;
+          transition: all 0.3s ease;
+          &:hover {
+            background-color: $theme-color-gold;
+            color: white;
+          }
+          .ico {
+            margin-right: 4px;
+          }
+        }
+      }
+    }
+  }
+}
+</style>