Bladeren bron

feat:添加农场美景视频页面和其他两个空页面

wangsisi 1 week geleden
bovenliggende
commit
b0f824e122
50 gewijzigde bestanden met toevoegingen van 4808 en 8 verwijderingen
  1. 4 1
      index.html
  2. 2 0
      package.json
  3. 4 0
      public/footer/tab-farm-active.svg
  4. 4 0
      public/footer/tab-farm.svg
  5. 6 0
      public/footer/tab-guard-active.svg
  6. 6 0
      public/footer/tab-guard.svg
  7. 5 0
      public/footer/tab-map-active.svg
  8. 5 0
      public/footer/tab-map.svg
  9. BIN
      public/video/demo.mp4
  10. 7 0
      src/App.vue
  11. BIN
      src/assets/img/icon/add-light.png
  12. 1 0
      src/assets/img/icon/love.svg
  13. 1 0
      src/assets/img/icon/loved.svg
  14. BIN
      src/assets/img/icon/ok-red.png
  15. BIN
      src/assets/img/icon/share-white-full.png
  16. 66 0
      src/assets/less/index.less
  17. 187 0
      src/components/BaseFooter.vue
  18. 34 0
      src/components/BaseMask.vue
  19. 160 0
      src/components/Comment.vue
  20. 184 0
      src/components/UserPanel.vue
  21. 141 0
      src/components/slide/SlideHorizontal.vue
  22. 14 0
      src/components/slide/SlideItem.vue
  23. 84 0
      src/components/slide/SlideVertical.vue
  24. 252 0
      src/components/video/BaseVideo.vue
  25. 101 0
      src/components/video/ItemDesc.vue
  26. 216 0
      src/components/video/ItemToolbar.vue
  27. 11 2
      src/env.d.ts
  28. 24 1
      src/main.ts
  29. 124 0
      src/mock/homeData.ts
  30. 1722 0
      src/mock/posts6.json
  31. 3 3
      src/router/globalRoutes.js
  32. 14 1
      src/router/mainRoutes.js
  33. 51 0
      src/utils/bus.ts
  34. 21 0
      src/utils/const_var.ts
  35. 35 0
      src/utils/dom.ts
  36. 8 0
      src/utils/hooks/useNav.ts
  37. 42 0
      src/utils/index.ts
  38. 196 0
      src/utils/slide.ts
  39. 47 0
      src/views/guard-map/index.vue
  40. 218 0
      src/views/home/components/IndicatorHome.vue
  41. 126 0
      src/views/home/components/VideoFeed.vue
  42. 367 0
      src/views/home/index.vue
  43. 57 0
      src/views/home/slide/Community.vue
  44. 74 0
      src/views/home/slide/LongVideo.vue
  45. 59 0
      src/views/home/slide/Slide0.vue
  46. 58 0
      src/views/home/slide/Slide2.vue
  47. 12 0
      src/views/home/slide/Slide4.vue
  48. 47 0
      src/views/my-guard/index.vue
  49. 1 0
      tsconfig.app.json
  50. 7 0
      vite.config.ts

+ 4 - 1
index.html

@@ -3,7 +3,10 @@
   <head>
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/vue.svg" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
+    />
     <title>飞鸟管家</title>
   </head>
   <body>

+ 2 - 0
package.json

@@ -9,7 +9,9 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@iconify/vue": "^5.0.1",
     "axios": "^1.16.1",
+    "less": "^4.6.4",
     "normalize.css": "^8.0.1",
     "nprogress": "^0.2.0",
     "qs": "^6.15.2",

+ 4 - 0
public/footer/tab-farm-active.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
+  <path d="M24 6L10 40h28L24 6z" fill="#ffb400"/>
+  <path d="M24 16v20M17 34h14" stroke="#ffb400" stroke-width="2.5" stroke-linecap="round"/>
+</svg>

+ 4 - 0
public/footer/tab-farm.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
+  <path d="M24 8L12 38h24L24 8z" fill="currentColor"/>
+  <path d="M24 14v24M18 32h12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
+</svg>

+ 6 - 0
public/footer/tab-guard-active.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
+  <rect x="8" y="12" width="32" height="24" rx="3" stroke="#ffb400" stroke-width="2"/>
+  <path d="M8 20h32" stroke="#ffb400" stroke-width="2"/>
+  <circle cx="18" cy="28" r="3" fill="#ffb400"/>
+  <path d="M28 26l4 4 8-8" stroke="#ffb400" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 6 - 0
public/footer/tab-guard.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
+  <rect x="8" y="12" width="32" height="24" rx="3" stroke="currentColor" stroke-width="2"/>
+  <path d="M8 20h32" stroke="currentColor" stroke-width="2"/>
+  <circle cx="18" cy="28" r="3" fill="currentColor"/>
+  <path d="M28 26l4 4 8-8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 5 - 0
public/footer/tab-map-active.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
+  <path d="M8 28c4-8 10-12 16-12s12 4 16 12" stroke="#ffb400" stroke-width="2.5" stroke-linecap="round"/>
+  <path d="M14 26c2-4 6-6 10-6s8 2 10 6" stroke="#ffb400" stroke-width="2" stroke-linecap="round"/>
+  <circle cx="32" cy="18" r="3" fill="#ffb400"/>
+</svg>

+ 5 - 0
public/footer/tab-map.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
+  <path d="M8 28c4-8 10-12 16-12s12 4 16 12" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
+  <path d="M14 26c2-4 6-6 10-6s8 2 10 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
+  <circle cx="32" cy="18" r="3" fill="currentColor"/>
+</svg>

BIN
public/video/demo.mp4


+ 7 - 0
src/App.vue

@@ -1,3 +1,10 @@
 <template>
   <router-view />
 </template>
+
+<style lang="less">
+#app {
+  height: 100%;
+  width: 100%;
+}
+</style>

BIN
src/assets/img/icon/add-light.png


+ 1 - 0
src/assets/img/icon/love.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1539614702463" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2049" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M739.584 70.592c-92.224 0-177.792 63.04-228.224 109.568C460.864 133.632 375.36 70.592 283.008 70.592 108.48 70.592 0 176.96 0 348.16 0 492.8 130.688 608.256 131.2 608.64l340.544 328.512c10.432 10.432 24.448 16.256 39.552 16.256s29.056-5.824 39.296-16l341.248-328.64c30.656-29.376 130.752-134.848 130.752-260.544C1022.656 176.96 914.176 70.592 739.584 70.592z" p-id="2050" fill="#ffffff"></path></svg>

+ 1 - 0
src/assets/img/icon/loved.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1539708547888" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1476" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M739.584 70.592c-92.224 0-177.792 63.04-228.224 109.568C460.864 133.632 375.36 70.592 283.008 70.592 108.48 70.592 0 176.96 0 348.16 0 492.8 130.688 608.256 131.2 608.64l340.544 328.512c10.432 10.432 24.448 16.256 39.552 16.256s29.056-5.824 39.296-16l341.248-328.64c30.656-29.376 130.752-134.848 130.752-260.544C1022.656 176.96 914.176 70.592 739.584 70.592z" p-id="1477" fill="#e73a57"></path></svg>

BIN
src/assets/img/icon/ok-red.png


BIN
src/assets/img/icon/share-white-full.png


+ 66 - 0
src/assets/less/index.less

@@ -0,0 +1,66 @@
+:root {
+  --home-header-height: 44rem;
+  --footer-height: 56rem;
+  --main-bg: rgb(21, 23, 36);
+  --second-btn-color: rgb(58, 58, 70);
+  --footer-color: black;
+  --mask-dark: #000000bb;
+  --mask-light: transparent;
+  --mask-white: rgba(0, 0, 0, 0.4);
+  --second-btn-color-tran: rgba(58, 58, 70, 0.4);
+  --primary-btn-color: #fe2c55;
+}
+
+html,
+body {
+  width: 100%;
+  height: 100%;
+  background: var(--main-bg);
+  font-size: 1px;
+  margin: 0;
+  padding: 0;
+  overflow: hidden;
+}
+
+#app {
+  height: 100%;
+  width: 100%;
+  position: relative;
+  font-size: 14rem;
+}
+
+.slide {
+  touch-action: none;
+  height: 100%;
+  width: 100%;
+  position: relative;
+  overflow: hidden;
+
+  .slide-list {
+    height: 100%;
+    width: 100%;
+    display: flex;
+    position: relative;
+  }
+
+  &.vertical {
+    height: 100%;
+  }
+}
+
+.flex-direction-column {
+  flex-direction: column;
+}
+
+.global-notice {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  background: rgba(0, 0, 0, 0.75);
+  color: white;
+  padding: 12rem 20rem;
+  border-radius: 8rem;
+  font-size: 14rem;
+  z-index: 9999;
+}

+ 187 - 0
src/components/BaseFooter.vue

@@ -0,0 +1,187 @@
+<template>
+  <Teleport to="body">
+    <div v-if="visible" class="footer-wrap">
+      <nav class="footer" :class="{ isWhite }">
+        <div
+          v-for="item in tabs"
+          :key="item.id"
+          class="footer-item"
+          :class="{ active: currentTab === item.id }"
+          @click="tab(item.id)"
+        >
+          <div class="icon-placeholder">
+            <img
+              class="icon-img"
+              :src="currentTab === item.id ? item.iconActive : item.icon"
+              :alt="item.label"
+            />
+          </div>
+          <span class="label" :class="{ active: currentTab === item.id }">{{ item.label }}</span>
+        </div>
+      </nav>
+    </div>
+  </Teleport>
+</template>
+
+<script setup lang="ts">
+import { onMounted, onUnmounted, ref, watch } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import bus, { EVENT_KEY } from '@/utils/bus'
+
+const props = defineProps({
+  initTab: { type: Number, default: 1 },
+  isWhite: { type: Boolean, default: false },
+})
+
+const router = useRouter()
+const route = useRoute()
+const currentTab = ref(props.initTab)
+const visible = ref(true)
+
+const tabs = [
+  {
+    id: 1,
+    path: '/',
+    label: '农场美景',
+    icon: '/footer/tab-farm.svg',
+    iconActive: '/footer/tab-farm-active.svg',
+  },
+  {
+    id: 2,
+    path: '/guard-map',
+    label: '守护地图',
+    icon: '/footer/tab-map.svg',
+    iconActive: '/footer/tab-map-active.svg',
+  },
+  {
+    id: 3,
+    path: '/my-guard',
+    label: '我的守护',
+    icon: '/footer/tab-guard.svg',
+    iconActive: '/footer/tab-guard-active.svg',
+  },
+]
+
+function syncTabFromRoute() {
+  const item = tabs.find((t) => t.path === route.path)
+  if (item) currentTab.value = item.id
+}
+
+function tab(index: number) {
+  const item = tabs.find((t) => t.id === index)
+  if (!item) return
+  currentTab.value = index
+  if (route.path !== item.path) {
+    router.push(item.path)
+  }
+}
+
+watch(() => route.path, syncTabFromRoute)
+
+onMounted(() => {
+  syncTabFromRoute()
+  bus.on('setFooterVisible', (e) => {
+    visible.value = e as boolean
+  })
+  bus.on(EVENT_KEY.ENTER_FULLSCREEN, () => {
+    visible.value = false
+  })
+  bus.on(EVENT_KEY.EXIT_FULLSCREEN, () => {
+    visible.value = true
+  })
+})
+
+onUnmounted(() => {
+  bus.off(EVENT_KEY.ENTER_FULLSCREEN)
+  bus.off(EVENT_KEY.EXIT_FULLSCREEN)
+})
+</script>
+
+<style scoped lang="less">
+.footer-wrap {
+  position: fixed;
+  left: 50%;
+  bottom: 16rem;
+  z-index: 10;
+  transform: translateX(-50%);
+  width: calc(100vw - 32rem);
+  max-width: 360rem;
+  pointer-events: none;
+  box-sizing: border-box;
+}
+
+.footer {
+  pointer-events: auto;
+  display: flex;
+  align-items: center;
+  justify-content: space-evenly;
+  width: 100%;
+  height: 56rem;
+  padding: 8rem 12rem;
+  box-sizing: border-box;
+  background: #4a4a4a;
+  border: 1px solid rgba(255, 255, 255, 0.35);
+  border-radius: 999rem;
+  box-shadow: 0 2rem 8rem rgba(0, 0, 0, 0.25);
+
+  &.isWhite {
+    background: #f5f5f5;
+    border-color: rgba(0, 0, 0, 0.12);
+  }
+}
+
+.footer-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 4rem;
+  cursor: pointer;
+  min-width: 0;
+  text-align: center;
+}
+
+.icon-placeholder {
+  width: 28rem;
+  height: 28rem;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.icon-img {
+  display: block;
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+  opacity: 0.4;
+}
+
+.footer-item.active .icon-img {
+  opacity: 1;
+}
+
+.label {
+  width: 100%;
+  font-size: 11rem;
+  line-height: 1.2;
+  text-align: center;
+  color: rgba(255, 255, 255, 0.35);
+  white-space: nowrap;
+
+  &.active {
+    color: #ffb400;
+    font-weight: 500;
+  }
+}
+
+.footer.isWhite .label {
+  color: rgba(0, 0, 0, 0.35);
+
+  &.active {
+    color: #e6a000;
+  }
+}
+</style>

+ 34 - 0
src/components/BaseMask.vue

@@ -0,0 +1,34 @@
+<template>
+  <div class="Mask" :class="mode" @click="$emit('click')" />
+</template>
+
+<script setup lang="ts">
+defineProps({
+  mode: { type: String, default: 'dark' },
+})
+defineEmits<{ click: [] }>()
+</script>
+
+<style lang="less">
+.Mask {
+  z-index: 3;
+  position: fixed;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: var(--mask-dark);
+
+  &.dark {
+    background: var(--mask-dark);
+  }
+
+  &.light {
+    background: var(--mask-light);
+  }
+
+  &.white {
+    background: var(--mask-white);
+  }
+}
+</style>

+ 160 - 0
src/components/Comment.vue

@@ -0,0 +1,160 @@
+<template>
+  <div v-if="modelValue" class="comment-overlay" @click.self="close">
+    <div class="comment-panel">
+      <div class="title">
+        <span>{{ comments.length }}条评论</span>
+        <Icon icon="ic:round-close" @click="close" />
+      </div>
+      <div v-if="comments.length" class="list">
+        <div v-for="item in comments" :key="item.id" class="item">
+          <img :src="item.avatar" class="avatar" alt="" />
+          <div class="body">
+            <div class="name">{{ item.nickname }}</div>
+            <div class="content">{{ item.content }}</div>
+            <div class="footer">
+              <span class="time">{{ formatTime(item.create_time) }}</span>
+              <span class="like" :class="{ loved: item.user_digged }">
+                <Icon icon="icon-park-solid:like" />
+                {{ formatCount(item.digg_count) }}
+              </span>
+            </div>
+          </div>
+        </div>
+      </div>
+      <p v-else class="empty">暂无评论</p>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Icon } from '@iconify/vue'
+import { ref, watch } from 'vue'
+import { formatCount, getVideoComments } from '@/mock/homeData'
+
+const props = defineProps({
+  pageId: { type: String, default: '' },
+  videoId: { type: String, default: '' },
+  modelValue: { type: Boolean, default: false },
+})
+
+const emit = defineEmits<{ 'update:modelValue': [boolean]; close: [] }>()
+
+const comments = ref<
+  Array<{
+    id: string
+    nickname: string
+    avatar: string
+    content: string
+    create_time: number
+    digg_count: number
+    user_digged: boolean
+  }>
+>([])
+
+function loadComments() {
+  const res = getVideoComments(props.videoId || '7267478481213181238')
+  comments.value = res.data
+}
+
+function formatTime(ts: number) {
+  const d = new Date(ts * 1000)
+  return `${d.getMonth() + 1}-${d.getDate()}`
+}
+
+function close() {
+  emit('update:modelValue', false)
+  emit('close')
+}
+
+watch(
+  () => [props.modelValue, props.videoId],
+  ([visible]) => {
+    if (visible) loadComments()
+  },
+)
+</script>
+
+<style scoped lang="less">
+.comment-overlay {
+  position: fixed;
+  inset: 0;
+  z-index: 10;
+  background: var(--mask-dark);
+  display: flex;
+  align-items: flex-end;
+}
+
+.comment-panel {
+  width: 100%;
+  max-height: 70%;
+  background: rgb(28, 30, 43);
+  border-radius: 12rem 12rem 0 0;
+  color: white;
+  display: flex;
+  flex-direction: column;
+
+  .title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-size: 16rem;
+    padding: 16rem;
+    border-bottom: 1px solid rgb(45, 45, 55);
+  }
+
+  .list {
+    overflow-y: auto;
+    padding: 0 16rem 16rem;
+    flex: 1;
+
+    .item {
+      display: flex;
+      gap: 10rem;
+      padding: 12rem 0;
+      border-bottom: 1px solid rgb(40, 40, 50);
+
+      .avatar {
+        width: 36rem;
+        height: 36rem;
+        border-radius: 50%;
+        flex-shrink: 0;
+      }
+
+      .name {
+        font-size: 13rem;
+        opacity: 0.7;
+        margin-bottom: 4rem;
+      }
+
+      .content {
+        font-size: 14rem;
+        line-height: 1.4;
+      }
+
+      .footer {
+        display: flex;
+        gap: 16rem;
+        margin-top: 8rem;
+        font-size: 12rem;
+        opacity: 0.5;
+
+        .like {
+          display: flex;
+          align-items: center;
+          gap: 4rem;
+
+          &.loved {
+            color: #fe2c55;
+          }
+        }
+      }
+    }
+  }
+
+  .empty {
+    padding: 24rem;
+    text-align: center;
+    opacity: 0.5;
+  }
+}
+</style>

+ 184 - 0
src/components/UserPanel.vue

@@ -0,0 +1,184 @@
+<template>
+  <div id="UserPanel" class="user-panel">
+    <div class="float">
+      <Icon icon="eva:arrow-ios-back-fill" class="icon" @click="emit('back')" />
+      <span class="title">{{ author.nickname || '用户主页' }}</span>
+    </div>
+    <div class="main">
+      <div class="profile">
+        <img :src="avatarUrl" class="avatar" alt="" />
+        <div class="stats">
+          <div class="cell">
+            <b>{{ formatCount(author.following_count as number) }}</b>
+            <span>关注</span>
+          </div>
+          <div class="cell">
+            <b>{{ formatCount(author.mplatform_followers_count as number) }}</b>
+            <span>粉丝</span>
+          </div>
+          <div class="cell">
+            <b>{{ formatCount(author.total_favorited as number) }}</b>
+            <span>获赞</span>
+          </div>
+        </div>
+      </div>
+      <p class="signature">{{ author.signature || '暂无简介' }}</p>
+      <p class="uid">抖音号:{{ author.unique_id || author.uid }}</p>
+      <div class="video-list">
+        <p class="section-title">作品 {{ author.aweme_count || 0 }}</p>
+        <div
+          v-for="v in userVideos"
+          :key="v.aweme_id"
+          class="video-card"
+          @click="openComments(v)"
+        >
+          <img :src="v.video?.cover?.url_list?.[0]" alt="" />
+          <span>{{ v.desc }}</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Icon } from '@iconify/vue'
+import { computed } from 'vue'
+import { formatCount, recommendVideos } from '@/mock/homeData'
+import bus, { EVENT_KEY } from '@/utils/bus'
+
+const props = defineProps({
+  currentItem: { type: Object, default: () => ({}) },
+  active: { type: Boolean, default: false },
+})
+
+const emit = defineEmits<{
+  back: []
+  toggleCanMove: [boolean]
+  showFollowSetting: []
+  showFollowSetting2: []
+  'update:currentItem': [unknown]
+}>()
+
+const author = computed(() => (props.currentItem as { author?: Record<string, unknown> }).author || {})
+
+const avatarUrl = computed(
+  () =>
+    (author.value.avatar_168x168 as { url_list?: string[] })?.url_list?.[0] ||
+    'https://p3-pc.douyinpic.com/img/aweme-avatar/tos-cn-avt-0015_99d3a4923c94e1e27b16209743eaec24~c5_168x168.jpeg?from=2956013662',
+)
+
+const userVideos = computed(() => {
+  const uid = author.value.uid
+  return recommendVideos.filter((v) => v.author?.uid === uid).slice(0, 3)
+})
+
+function openComments(v: (typeof recommendVideos)[0]) {
+  bus.emit(EVENT_KEY.CURRENT_ITEM, v)
+  bus.emit(EVENT_KEY.OPEN_COMMENTS)
+}
+
+defineExpose({
+  cancelFollow: () => {},
+})
+</script>
+
+<style scoped lang="less">
+.user-panel {
+  height: 100%;
+  background: rgb(22, 22, 22);
+  color: white;
+  overflow: auto;
+
+  .float {
+    display: flex;
+    align-items: center;
+    gap: 12rem;
+    padding: 12rem 15rem;
+    position: sticky;
+    top: 0;
+    background: rgb(22, 22, 22);
+    z-index: 1;
+
+    .icon {
+      font-size: 28rem;
+      cursor: pointer;
+    }
+
+    .title {
+      font-size: 16rem;
+      font-weight: bold;
+    }
+  }
+
+  .main {
+    padding: 0 15rem 20rem;
+
+    .profile {
+      display: flex;
+      align-items: center;
+      gap: 20rem;
+      margin-bottom: 12rem;
+
+      .avatar {
+        width: 80rem;
+        height: 80rem;
+        border-radius: 50%;
+      }
+
+      .stats {
+        flex: 1;
+        display: flex;
+        justify-content: space-around;
+
+        .cell {
+          text-align: center;
+
+          b {
+            display: block;
+            font-size: 16rem;
+          }
+
+          span {
+            font-size: 12rem;
+            opacity: 0.6;
+          }
+        }
+      }
+    }
+
+    .signature,
+    .uid {
+      font-size: 13rem;
+      opacity: 0.7;
+      margin: 0 0 8rem;
+    }
+
+    .section-title {
+      font-size: 14rem;
+      margin: 16rem 0 10rem;
+    }
+
+    .video-card {
+      display: flex;
+      gap: 10rem;
+      margin-bottom: 10rem;
+      background: rgb(29, 29, 29);
+      border-radius: 8rem;
+      overflow: hidden;
+      cursor: pointer;
+
+      img {
+        width: 80rem;
+        height: 80rem;
+        object-fit: cover;
+      }
+
+      span {
+        padding: 10rem;
+        font-size: 13rem;
+        line-height: 1.4;
+      }
+    }
+  }
+}
+</style>

+ 141 - 0
src/components/slide/SlideHorizontal.vue

@@ -0,0 +1,141 @@
+<script setup lang="ts">
+import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'
+import {
+  getSlideOffset,
+  slideInit,
+  slideReset,
+  slideTouchEnd,
+  slideTouchMove,
+  slideTouchStart,
+} from '@/utils/slide'
+import { SlideType } from '@/utils/const_var'
+import { _css } from '@/utils/dom'
+
+const props = defineProps({
+  index: { type: Number, default: 0 },
+  name: { type: String, default: '' },
+  autoplay: { type: Boolean, default: false },
+  indicator: { type: Boolean, default: false },
+  changeActiveIndexUseAnim: { type: Boolean, default: true },
+})
+
+const emit = defineEmits<{ 'update:index': [number] }>()
+
+let ob: MutationObserver | null = null
+const slideListEl = ref<HTMLElement | null>(null)
+
+const state = reactive({
+  judgeValue: 20,
+  type: SlideType.HORIZONTAL,
+  name: props.name,
+  localIndex: props.index,
+  needCheck: true,
+  next: false,
+  isDown: false,
+  start: { x: 0, y: 0, time: 0 },
+  move: { x: 0, y: 0 },
+  wrapper: { width: 0, height: 0, childrenLength: 0 },
+})
+
+watch(
+  () => props.index,
+  (newVal) => {
+    if (state.localIndex !== newVal) {
+      state.localIndex = newVal
+      if (!slideListEl.value) return
+      if (props.changeActiveIndexUseAnim) {
+        _css(slideListEl.value, 'transition-duration', '300ms')
+      }
+      _css(
+        slideListEl.value,
+        'transform',
+        `translate3d(${getSlideOffset(state, slideListEl.value)}px, 0, 0)`,
+      )
+    }
+  },
+)
+
+onMounted(() => {
+  if (!slideListEl.value) return
+  slideInit(slideListEl.value, state)
+  if (props.autoplay) {
+    setInterval(() => {
+      if (state.localIndex === state.wrapper.childrenLength - 1) {
+        emit('update:index', 0)
+      } else {
+        emit('update:index', state.localIndex + 1)
+      }
+    }, 3000)
+  }
+  ob = new MutationObserver(() => {
+    if (slideListEl.value) state.wrapper.childrenLength = slideListEl.value.children.length
+  })
+  ob.observe(slideListEl.value, { childList: true })
+})
+
+onUnmounted(() => {
+  ob?.disconnect()
+})
+
+function touchStart(e: PointerEvent) {
+  if (slideListEl.value) slideTouchStart(e, slideListEl.value, state)
+}
+
+function touchMove(e: PointerEvent) {
+  if (slideListEl.value) slideTouchMove(e, slideListEl.value, state)
+}
+
+function touchEnd(e: PointerEvent) {
+  slideTouchEnd(e, state)
+  if (slideListEl.value) slideReset(e, slideListEl.value, state, emit)
+}
+</script>
+
+<template>
+  <div class="slide horizontal">
+    <div
+      v-if="indicator && state.wrapper.childrenLength"
+      class="indicator-bullets"
+    >
+      <div
+        v-for="item in state.wrapper.childrenLength"
+        :key="item"
+        class="bullet"
+        :class="{ active: state.localIndex === item - 1 }"
+      />
+    </div>
+    <div
+      ref="slideListEl"
+      class="slide-list"
+      @pointerdown.prevent="touchStart"
+      @pointermove.prevent="touchMove"
+      @pointerup.prevent="touchEnd"
+    >
+      <slot />
+    </div>
+  </div>
+</template>
+
+<style scoped lang="less">
+.indicator-bullets {
+  position: absolute;
+  bottom: 10rem;
+  z-index: 2;
+  left: 50%;
+  transform: translateX(-50%);
+  display: flex;
+  justify-content: center;
+  gap: 7rem;
+
+  .bullet {
+    width: 5rem;
+    height: 5rem;
+    border-radius: 50%;
+    background: var(--second-btn-color);
+
+    &.active {
+      background: white;
+    }
+  }
+}
+</style>

+ 14 - 0
src/components/slide/SlideItem.vue

@@ -0,0 +1,14 @@
+<template>
+  <div class="slide-item">
+    <slot />
+  </div>
+</template>
+
+<style lang="less">
+.slide-item {
+  height: 100%;
+  width: 100%;
+  flex-shrink: 0;
+  position: relative;
+}
+</style>

+ 84 - 0
src/components/slide/SlideVertical.vue

@@ -0,0 +1,84 @@
+<script setup lang="ts">
+import { onMounted, reactive, ref, watch } from 'vue'
+import {
+  getSlideOffset,
+  slideInit,
+  slideReset,
+  slideTouchEnd,
+  slideTouchMove,
+  slideTouchStart,
+} from '@/utils/slide'
+import { SlideType } from '@/utils/const_var'
+import { _css } from '@/utils/dom'
+
+const props = defineProps({
+  index: { type: Number, default: 0 },
+  changeActiveIndexUseAnim: { type: Boolean, default: true },
+  name: { type: String, default: 'SlideVertical' },
+})
+
+const emit = defineEmits<{ 'update:index': [number] }>()
+
+const slideListEl = ref<HTMLElement | null>(null)
+
+const state = reactive({
+  judgeValue: 20,
+  type: SlideType.VERTICAL,
+  name: props.name,
+  localIndex: props.index,
+  needCheck: true,
+  next: false,
+  isDown: false,
+  start: { x: 0, y: 0, time: 0 },
+  move: { x: 0, y: 0 },
+  wrapper: { width: 0, height: 0, childrenLength: 0 },
+})
+
+watch(
+  () => props.index,
+  (newVal) => {
+    if (state.localIndex !== newVal && slideListEl.value) {
+      state.localIndex = newVal
+      if (props.changeActiveIndexUseAnim) {
+        _css(slideListEl.value, 'transition-duration', '300ms')
+      }
+      _css(
+        slideListEl.value,
+        'transform',
+        `translate3d(0, ${getSlideOffset(state, slideListEl.value)}px, 0)`,
+      )
+    }
+  },
+)
+
+onMounted(() => {
+  if (slideListEl.value) slideInit(slideListEl.value, state)
+})
+
+function touchStart(e: PointerEvent) {
+  if (slideListEl.value) slideTouchStart(e, slideListEl.value, state)
+}
+
+function touchMove(e: PointerEvent) {
+  if (slideListEl.value) slideTouchMove(e, slideListEl.value, state)
+}
+
+function touchEnd(e: PointerEvent) {
+  slideTouchEnd(e, state)
+  if (slideListEl.value) slideReset(e, slideListEl.value, state, emit)
+}
+</script>
+
+<template>
+  <div class="slide vertical">
+    <div
+      ref="slideListEl"
+      class="slide-list flex-direction-column"
+      @pointerdown.prevent="touchStart"
+      @pointermove.prevent="touchMove"
+      @pointerup.prevent="touchEnd"
+    >
+      <slot />
+    </div>
+  </div>
+</template>

+ 252 - 0
src/components/video/BaseVideo.vue

@@ -0,0 +1,252 @@
+<template>
+  <div class="video-wrapper" @click="onClick">
+    <video
+      ref="videoEl"
+      class="video"
+      :src="currentSrc"
+      :poster="poster"
+      :muted="isMuted"
+      loop
+      playsinline
+      webkit-playsinline
+      x5-playsinline
+      x5-video-player-type="h5-page"
+      preload="auto"
+      @error="onVideoError"
+      @loadeddata="onLoaded"
+    />
+    <Icon v-if="!isPlaying && !loadFailed" icon="fluent:play-28-filled" class="pause-icon" />
+    <p v-if="loadFailed" class="error-tip">视频加载失败,请检查网络</p>
+    <div class="float">
+      <div class="normal">
+        <ItemToolbar :item="localItem" @update:item="onItemUpdate" />
+        <ItemDesc />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Icon } from '@iconify/vue'
+import { computed, onMounted, onUnmounted, provide, reactive, ref, watch } from 'vue'
+import bus, { EVENT_KEY } from '@/utils/bus'
+import { SlideItemPlayStatus } from '@/utils/const_var'
+import { _checkImgUrl } from '@/utils/index'
+import ItemToolbar from './ItemToolbar.vue'
+import ItemDesc from './ItemDesc.vue'
+
+const props = defineProps({
+  item: { type: Object, required: true },
+  index: { type: Number, default: 0 },
+  position: {
+    type: Object,
+    default: () => ({ uniqueId: 'home', index: 0 }),
+  },
+  isPlay: { type: Boolean, default: false },
+})
+
+const emit = defineEmits<{ 'update:item': [unknown] }>()
+
+const videoEl = ref<HTMLVideoElement | null>(null)
+const isMuted = ref(
+  typeof window !== 'undefined' ? (window as Window).isMuted !== false : true,
+)
+const urlIndex = ref(0)
+const loadFailed = ref(false)
+
+const localItem = reactive({ ...(props.item as object) })
+
+const state = reactive({
+  status: props.isPlay ? SlideItemPlayStatus.Play : SlideItemPlayStatus.Pause,
+})
+
+const playUrls = computed(() => {
+  const video = (localItem as { video?: { play_addr?: { url_list?: string[] } } }).video
+  return video?.play_addr?.url_list?.filter(Boolean) || []
+})
+
+const currentSrc = computed(() => playUrls.value[urlIndex.value] || '')
+
+const poster = computed(() => {
+  const video = (localItem as { video?: { cover?: { url_list?: string[] }; poster?: string } }).video
+  const raw = video?.poster || video?.cover?.url_list?.[0]
+  return _checkImgUrl(raw)
+})
+
+const isPlaying = computed(() => state.status === SlideItemPlayStatus.Play)
+
+provide('item', localItem)
+provide('position', computed(() => props.position))
+provide('isPlaying', isPlaying)
+provide('isMuted', isMuted)
+
+function onItemUpdate(val: unknown) {
+  Object.assign(localItem, val as object)
+  emit('update:item', val)
+}
+
+watch(
+  () => props.item,
+  (v) => {
+    Object.assign(localItem, v as object)
+    urlIndex.value = 0
+    loadFailed.value = false
+  },
+  { deep: true },
+)
+
+function onVideoError() {
+  if (urlIndex.value < playUrls.value.length - 1) {
+    urlIndex.value += 1
+    loadFailed.value = false
+    const el = videoEl.value
+    if (el) {
+      el.load()
+      if (props.isPlay) play()
+    }
+    return
+  }
+  loadFailed.value = true
+  state.status = SlideItemPlayStatus.Pause
+}
+
+function onLoaded() {
+  loadFailed.value = false
+}
+
+function unmute() {
+  isMuted.value = false
+  ;(window as Window).isMuted = false
+  const el = videoEl.value
+  if (el) {
+    el.muted = false
+    el.volume = 1
+  }
+}
+
+function play() {
+  const el = videoEl.value
+  if (!el || !currentSrc.value || loadFailed.value) return
+  state.status = SlideItemPlayStatus.Play
+  if (!isMuted.value) {
+    el.muted = false
+    el.volume = 1
+  }
+  el.play().catch(() => {
+    state.status = SlideItemPlayStatus.Pause
+  })
+}
+
+function pause() {
+  const el = videoEl.value
+  if (!el) return
+  state.status = SlideItemPlayStatus.Pause
+  el.pause()
+}
+
+function onClick() {
+  unmute()
+  bus.emit(EVENT_KEY.SINGLE_CLICK_BROADCAST, {
+    uniqueId: props.position.uniqueId,
+    index: props.position.index,
+    type: EVENT_KEY.ITEM_TOGGLE,
+  })
+}
+
+function onBusClick(val?: unknown) {
+  const payload = val as { uniqueId?: string; index?: number; type?: string } | undefined
+  if (!payload) return
+  const { uniqueId, index, type } = payload
+  if (props.position.uniqueId !== uniqueId || props.position.index !== index) return
+
+  if (type === EVENT_KEY.ITEM_TOGGLE) {
+    if (state.status === SlideItemPlayStatus.Play) pause()
+    else play()
+  }
+  if (type === EVENT_KEY.ITEM_STOP) {
+    const el = videoEl.value
+    if (el) el.currentTime = 0
+    pause()
+  }
+  if (type === EVENT_KEY.ITEM_PLAY) {
+    const el = videoEl.value
+    if (el) el.currentTime = 0
+    play()
+  }
+}
+
+watch(
+  () => props.isPlay,
+  (v) => {
+    if (v) play()
+    else pause()
+  },
+)
+
+function onRemoveMuted() {
+  unmute()
+}
+
+onMounted(() => {
+  bus.on(EVENT_KEY.SINGLE_CLICK_BROADCAST, onBusClick)
+  bus.on(EVENT_KEY.REMOVE_MUTED, onRemoveMuted)
+  if ((window as Window).isMuted === false) unmute()
+  if (props.isPlay) play()
+})
+
+onUnmounted(() => {
+  bus.off(EVENT_KEY.SINGLE_CLICK_BROADCAST, onBusClick)
+  bus.off(EVENT_KEY.REMOVE_MUTED, onRemoveMuted)
+  pause()
+})
+</script>
+
+<style scoped lang="less">
+.video-wrapper {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  background: #000;
+  overflow: hidden;
+
+  .video {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+  }
+
+  .pause-icon {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    font-size: 60rem;
+    color: rgba(255, 255, 255, 0.8);
+    pointer-events: none;
+    z-index: 1;
+  }
+
+  .error-tip {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    color: rgba(255, 255, 255, 0.7);
+    font-size: 14rem;
+    z-index: 1;
+  }
+
+  .float {
+    position: absolute;
+    inset: 0;
+    z-index: 2;
+    pointer-events: none;
+
+    .normal {
+      position: absolute;
+      inset: 0;
+      pointer-events: none;
+    }
+  }
+}
+</style>

+ 101 - 0
src/components/video/ItemDesc.vue

@@ -0,0 +1,101 @@
+<script setup lang="ts">
+import { Icon } from '@iconify/vue'
+import { inject } from 'vue'
+import bus, { EVENT_KEY } from '@/utils/bus'
+
+defineProps({
+  isMy: { type: Boolean, default: false },
+  isLive: { type: Boolean, default: false },
+})
+
+const item = inject<{
+  desc?: string
+  city?: string
+  address?: string
+  author?: { nickname?: string }
+}>('item')
+
+function goUser() {
+  bus.emit(EVENT_KEY.GO_USERINFO)
+}
+</script>
+
+<template>
+  <div v-if="item && !isMy" class="item-desc" @click.stop>
+    <div v-if="item.city || item.address" class="location-wrapper">
+      <div class="location">
+        <Icon icon="mdi:map-marker" class="loc-icon" />
+        <span>{{ item.city }}</span>
+        <template v-if="item.address">
+          <div class="gang" />
+          <span>{{ item.address }}</span>
+        </template>
+      </div>
+    </div>
+    <div v-if="isLive" class="live">直播中</div>
+    <div class="name" @click.stop="goUser">@{{ item.author?.nickname }}</div>
+    <div class="description">{{ item.desc }}</div>
+  </div>
+</template>
+
+<style scoped lang="less">
+.item-desc {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  width: 70%;
+  padding: 0 0 20rem 15rem;
+  box-sizing: border-box;
+  pointer-events: auto;
+  z-index: 4;
+  text-align: left;
+  color: #fff;
+
+  .location-wrapper {
+    margin-bottom: 10rem;
+
+    .location {
+      display: inline-flex;
+      align-items: center;
+      font-size: 12rem;
+      padding: 4rem 8rem;
+      border-radius: 3rem;
+      background: rgba(58, 58, 70, 0.4);
+
+      .loc-icon {
+        font-size: 14rem;
+        margin-right: 6rem;
+      }
+
+      .gang {
+        height: 8rem;
+        width: 1.5px;
+        margin: 0 5rem;
+        background: gray;
+      }
+    }
+  }
+
+  .live {
+    display: inline-block;
+    margin-bottom: 10rem;
+    padding: 3rem 6rem;
+    font-size: 11rem;
+    border-radius: 3rem;
+    background: #fe2c55;
+  }
+
+  .name {
+    font-size: 18rem;
+    font-weight: bold;
+    margin-bottom: 8rem;
+    cursor: pointer;
+  }
+
+  .description {
+    font-size: 14rem;
+    line-height: 1.4;
+    opacity: 0.95;
+  }
+}
+</style>

+ 216 - 0
src/components/video/ItemToolbar.vue

@@ -0,0 +1,216 @@
+<script setup lang="ts">
+import { Icon } from '@iconify/vue'
+import { inject } from 'vue'
+import bus, { EVENT_KEY } from '@/utils/bus'
+import { _checkImgUrl, _formatNumber } from '@/utils/index'
+const props = defineProps({
+  item: { type: Object, required: true },
+  isMy: { type: Boolean, default: false },
+})
+
+const emit = defineEmits<{ 'update:item': [unknown] }>()
+
+const position = inject<{ uniqueId: string; index: number }>('position')
+
+type VideoItem = {
+  author?: { avatar_168x168?: { url_list?: string[] } }
+  isAttention?: boolean
+  isLoved?: boolean
+  isCollect?: boolean
+  statistics: {
+    digg_count: number
+    comment_count: number
+    collect_count: number
+    share_count: number
+  }
+}
+
+function patch(partial: Partial<VideoItem>) {
+  const next = { ...(props.item as VideoItem), ...partial }
+  emit('update:item', next)
+  if (position) {
+    bus.emit(EVENT_KEY.UPDATE_ITEM, { position, item: next })
+  }
+}
+
+function loved() {
+  const item = props.item as VideoItem
+  const nextLoved = !item.isLoved
+  patch({
+    isLoved: nextLoved,
+    statistics: {
+      ...item.statistics,
+      digg_count: item.statistics.digg_count + (nextLoved ? 1 : -1),
+    },
+  })
+}
+
+function collected() {
+  const item = props.item as VideoItem
+  const next = !item.isCollect
+  patch({
+    isCollect: next,
+    statistics: {
+      ...item.statistics,
+      collect_count: item.statistics.collect_count + (next ? 1 : -1),
+    },
+  })
+}
+
+function attention(e: Event) {
+  const el = e.currentTarget as HTMLElement
+  el.classList.add('attention')
+  setTimeout(() => patch({ isAttention: true }), 1000)
+}
+
+function showComments() {
+  bus.emit(EVENT_KEY.OPEN_COMMENTS)
+}
+
+function goUser() {
+  bus.emit(EVENT_KEY.GO_USERINFO)
+}
+
+function showShare() {
+  bus.emit(EVENT_KEY.SHOW_SHARE)
+}
+
+const item = props.item as VideoItem
+</script>
+
+<template>
+  <div class="toolbar" @click.stop>
+    <div class="avatar-ctn">
+      <img
+        class="avatar"
+        :src="_checkImgUrl(item.author?.avatar_168x168?.url_list?.[0])"
+        alt=""
+        @click.stop="goUser"
+      />
+      <transition name="fade">
+        <div v-if="!item.isAttention" class="options" @click.stop="attention">
+          <img class="no" src="@/assets/img/icon/add-light.png" alt="" />
+          <img class="yes" src="@/assets/img/icon/ok-red.png" alt="" />
+        </div>
+      </transition>
+    </div>
+    <div class="love" @click.stop="loved">
+      <img v-if="!item.isLoved" src="@/assets/img/icon/love.svg" class="action-icon" alt="" />
+      <img v-else src="@/assets/img/icon/loved.svg" class="action-icon" alt="" />
+      <span>{{ _formatNumber(item.statistics.digg_count) }}</span>
+    </div>
+    <div class="message" @click.stop="showComments">
+      <Icon icon="mage:message-dots-round-fill" class="icon" />
+      <span>{{ _formatNumber(item.statistics.comment_count) }}</span>
+    </div>
+    <div class="message" @click.stop="collected">
+      <Icon
+        icon="ic:round-star"
+        class="icon"
+        :style="{ color: item.isCollect ? 'rgb(252, 179, 3)' : 'white' }"
+      />
+      <span>{{ _formatNumber(item.statistics.collect_count) }}</span>
+    </div>
+    <div v-if="!isMy" class="share" @click.stop="showShare">
+      <img src="@/assets/img/icon/share-white-full.png" class="action-icon" alt="" />
+      <span>{{ _formatNumber(item.statistics.share_count) }}</span>
+    </div>
+    <div v-else class="share" @click.stop="showShare">
+      <Icon icon="ri:more-line" class="icon" />
+    </div>
+  </div>
+</template>
+
+<style scoped lang="less">
+.toolbar {
+  position: absolute;
+  bottom: 0;
+  right: 10rem;
+  color: #fff;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  pointer-events: auto;
+  z-index: 4;
+
+  .avatar-ctn {
+    position: relative;
+    margin-bottom: 20rem;
+
+    .avatar {
+      width: 45rem;
+      height: 45rem;
+      border: 3rem solid white;
+      border-radius: 50%;
+      cursor: pointer;
+      object-fit: cover;
+    }
+
+    .options {
+      position: absolute;
+      left: 0;
+      right: 0;
+      bottom: -5rem;
+      margin: auto;
+      background: #fe2c55;
+      width: 18rem;
+      height: 18rem;
+      border-radius: 50%;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      cursor: pointer;
+
+      img {
+        position: absolute;
+        width: 14rem;
+        height: 14rem;
+        transition: all 0.3s;
+      }
+
+      .yes {
+        opacity: 0;
+        transform: rotate(-180deg);
+      }
+
+      &.attention {
+        background: white;
+
+        .no {
+          opacity: 0;
+          transform: rotate(180deg);
+        }
+
+        .yes {
+          opacity: 1;
+          transform: rotate(0deg);
+        }
+      }
+    }
+  }
+
+  .love,
+  .message,
+  .share {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    margin-bottom: 20rem;
+    cursor: pointer;
+
+    .action-icon {
+      width: 35rem;
+      height: 35rem;
+    }
+
+    .icon {
+      font-size: 40rem;
+    }
+
+    span {
+      font-size: 12rem;
+      margin-top: 4rem;
+    }
+  }
+}
+</style>

+ 11 - 2
src/env.d.ts

@@ -17,8 +17,17 @@ interface ImportMeta {
   readonly env: ImportMetaEnv
 }
 
-interface Window {
-  VE_API?: Record<string, Record<string, unknown>>
+declare global {
+  interface Window {
+    VE_API?: Record<string, Record<string, unknown>>
+    isMoved?: boolean
+    isMuted?: boolean
+  }
+}
+
+declare module '*.json' {
+  const value: Record<string, unknown>[]
+  export default value
 }
 
 declare module '*.vue' {

+ 24 - 1
src/main.ts

@@ -1,11 +1,23 @@
 import { createApp } from 'vue'
 import 'normalize.css/normalize.css'
 import 'nprogress/nprogress.css'
-import './style.css'
+import './assets/less/index.less'
 import App from './App.vue'
 import router from './router/index.js'
 import store from './store/index.js'
 import axios from './plugins/axios.js'
+import bus, { EVENT_KEY } from './utils/bus'
+
+;(window as Window).isMoved = false
+;(window as Window).isMuted = true
+
+function resetVhAndPx() {
+  const vh = window.innerHeight * 0.01
+  document.documentElement.style.setProperty('--vh', `${vh}px`)
+}
+
+resetVhAndPx()
+window.addEventListener('resize', resetVhAndPx)
 
 const app = createApp(App)
 
@@ -14,3 +26,14 @@ app.use(router)
 app.use(axios as import('vue').Plugin, { router, store, opt: 'VE_API' })
 
 app.mount('#app')
+
+// 与 douyin-vue 一致:先静音自动播放,2 秒后取消静音
+setTimeout(() => {
+  ;(window as Window).isMuted = false
+  bus.emit(EVENT_KEY.REMOVE_MUTED)
+  bus.emit('HIDE_MUTED_NOTICE')
+}, 2000)
+
+bus.on(EVENT_KEY.REMOVE_MUTED, () => {
+  ;(window as Window).isMuted = false
+})

+ 124 - 0
src/mock/homeData.ts

@@ -0,0 +1,124 @@
+/** 结构来自 posts6.json;播放地址改成本地/稳定 CDN(抖音 CDN 会 ERR_CONNECTION_RESET) */
+import posts6 from './posts6.json'
+
+export type RecommendVideo = (typeof posts6)[number] & { type: string }
+
+/** 本地演示视频,避免 douyin 跳转 CDN 连接被重置 */
+const LOCAL_MP4 = '/video/demo.mp4'
+
+const REMOTE_MP4 = [
+  'https://www.w3schools.com/html/mov_bbb.mp4',
+  'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
+]
+
+export const recommendVideos = posts6.map((v, i) => ({
+  ...v,
+  type: 'recommend-video',
+  isLoved: false,
+  isCollect: false,
+  isAttention: false,
+  video: {
+    ...v.video,
+    play_addr: {
+      ...v.video?.play_addr,
+      url_list: [LOCAL_MP4, REMOTE_MP4[i % REMOTE_MP4.length]],
+    },
+  },
+})) as RecommendVideo[]
+
+const AVATAR =
+  'https://p3-pc.douyinpic.com/img/aweme-avatar/tos-cn-avt-0015_99d3a4923c94e1e27b16209743eaec24~c5_168x168.jpeg?from=2956013662'
+
+export const miniPrograms = [
+  {
+    id: 'mp1',
+    name: '飞鸟认养',
+    icon: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/pipieh7nupabozups/toutiao_web_pc/tt-icon.png',
+  },
+  {
+    id: 'mp2',
+    name: '果园直播',
+    icon: 'https://gd-hbimg.huaban.com/65130a3e6a139530bb03bd118e21a2603af7df4e1303b-OOzcBu_fw658webp',
+  },
+]
+
+export const recentUsers = recommendVideos.slice(0, 6).map((v) => ({
+  uid: String(v.author?.uid || v.author_user_id),
+  nickname: v.author?.nickname || '用户',
+  avatar: v.author?.avatar_168x168?.url_list?.[0] || AVATAR,
+}))
+
+export const longVideos = recommendVideos.slice(0, 2).map((v, i) => ({
+  id: `lv${i}`,
+  title: v.desc?.slice(0, 20) || '长视频',
+  cover: v.video?.cover?.url_list?.[0] || '',
+  duration: '10:00',
+  play_count: v.statistics?.play_count || v.statistics?.digg_count || 0,
+  author: v.author?.nickname || '',
+}))
+
+export const communityPosts = recommendVideos.slice(0, 2).map((v, i) => ({
+  id: `cp${i}`,
+  title: v.desc || '经验分享',
+  cover: v.video?.cover?.url_list?.[0] || '',
+  author: v.author?.nickname || '',
+  digg_count: v.statistics?.digg_count || 0,
+}))
+
+export const hotFeed = recommendVideos.slice(0, 2)
+export const followFeed = recommendVideos.slice(2, 3)
+
+export const videoComments: Record<
+  string,
+  Array<{
+    id: string
+    nickname: string
+    avatar: string
+    content: string
+    create_time: number
+    digg_count: number
+    user_digged: boolean
+  }>
+> = {
+  [recommendVideos[0]?.aweme_id]: [
+    {
+      id: 'c1',
+      nickname: '小果园',
+      avatar: AVATAR,
+      content: '认养链接在哪?',
+      create_time: 1692092000,
+      digg_count: 1280,
+      user_digged: false,
+    },
+    {
+      id: 'c2',
+      nickname: '旅行日记',
+      avatar: AVATAR,
+      content: '画面太美了',
+      create_time: 1692092500,
+      digg_count: 5600,
+      user_digged: true,
+    },
+  ],
+}
+
+export function getRecommendedVideos(pageNo = 0, pageSize = 10) {
+  const start = pageNo * pageSize
+  const list = recommendVideos.slice(start, start + pageSize)
+  return {
+    code: 200,
+    msg: '',
+    data: { total: recommendVideos.length, list },
+  }
+}
+
+export function getVideoComments(videoId: string) {
+  const list = videoComments[videoId] || videoComments[recommendVideos[0]?.aweme_id] || []
+  return { code: 200, data: list }
+}
+
+export function formatCount(n: number) {
+  if (n >= 10000) return (n / 10000).toFixed(1) + 'w'
+  if (n >= 1000) return (n / 1000).toFixed(1) + 'k'
+  return String(n)
+}

+ 1722 - 0
src/mock/posts6.json

@@ -0,0 +1,1722 @@
+[
+  {
+    "aweme_id": "7267478481213181238",
+    "desc": "把东方美学带到欧洲 #卢浮宫\n#马面裙",
+    "create_time": 1692091704,
+    "music": {
+      "id": 7267478542388760000,
+      "title": "@彭十六elf创作的原声",
+      "author": "彭十六elf",
+      "cover_medium": {
+        "uri": "720x720/aweme-avatar/tos-cn-avt-0015_99d3a4923c94e1e27b16209743eaec24",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/aweme/720x720/aweme-avatar/tos-cn-avt-0015_99d3a4923c94e1e27b16209743eaec24.jpeg?from=116350172"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "cover_thumb": {
+        "uri": "100x100/aweme-avatar/tos-cn-avt-0015_99d3a4923c94e1e27b16209743eaec24",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/aweme/100x100/aweme-avatar/tos-cn-avt-0015_99d3a4923c94e1e27b16209743eaec24.jpeg?from=116350172"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "play_url": {
+        "uri": "https://sf5-hl-cdn-tos.douyinstatic.com/obj/ies-music/7267478540864572197.mp3",
+        "url_list": [
+          "https://sf5-hl-cdn-tos.douyinstatic.com/obj/ies-music/7267478540864572197.mp3",
+          "https://sf3-cdn-tos.douyinstatic.com/obj/ies-music/7267478540864572197.mp3"
+        ],
+        "width": 720,
+        "height": 720,
+        "url_key": "7267478542388759356"
+      },
+      "duration": 25,
+      "user_count": 0,
+      "owner_id": "24058267831",
+      "owner_nickname": "彭十六elf",
+      "is_original": false
+    },
+    "video": {
+      "play_addr": {
+        "uri": "v0200fg10000cjdk9q3c77ud1oonqoqg",
+        "url_list": [
+          "https://www.douyin.com/aweme/v1/play/?video_id=v0200fg10000cjdk9q3c77ud1oonqoqg&line=0&file_id=b636323c468d42df8b21695bb124d9ee&sign=5786da50100031a108d24a9ef596de33&is_play_url=1&source=PackSourceEnum_PUBLISH"
+        ],
+        "width": 1080,
+        "height": 1920,
+        "url_key": "v0200fg10000cjdk9q3c77ud1oonqoqg_h264_1080p_5777261",
+        "data_size": 18662720,
+        "file_hash": "5786da50100031a108d24a9ef596de33",
+        "file_cs": "c:0-43596-871e|d:0-9331359-b106,9331360-18662719-d5cf|a:v0200fg10000cjdk9q3c77ud1oonqoqg"
+      },
+      "cover": {
+        "uri": "tos-cn-i-0813c001/oUd3UMAAIDgHe8AA0ikA2nmC2Q2tCrAbgAkeG9",
+        "url_list": [
+          "W6X9755J4xsbUal6MEmks.png"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "poster": "out3.jpg",
+      "height": 3840,
+      "width": 2160,
+      "ratio": "1080p",
+      "use_static_cover": true,
+      "duration": 25843
+    },
+    "share_url": "https://www.iesdouyin.com/share/video/7267478481213181238/?region=CN&mid=7267478542388759356&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&titleType=title&share_sign=1hw1UVQCy.UIw6olAXrtBoFKouFOtjTXTHQ4pY02UPg-&share_version=170400&ts=1710491939&from_aid=6383&from_ssr=1",
+    "statistics": {
+      "admire_count": 679,
+      "comment_count": 136284,
+      "digg_count": 6400077,
+      "collect_count": 245585,
+      "play_count": 0,
+      "share_count": 446468
+    },
+    "status": {
+      "listen_video_status": 0,
+      "is_delete": false,
+      "allow_share": true,
+      "is_prohibited": false,
+      "in_reviewing": false,
+      "part_see": 0,
+      "private_status": 0,
+      "review_result": {
+        "review_status": 0
+      }
+    },
+    "text_extra": [
+      {
+        "start": 10,
+        "end": 14,
+        "type": 1,
+        "hashtag_name": "卢浮宫",
+        "hashtag_id": "1583784381403150",
+        "is_commerce": false,
+        "caption_start": 0,
+        "caption_end": 0
+      },
+      {
+        "start": 15,
+        "end": 19,
+        "type": 1,
+        "hashtag_name": "马面裙",
+        "hashtag_id": "1618019281021965",
+        "is_commerce": false,
+        "caption_start": 0,
+        "caption_end": 0
+      }
+    ],
+    "is_top": 1,
+    "share_info": {
+      "share_url": "https://www.iesdouyin.com/share/video/7267478481213181238/?region=CN&mid=7267478542388759356&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&titleType=title&share_sign=1hw1UVQCy.UIw6olAXrtBoFKouFOtjTXTHQ4pY02UPg-&share_version=170400&ts=1710491939&from_aid=6383&from_ssr=1",
+      "share_link_desc": "7.15 q@r.eo 03/29 sEh:/ 把东方美学带到欧洲 # 卢浮宫 # 马面裙  %s 复制此链接,打开Dou音搜索,直接观看视频!"
+    },
+    "duration": 25843,
+    "image_infos": null,
+    "risk_infos": {
+      "vote": false,
+      "warn": false,
+      "risk_sink": false,
+      "type": 0,
+      "content": ""
+    },
+    "position": null,
+    "author_user_id": 24058267831,
+    "author": {
+      "avatar_168x168": {
+        "height": 720,
+        "uri": "aweme-avatar/tos-cn-avt-0015_99d3a4923c94e1e27b16209743eaec24",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/img/aweme-avatar/tos-cn-avt-0015_99d3a4923c94e1e27b16209743eaec24~c5_168x168.jpeg?from=2956013662"
+        ],
+        "width": 720
+      },
+      "avatar_300x300": {
+        "height": 720,
+        "uri": "aweme-avatar/tos-cn-avt-0015_99d3a4923c94e1e27b16209743eaec24",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/img/aweme-avatar/tos-cn-avt-0015_99d3a4923c94e1e27b16209743eaec24~c5_300x300.jpeg?from=2956013662"
+        ],
+        "width": 720
+      },
+      "aweme_count": 359,
+      "birthday_hide_level": 1,
+      "can_show_group_card": 1,
+      "card_entries": [
+        {
+          "goto_url": "aweme://im/FansGroup/GuestState",
+          "icon_dark": {
+            "uri": "https://p3.douyinpic.com/obj/im-resource/old_fans_group_manage_dark.png",
+            "url_list": [
+              "https://p3.douyinpic.com/obj/im-resource/old_fans_group_manage_dark.png",
+              "https://p6.douyinpic.com/obj/im-resource/old_fans_group_manage_dark.png",
+              "https://p9.douyinpic.com/obj/im-resource/old_fans_group_manage_dark.png"
+            ]
+          },
+          "icon_light": {
+            "uri": "https://p3.douyinpic.com/obj/im-resource/old_fans_group_manage_light.png",
+            "url_list": [
+              "https://p3.douyinpic.com/obj/im-resource/old_fans_group_manage_light.png",
+              "https://p6.douyinpic.com/obj/im-resource/old_fans_group_manage_light.png",
+              "https://p9.douyinpic.com/obj/im-resource/old_fans_group_manage_light.png"
+            ]
+          },
+          "sub_title": "2个群聊",
+          "title": "粉丝群",
+          "type": 2
+        }
+      ],
+      "city": "",
+      "commerce_info": {
+        "challenge_list": null,
+        "head_image_list": null,
+        "offline_info_list": [],
+        "smart_phone_list": null,
+        "task_list": null
+      },
+      "commerce_user_info": {
+        "ad_revenue_rits": null,
+        "has_ads_entry": true,
+        "show_star_atlas_cooperation": true,
+        "star_atlas": 1
+      },
+      "commerce_user_level": 0,
+      "country": "",
+      "cover_colour": "#03373EE5",
+      "cover_url": [
+        {
+          "uri": "douyin-user-image-file/d46c49d4b05053c65595ecbe61c6891b",
+          "url_list": [
+            "bR6bvJkjP1rb9VgPazc2s.png"
+          ]
+        },
+        {
+          "uri": "c8510002be9a3a61aad2",
+          "url_list": [
+            "p1grunB9W_XiTEc7PICV1.png"
+          ]
+        }
+      ],
+      "district": "",
+      "favoriting_count": 2311,
+      "follow_status": 0,
+      "follower_count": 32588958,
+      "follower_request_status": 0,
+      "follower_status": 0,
+      "following_count": 176,
+      "forward_count": 5,
+      "gender": 0,
+      "ip_location": "IP属地:上海",
+      "max_follower_count": 34420921,
+      "mplatform_followers_count": 32588958,
+      "nickname": "彭十六elf",
+      "province": "",
+      "public_collects_count": 0,
+      "share_info": {
+        "bool_persist": 1,
+        "share_desc": "长按复制此条消息,打开抖音搜索,查看TA的更多作品。",
+        "share_image_url": {
+          "uri": "tos-cn-p-0015/owsfNETCAfOArLdvZBoycrIjBJDY0ufGeAfDHV",
+          "url_list": [
+            "MubOElriJZkNJ-3dFNjhQ.png"
+          ]
+        },
+        "share_qrcode_url": {
+          "uri": "c15000ddb20b5723744",
+          "url_list": [
+            "https://p6.douyinpic.com/obj/c15000ddb20b5723744",
+            "https://p26.douyinpic.com/obj/c15000ddb20b5723744",
+            "https://p3.douyinpic.com/obj/c15000ddb20b5723744"
+          ]
+        },
+        "share_title": "快来加入抖音,让你发现最有趣的我!",
+        "share_url": "www.iesdouyin.com/share/user/MS4wLjABAAAAAAKy2_R6k-oFWT5E-97gbGZQ1laaweQMWImJDkDaef0?sec_uid=MS4wLjABAAAAAAKy2_R6k-oFWT5E-97gbGZQ1laaweQMWImJDkDaef0&from_ssr=1&from_aid=6383&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1",
+        "share_weibo_desc": "长按复制此条消息,打开抖音搜索,查看TA的更多作品。"
+      },
+      "short_id": "0",
+      "signature": "记录成长\n做有意义的事情🐰☀️\n分享日常:@六猪变美日记 \n🍠:彭十六elf \n商务v:MUXUAN16e(招摄影&剪辑)\n演出v:ME13919\n何其荣幸 何德何能💜",
+      "total_favorited": 1012905132,
+      "uid": "24058267831",
+      "unique_id": "elfin16",
+      "user_age": -1,
+      "white_cover_url": [
+        {
+          "uri": "douyin-user-image-file/d46c49d4b05053c65595ecbe61c6891b",
+          "url_list": [
+            "IO7207wEo0bNpJ9XW3dSI.png"
+          ]
+        },
+        {
+          "uri": "318f1000413827e122102",
+          "url_list": [
+            "owYg1mq7cB3X43GqIlD8g.png"
+          ]
+        }
+      ]
+    },
+    "prevent_download": false,
+    "long_video": null,
+    "aweme_control": {
+      "can_forward": true,
+      "can_share": true,
+      "can_comment": true,
+      "can_show_comment": true
+    },
+    "images": null,
+    "suggest_words": {
+      "suggest_words": [
+        {
+          "words": [
+            {
+              "word": "东方美学惊动外国人",
+              "word_id": "7113485773504402688",
+              "info": "{\"qrec_for_search\":\"{}\"}"
+            }
+          ],
+          "scene": "comment_top_rec",
+          "icon_url": "",
+          "hint_text": "大家都在搜:",
+          "extra_info": "{}"
+        },
+        {
+          "words": [
+            {
+              "word": "东方美学惊动外国人",
+              "word_id": "7113485773504402688",
+              "info": "{\"qrec_for_search\":\"{}\"}"
+            }
+          ],
+          "scene": "feed_bottom_rec",
+          "icon_url": "",
+          "hint_text": "相关搜索",
+          "extra_info": "{}"
+        },
+        {
+          "words": [
+            {
+              "word": "马面裙",
+              "word_id": "6585792700228867335",
+              "info": "{\"qrec_for_search\":\"{\\\"query_ecom\\\":\\\"1\\\"}\"}"
+            }
+          ],
+          "scene": "detail_inbox_rex",
+          "icon_url": "",
+          "hint_text": "",
+          "extra_info": "{\"is_life_intent\":1}"
+        }
+      ]
+    }
+  },
+  {
+    "aweme_id": "7194815099381484860",
+    "desc": "“舒服自在 心情可爱”",
+    "create_time": 1675173437,
+    "music": {
+      "id": 7148009620639615000,
+      "title": "热恋情节 (剪辑版)",
+      "author": "吴子健REmi,Kiya",
+      "cover_medium": {
+        "uri": "tos-cn-v-2774c002/9d37509812014d409364325b77e6d237",
+        "url_list": [
+          "https://p26.douyinpic.com/aweme/200x200/tos-cn-v-2774c002/9d37509812014d409364325b77e6d237.jpeg",
+          "https://p11.douyinpic.com/aweme/200x200/tos-cn-v-2774c002/9d37509812014d409364325b77e6d237.jpeg"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "cover_thumb": {
+        "uri": "tos-cn-v-2774c002/9d37509812014d409364325b77e6d237",
+        "url_list": [
+          "https://p26.douyinpic.com/aweme/100x100/tos-cn-v-2774c002/9d37509812014d409364325b77e6d237.jpeg",
+          "https://p11.douyinpic.com/aweme/100x100/tos-cn-v-2774c002/9d37509812014d409364325b77e6d237.jpeg"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "play_url": {
+        "uri": "https://sf5-hl-cdn-tos.douyinstatic.com/obj/tos-cn-ve-2774/02b4ba225f494a059364af97041be2c6",
+        "url_list": [
+          "https://sf5-hl-cdn-tos.douyinstatic.com/obj/tos-cn-ve-2774/02b4ba225f494a059364af97041be2c6",
+          "https://sf3-cdn-tos.douyinstatic.com/obj/tos-cn-ve-2774/02b4ba225f494a059364af97041be2c6"
+        ],
+        "width": 720,
+        "height": 720,
+        "url_key": "7148009620639614978"
+      },
+      "duration": 26,
+      "user_count": 0,
+      "owner_id": "102729275012",
+      "owner_nickname": "",
+      "is_original": true
+    },
+    "video": {
+      "play_addr": {
+        "uri": "v0200fg10000cfchocjc77u067mjilm0",
+        "url_list": [
+          "https://www.douyin.com/aweme/v1/play/?video_id=v0200fg10000cfchocjc77u067mjilm0&line=0&file_id=805ad6fd235a4de6b40a328ef5dd2b02&sign=beaf8e329f41389bea0902d238c08351&is_play_url=1&source=PackSourceEnum_PUBLISH"
+        ],
+        "width": 1080,
+        "height": 1920,
+        "url_key": "v0200fg10000cfchocjc77u067mjilm0_h264_1080p_4137918",
+        "data_size": 11949792,
+        "file_hash": "beaf8e329f41389bea0902d238c08351",
+        "file_cs": "c:0-38336-52cc|d:0-5974895-3a9e,5974896-11949791-d86b|a:v0200fg10000cfchocjc77u067mjilm0"
+      },
+      "cover": {
+        "uri": "tos-cn-p-0015/a20bb1cf567b4d02a5b32dda7968afb2_1675173440",
+        "url_list": [
+          "xTLLBfd4OOunKpyEaZfMS.png"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "poster": "out4.jpg",
+      "height": 1920,
+      "width": 1080,
+      "ratio": "1080p",
+      "duration": 23103
+    },
+    "share_url": "https://www.iesdouyin.com/share/video/7194815099381484860/?region=CN&mid=7148009620639614978&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&titleType=title&share_sign=F2Ek0wwDxcMoJO3F1RGG2yh8K67IDRhjKsx0w27aBaM-&share_version=170400&ts=1710490642&from_aid=6383&from_ssr=1",
+    "statistics": {
+      "admire_count": 0,
+      "comment_count": 90107,
+      "digg_count": 2484136,
+      "collect_count": 128162,
+      "play_count": 0,
+      "share_count": 337108
+    },
+    "status": {
+      "listen_video_status": 0,
+      "is_delete": false,
+      "allow_share": true,
+      "is_prohibited": false,
+      "in_reviewing": false,
+      "part_see": 0,
+      "private_status": 0,
+      "review_result": {
+        "review_status": 0
+      }
+    },
+    "text_extra": [],
+    "is_top": 1,
+    "share_info": {
+      "share_url": "https://www.iesdouyin.com/share/video/7194815099381484860/?region=CN&mid=7148009620639614978&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&titleType=title&share_sign=F2Ek0wwDxcMoJO3F1RGG2yh8K67IDRhjKsx0w27aBaM-&share_version=170400&ts=1710490642&from_aid=6383&from_ssr=1",
+      "share_link_desc": "9.94 U@l.pD 02/28 qrr:/ “舒服自在 心情可爱”  %s 复制此链接,打开Dou音搜索,直接观看视频!"
+    },
+    "duration": 23103,
+    "image_infos": null,
+    "risk_infos": {
+      "vote": false,
+      "warn": false,
+      "risk_sink": false,
+      "type": 0,
+      "content": ""
+    },
+    "position": null,
+    "author_user_id": 60685235913,
+    "author": {
+      "avatar_168x168": {
+        "height": 720,
+        "uri": "aweme-avatar/tos-cn-avt-0015_31589976a8790e475243be1ec4e24141",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/img/aweme-avatar/tos-cn-avt-0015_31589976a8790e475243be1ec4e24141~c5_168x168.jpeg?from=2956013662"
+        ],
+        "width": 720
+      },
+      "avatar_300x300": {
+        "height": 720,
+        "uri": "aweme-avatar/tos-cn-avt-0015_31589976a8790e475243be1ec4e24141",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/img/aweme-avatar/tos-cn-avt-0015_31589976a8790e475243be1ec4e24141~c5_300x300.jpeg?from=2956013662"
+        ],
+        "width": 720
+      },
+      "aweme_count": 386,
+      "birthday_hide_level": 0,
+      "can_show_group_card": 1,
+      "card_entries": [
+        {
+          "card_data": "{\"is_order_card\":false,\"has_new\":false,\"is_store\":false,\"shop_id\":\"\",\"product_count\":106,\"store_type\":\"window\",\"icon_is_repeat\":false,\"icon_type\":\"png\",\"is_promotion_icon\":false,\"subtitle_resource_list\":\"\"}",
+          "event_params": "{\"entrance_location\":\"others_homepage\"}",
+          "goto_url": "sslocal://goods/shop?uid=60685235913&sec_uid=MS4wLjABAAAAaSfA0HM0mHsoLdNIiwcFfUUYmmD_xGE6IEni35uxzkE",
+          "icon_dark": {
+            "url_list": [
+              "https://lf3-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_dark-3x.png",
+              "https://lf9-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_dark-3x.png",
+              "https://lf26-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_dark-3x.png",
+              "https://lf3-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_dark-2x.png",
+              "https://lf9-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_dark-2x.png",
+              "https://lf26-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_dark-2x.png"
+            ]
+          },
+          "icon_light": {
+            "url_list": [
+              "https://lf3-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_light-3x.png",
+              "https://lf9-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_light-3x.png",
+              "https://lf26-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_light-3x.png",
+              "https://lf3-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_light-2x.png",
+              "https://lf9-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_light-2x.png",
+              "https://lf26-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_light-2x.png"
+            ]
+          },
+          "sub_title": "106件好物",
+          "title": "进入橱窗",
+          "type": 1
+        },
+        {
+          "goto_url": "aweme://im/FansGroup/GuestState",
+          "icon_dark": {
+            "uri": "https://p3.douyinpic.com/obj/im-resource/old_fans_group_manage_dark.png",
+            "url_list": [
+              "https://p3.douyinpic.com/obj/im-resource/old_fans_group_manage_dark.png",
+              "https://p6.douyinpic.com/obj/im-resource/old_fans_group_manage_dark.png",
+              "https://p9.douyinpic.com/obj/im-resource/old_fans_group_manage_dark.png"
+            ]
+          },
+          "icon_light": {
+            "uri": "https://p3.douyinpic.com/obj/im-resource/old_fans_group_manage_light.png",
+            "url_list": [
+              "https://p3.douyinpic.com/obj/im-resource/old_fans_group_manage_light.png",
+              "https://p6.douyinpic.com/obj/im-resource/old_fans_group_manage_light.png",
+              "https://p9.douyinpic.com/obj/im-resource/old_fans_group_manage_light.png"
+            ]
+          },
+          "sub_title": "6个群聊",
+          "title": "粉丝群",
+          "type": 2
+        },
+        {
+          "card_data": "{\"has_yellow_point\":false,\"announcement_release_time\":0,\"preview_video_release_time\":0,\"precipitation_video_release_time\":0,\"style\":0,\"appointment_id\":0,\"typ\":0,\"subscribe_cnt\":0,\"subscribe_status\":0,\"top_title\":\"\",\"top_subtitle\":\"\",\"cycle\":0}",
+          "goto_url": "sslocal://webcast_lynxview?url=https%3A%2F%2Flf-webcast-gr-sourcecdn.bytegecko.com%2Fobj%2Fbyte-gurd-source-gr%2Fwebcast%2Fmono%2Flynx%2Fcommunity_live_dynamic_douyin%2Ftemplate%2Fpages%2Flive_dynamic%2Ftemplate.js%3Fanchor_id%3D60685235913%26sec_anchor_id%3DMS4wLjABAAAAaSfA0HM0mHsoLdNIiwcFfUUYmmD_xGE6IEni35uxzkE&web_bg_color=%23161823&status_bar_color=white&type=fullscreen&hide_nav_bar=1&trans_status_bar=1&enable_preload=main&fallback_url=sslocal%3A%2F%2Fwebcast_webview%3Furl%3Dhttps%253A%252F%252Flf-webcast-gr-sourcecdn.bytegecko.com%252Fobj%252Fbyte-gurd-source-gr%252Fwebcast%252Fmono%252Flynx%252Fcommunity_live_dynamic_douyin%252Fweb%252Ftemplate%252Fpages%252Flive_dynamic%252Findex.html%253Fanchor_id%253D60685235913%2526sec_anchor_id%253DMS4wLjABAAAAaSfA0HM0mHsoLdNIiwcFfUUYmmD_xGE6IEni35uxzkE%26web_bg_color%3D%2523161823%26status_bar_color%3Dwhite%26type%3Dfullscreen%26hide_nav_bar%3D1%26trans_status_bar%3D1&enable_pad_adapter=1&screen_size_adaptation=1&pad_ratio=1",
+          "icon_dark": {
+            "uri": "obj/eden-cn/91eh7uhfnult/all_cards_old_version/type6_live_dynamic_dark.png",
+            "url_list": [
+              "https://p6-dy-ipv6.byteimg.com/obj/eden-cn/91eh7uhfnult/all_cards_old_version/type6_live_dynamic_dark.png",
+              "https://p3-dy-ipv6.byteimg.com/obj/eden-cn/91eh7uhfnult/all_cards_old_version/type6_live_dynamic_dark.png",
+              "https://p9-dy.byteimg.com/obj/eden-cn/91eh7uhfnult/all_cards_old_version/type6_live_dynamic_dark.png"
+            ]
+          },
+          "icon_light": {
+            "uri": "obj/eden-cn/91eh7uhfnult/all_cards_old_version/type6_live_dynamic_light.png",
+            "url_list": [
+              "https://p6-dy-ipv6.byteimg.com/obj/eden-cn/91eh7uhfnult/all_cards_old_version/type6_live_dynamic_light.png",
+              "https://p3-dy-ipv6.byteimg.com/obj/eden-cn/91eh7uhfnult/all_cards_old_version/type6_live_dynamic_light.png",
+              "https://p9-dy.byteimg.com/obj/eden-cn/91eh7uhfnult/all_cards_old_version/type6_live_dynamic_light.png"
+            ]
+          },
+          "sub_title": "查看历史记录",
+          "title": "直播动态",
+          "type": 6
+        }
+      ],
+      "city": "",
+      "commerce_info": {
+        "challenge_list": null,
+        "head_image_list": null,
+        "offline_info_list": [],
+        "smart_phone_list": null,
+        "task_list": null
+      },
+      "commerce_user_info": {
+        "ad_revenue_rits": null,
+        "has_ads_entry": true,
+        "show_star_atlas_cooperation": true,
+        "star_atlas": 1
+      },
+      "commerce_user_level": 0,
+      "country": "",
+      "cover_colour": "#03373EE5",
+      "cover_url": [
+        {
+          "uri": "douyin-user-image-file/fe9be2c397cad5a176fab3334b1489af",
+          "url_list": [
+            "OJejSMUWjRSsOIQpN6PQt.png"
+          ]
+        },
+        {
+          "uri": "c8510002be9a3a61aad2",
+          "url_list": [
+            "c5i0r0kDPE8RhtwpG4laP.png"
+          ]
+        }
+      ],
+      "district": "",
+      "favoriting_count": 0,
+      "follow_status": 0,
+      "follower_count": 20702784,
+      "follower_request_status": 0,
+      "follower_status": 0,
+      "following_count": 331,
+      "forward_count": 0,
+      "gender": 0,
+      "ip_location": "IP属地:北京",
+      "max_follower_count": 22273750,
+      "mplatform_followers_count": 20702784,
+      "nickname": "刘思瑶nice",
+      "province": "",
+      "public_collects_count": 0,
+      "share_info": {
+        "bool_persist": 1,
+        "share_desc": "长按复制此条消息,打开抖音搜索,查看TA的更多作品。",
+        "share_image_url": {
+          "uri": "tos-cn-p-0015/oIkBAqbnNQMf9RlAgGlkn7JYbBPAyDwZeCWCQg",
+          "url_list": [
+            "9WwAt6mr5u_3PBt70BhBB.png"
+          ]
+        },
+        "share_qrcode_url": {
+          "uri": "216a0032711d2c2a129c",
+          "url_list": [
+            "https://p6.douyinpic.com/obj/216a0032711d2c2a129c",
+            "https://p3.douyinpic.com/obj/216a0032711d2c2a129c",
+            "https://p11.douyinpic.com/obj/216a0032711d2c2a129c"
+          ]
+        },
+        "share_title": "快来加入抖音,让你发现最有趣的我!",
+        "share_url": "www.iesdouyin.com/share/user/MS4wLjABAAAAaSfA0HM0mHsoLdNIiwcFfUUYmmD_xGE6IEni35uxzkE?iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&sec_uid=MS4wLjABAAAAaSfA0HM0mHsoLdNIiwcFfUUYmmD_xGE6IEni35uxzkE&from_ssr=1&from_aid=6383&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB",
+        "share_weibo_desc": "长按复制此条消息,打开抖音搜索,查看TA的更多作品。"
+      },
+      "short_id": "0",
+      "signature": "以梦为马 不负韶华\n橱窗不打烊 直播同款好物点击橱窗\n售后问题联系@思瑶小助理(售后版) \n唯一小号:@刘大嘴很nice\n团队招新剧情编导、摄影:BBQ190621(非本人)",
+      "total_favorited": 492829057,
+      "uid": "60685235913",
+      "unique_id": "Lsy0508",
+      "user_age": -1,
+      "white_cover_url": [
+        {
+          "uri": "douyin-user-image-file/fe9be2c397cad5a176fab3334b1489af",
+          "url_list": [
+            "5rd0jNlFnC8waKzr-sQgE.png"
+          ]
+        },
+        {
+          "uri": "318f1000413827e122102",
+          "url_list": [
+            "XjeGajXblJgviElLPAm4o.png"
+          ]
+        }
+      ]
+    },
+    "prevent_download": false,
+    "long_video": null,
+    "aweme_control": {
+      "can_forward": true,
+      "can_share": true,
+      "can_comment": true,
+      "can_show_comment": true
+    },
+    "images": null,
+    "suggest_words": {
+      "suggest_words": [
+        {
+          "words": [
+            {
+              "word": "刘思瑶路人实拍",
+              "word_id": "6847993825084839181",
+              "info": "{\"qrec_for_search\":\"{}\"}"
+            }
+          ],
+          "scene": "comment_top_rec",
+          "icon_url": "",
+          "hint_text": "大家都在搜:",
+          "extra_info": "{}"
+        },
+        {
+          "words": [
+            {
+              "word": "刘思瑶靠哪个视频火的",
+              "word_id": "6848883031428568328",
+              "info": "{\"qrec_for_search\":\"{}\"}"
+            }
+          ],
+          "scene": "feed_bottom_rec",
+          "icon_url": "",
+          "hint_text": "相关搜索",
+          "extra_info": "{}"
+        },
+        {
+          "words": [
+            {
+              "word": "刘思瑶",
+              "word_id": "6583343168425563395",
+              "info": "{\"qrec_for_search\":\"{}\"}"
+            }
+          ],
+          "scene": "detail_inbox_rex",
+          "icon_url": "",
+          "hint_text": "",
+          "extra_info": "{}"
+        }
+      ]
+    }
+  },
+  {
+    "aweme_id": "6826943630775831812",
+    "desc": "豌豆大丰收!得闲把豌豆吃食都做个遍——豌豆凉粉和豌豆黄 #美食趣胃计划",
+    "create_time": 1589541629,
+    "music": {
+      "id": 6826943794529897000,
+      "title": "@李子柒创作的原声",
+      "author": "李子柒",
+      "cover_medium": {
+        "uri": "720x720/aweme-avatar/mosaic-legacy_330b002fd56a93e8b6f1",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/aweme/720x720/aweme-avatar/mosaic-legacy_330b002fd56a93e8b6f1.jpeg?from=116350172"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "cover_thumb": {
+        "uri": "100x100/aweme-avatar/mosaic-legacy_330b002fd56a93e8b6f1",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/aweme/100x100/aweme-avatar/mosaic-legacy_330b002fd56a93e8b6f1.jpeg?from=116350172"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "play_url": {
+        "uri": "https://sf3-cdn-tos.douyinstatic.com/obj/ies-music/40698c870e6ad3ed385a5fc6f1440010.mp3",
+        "url_list": [
+          "https://sf3-cdn-tos.douyinstatic.com/obj/ies-music/40698c870e6ad3ed385a5fc6f1440010.mp3",
+          "https://sf5-hl-cdn-tos.douyinstatic.com/obj/ies-music/40698c870e6ad3ed385a5fc6f1440010.mp3"
+        ],
+        "width": 720,
+        "height": 720,
+        "url_key": "6826943794529897224"
+      },
+      "duration": 212,
+      "user_count": 0,
+      "owner_id": "68310389333",
+      "owner_nickname": "李子柒",
+      "is_original": false
+    },
+    "video": {
+      "play_addr": {
+        "uri": "v0200fa50000bqv2ovedm15352jvv5vg",
+        "url_list": [
+          "https://www.douyin.com/aweme/v1/play/?video_id=v0200fa50000bqv2ovedm15352jvv5vg&line=0&file_id=efac24de9d2548228975fc8429e5bdcb&sign=3b7c4acc3b831e92448d6909510074c0&is_play_url=1&source=PackSourceEnum_PUBLISH"
+        ],
+        "width": 1920,
+        "height": 1080,
+        "url_key": "v0200fa50000bqv2ovedm15352jvv5vg_h264_1080p_3841299",
+        "data_size": 102006660,
+        "file_hash": "3b7c4acc3b831e92448d6909510074c0"
+      },
+      "cover": {
+        "uri": "tos-cn-p-0015/917accda82fe4db1a3eb8cda9e85d4d3_1589521771",
+        "url_list": [
+          "I81-xgRsVO_i7ol6Gt-BH.png"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "height": 1080,
+      "width": 1920,
+      "ratio": "1080p",
+      "use_static_cover": false,
+      "duration": 212442,
+      "horizontal_type": 1
+    },
+    "share_url": "https://www.iesdouyin.com/share/video/6826943630775831812/?region=CN&mid=6826943794529897224&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&titleType=title&share_sign=v.6EMSpJflsIouammw_x31VJ_JUFXNS_Lq97U5StboI-&share_version=170400&ts=1710483022&from_aid=6383&from_ssr=1",
+    "statistics": {
+      "admire_count": 0,
+      "comment_count": 11735,
+      "digg_count": 511928,
+      "collect_count": 1971,
+      "play_count": 0,
+      "share_count": 4059
+    },
+    "status": {
+      "listen_video_status": 0,
+      "is_delete": false,
+      "allow_share": true,
+      "is_prohibited": false,
+      "in_reviewing": false,
+      "part_see": 0,
+      "private_status": 0,
+      "review_result": {
+        "review_status": 0
+      }
+    },
+    "text_extra": [
+      {
+        "start": 28,
+        "end": 35,
+        "type": 1,
+        "hashtag_name": "美食趣胃计划",
+        "hashtag_id": "1657693004378126",
+        "is_commerce": false,
+        "caption_start": 0,
+        "caption_end": 0
+      }
+    ],
+    "is_top": 0,
+    "share_info": {
+      "share_url": "https://www.iesdouyin.com/share/video/6826943630775831812/?region=CN&mid=6826943794529897224&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&titleType=title&share_sign=v.6EMSpJflsIouammw_x31VJ_JUFXNS_Lq97U5StboI-&share_version=170400&ts=1710483022&from_aid=6383&from_ssr=1",
+      "share_link_desc": "3.89 HiC:/ J@I.ic 10/04 豌豆大丰收!得闲把豌豆吃食都做个遍——豌豆凉粉和豌豆黄 # 美食趣胃计划  %s 复制此链接,打开Dou音搜索,直接观看视频!"
+    },
+    "duration": 212442,
+    "image_infos": null,
+    "risk_infos": {
+      "vote": false,
+      "warn": false,
+      "risk_sink": false,
+      "type": 0,
+      "content": ""
+    },
+    "position": null,
+    "author_user_id": 68310389333,
+    "author": {
+      "avatar_168x168": {
+        "height": 720,
+        "uri": "aweme-avatar/mosaic-legacy_330b002fd56a93e8b6f1",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/img/aweme-avatar/mosaic-legacy_330b002fd56a93e8b6f1~c5_168x168.jpeg?from=2956013662"
+        ],
+        "width": 720
+      },
+      "avatar_300x300": {
+        "height": 720,
+        "uri": "aweme-avatar/mosaic-legacy_330b002fd56a93e8b6f1",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/img/aweme-avatar/mosaic-legacy_330b002fd56a93e8b6f1~c5_300x300.jpeg?from=2956013662"
+        ],
+        "width": 720
+      },
+      "aweme_count": 772,
+      "birthday_hide_level": 0,
+      "can_show_group_card": 1,
+      "city": "绵阳",
+      "commerce_info": {
+        "challenge_list": null,
+        "head_image_list": null,
+        "offline_info_list": [],
+        "smart_phone_list": null,
+        "task_list": null
+      },
+      "commerce_user_info": {
+        "ad_revenue_rits": null,
+        "has_ads_entry": true,
+        "show_star_atlas_cooperation": false,
+        "star_atlas": 1
+      },
+      "commerce_user_level": 0,
+      "country": "中国",
+      "cover_colour": "#02161823",
+      "cover_url": [
+        {
+          "uri": "c8510002be9a3a61aad2",
+          "url_list": [
+            "2uHX3U05JE9hy7W6loPDK.png"
+          ]
+        }
+      ],
+      "district": null,
+      "favoriting_count": 0,
+      "follow_status": 0,
+      "follower_count": 40201989,
+      "follower_request_status": 0,
+      "follower_status": 0,
+      "following_count": 1,
+      "forward_count": 1,
+      "gender": 2,
+      "max_follower_count": 45635987,
+      "mplatform_followers_count": 48209510,
+      "nickname": "李子柒",
+      "province": "四川",
+      "public_collects_count": 0,
+      "share_info": {
+        "bool_persist": 1,
+        "share_desc": "长按复制此条消息,打开抖音搜索,查看TA的更多作品。",
+        "share_image_url": {
+          "uri": "tos-cn-p-0015/b01c417ab84c48a18151df6f4874c517_1651306670",
+          "url_list": [
+            "noPw6HHZHlcIQTKhc-Sr4.png"
+          ]
+        },
+        "share_qrcode_url": {
+          "uri": "330b002fd4ab5b64f36e",
+          "url_list": [
+            "https://p11.douyinpic.com/obj/330b002fd4ab5b64f36e",
+            "https://p3.douyinpic.com/obj/330b002fd4ab5b64f36e",
+            "https://p26.douyinpic.com/obj/330b002fd4ab5b64f36e"
+          ]
+        },
+        "share_title": "快来加入抖音,让你发现最有趣的我!",
+        "share_url": "www.iesdouyin.com/share/user/MS4wLjABAAAAPCnTQLqza4Xqu-uO7KZHcKuILkO7RRz2oapyOC04AQ0?iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&sec_uid=MS4wLjABAAAAPCnTQLqza4Xqu-uO7KZHcKuILkO7RRz2oapyOC04AQ0&from_ssr=1&from_aid=6383&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB",
+        "share_weibo_desc": "长按复制此条消息,打开抖音搜索,查看TA的更多作品。"
+      },
+      "short_id": "71158770",
+      "signature": "李家有女,人称子柒。联系邮箱:loveliziqi777@163.com",
+      "total_favorited": 222610560,
+      "uid": "68310389333",
+      "unique_id": "",
+      "user_age": -1,
+      "white_cover_url": [
+        {
+          "uri": "318f1000413827e122102",
+          "url_list": [
+            "wqKmvIFifx1re2KR2VAXF.png"
+          ]
+        }
+      ]
+    },
+    "prevent_download": false,
+    "long_video": null,
+    "aweme_control": {
+      "can_forward": true,
+      "can_share": true,
+      "can_comment": true,
+      "can_show_comment": true
+    },
+    "images": null,
+    "suggest_words": {
+      "suggest_words": [
+        {
+          "words": [],
+          "scene": "detail_inbox_rex",
+          "icon_url": "",
+          "hint_text": "",
+          "extra_info": "{}"
+        }
+      ]
+    }
+  },
+  {
+    "aweme_id": "7086793311662427422",
+    "desc": "是谁多事种芭蕉 早也潇潇 晚也潇潇",
+    "create_time": 1650022650,
+    "music": {
+      "id": 7078968306874519000,
+      "title": "@那份情创作的原声",
+      "author": "那份情",
+      "cover_medium": {
+        "uri": "720x720/aweme-avatar/tos-cn-avt-0015_7dc46fb855cbdd0b5477b1c7870bc402",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/aweme/720x720/aweme-avatar/tos-cn-avt-0015_7dc46fb855cbdd0b5477b1c7870bc402.jpeg?from=116350172"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "cover_thumb": {
+        "uri": "100x100/aweme-avatar/tos-cn-avt-0015_7dc46fb855cbdd0b5477b1c7870bc402",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/aweme/100x100/aweme-avatar/tos-cn-avt-0015_7dc46fb855cbdd0b5477b1c7870bc402.jpeg?from=116350172"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "play_url": {
+        "uri": "https://sf5-hl-cdn-tos.douyinstatic.com/obj/ies-music/7078968303053802248.mp3",
+        "url_list": [
+          "https://sf5-hl-cdn-tos.douyinstatic.com/obj/ies-music/7078968303053802248.mp3",
+          "https://sf3-cdn-tos.douyinstatic.com/obj/ies-music/7078968303053802248.mp3"
+        ],
+        "width": 720,
+        "height": 720,
+        "url_key": "7078968306874518280"
+      },
+      "duration": 11,
+      "user_count": 0,
+      "owner_id": "1284709492195710",
+      "owner_nickname": "那份情",
+      "is_original": false
+    },
+    "video": {
+      "play_addr": {
+        "uri": "v0200fg10000c9clgmjc77ue4odprim0",
+        "url_list": [
+          "https://www.douyin.com/aweme/v1/play/?video_id=v0200fg10000c9clgmjc77ue4odprim0&line=0&file_id=84a8b20033f44c85b044a2ff3e6f338c&sign=0fe078620c046cae1ba801d713573544&is_play_url=1&source=PackSourceEnum_PUBLISH"
+        ],
+        "width": 1080,
+        "height": 1920,
+        "url_key": "v0200fg10000c9clgmjc77ue4odprim0_h264_1080p_1495016",
+        "data_size": 1679090,
+        "file_hash": "0fe078620c046cae1ba801d713573544",
+        "file_cs": "c:0-11133-0757|d:0-839544-0a4e,839545-1679089-fbd7|a:v0200fg10000c9clgmjc77ue4odprim0"
+      },
+      "cover": {
+        "uri": "tos-cn-p-0015/b1803bf884bf4eccb485c4a0b3f21fa2_1650022656",
+        "url_list": [
+          "rpnJD_oAHyZKAIxTcHYFR.png"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "height": 1920,
+      "width": 1080,
+      "ratio": "1080p",
+      "use_static_cover": false,
+      "duration": 8985
+    },
+    "share_url": "https://www.iesdouyin.com/share/video/7086793311662427422/?region=CN&mid=7078968306874518280&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&titleType=title&share_sign=oft5kgO.GqX9vvBNODXIaSIzsUFkNpHOGwYiJ5ahUs4-&share_version=170400&ts=1710491656&from_aid=6383&from_ssr=1",
+    "statistics": {
+      "admire_count": 0,
+      "comment_count": 71707,
+      "digg_count": 1970263,
+      "collect_count": 101425,
+      "play_count": 0,
+      "share_count": 183570
+    },
+    "status": {
+      "listen_video_status": 0,
+      "is_delete": false,
+      "allow_share": true,
+      "is_prohibited": false,
+      "in_reviewing": false,
+      "part_see": 0,
+      "private_status": 0,
+      "review_result": {
+        "review_status": 0
+      }
+    },
+    "text_extra": [],
+    "is_top": 1,
+    "share_info": {
+      "share_url": "https://www.iesdouyin.com/share/video/7086793311662427422/?region=CN&mid=7078968306874518280&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&titleType=title&share_sign=oft5kgO.GqX9vvBNODXIaSIzsUFkNpHOGwYiJ5ahUs4-&share_version=170400&ts=1710491656&from_aid=6383&from_ssr=1",
+      "share_link_desc": "8.97 Jip:/ 09/15 A@g.Ok 是谁多事种芭蕉 早也潇潇 晚也潇潇  %s 复制此链接,打开Dou音搜索,直接观看视频!"
+    },
+    "duration": 8985,
+    "image_infos": null,
+    "risk_infos": {
+      "vote": false,
+      "warn": false,
+      "risk_sink": false,
+      "type": 0,
+      "content": ""
+    },
+    "position": null,
+    "author_user_id": 59054327754,
+    "author": {
+      "avatar_168x168": {
+        "height": 720,
+        "uri": "aweme-avatar/mosaic-legacy_20b7700050147c01968f3",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/img/aweme-avatar/mosaic-legacy_20b7700050147c01968f3~c5_168x168.jpeg?from=2956013662"
+        ],
+        "width": 720
+      },
+      "avatar_300x300": {
+        "height": 720,
+        "uri": "aweme-avatar/mosaic-legacy_20b7700050147c01968f3",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/img/aweme-avatar/mosaic-legacy_20b7700050147c01968f3~c5_300x300.jpeg?from=2956013662"
+        ],
+        "width": 720
+      },
+      "aweme_count": 296,
+      "birthday_hide_level": 1,
+      "can_show_group_card": 1,
+      "card_entries": [
+        {
+          "card_data": "{\"is_order_card\":false,\"has_new\":false,\"is_store\":false,\"shop_id\":\"\",\"product_count\":6,\"store_type\":\"window\",\"icon_is_repeat\":false,\"icon_type\":\"png\",\"is_promotion_icon\":false,\"subtitle_resource_list\":\"\"}",
+          "event_params": "{\"entrance_location\":\"others_homepage\"}",
+          "goto_url": "sslocal://goods/shop?uid=59054327754&sec_uid=MS4wLjABAAAAe_AKPvxBX0C_4vyLj5Wye-_BU8M0S6tZFZUu61FuycU",
+          "icon_dark": {
+            "url_list": [
+              "https://lf3-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_dark-3x.png",
+              "https://lf9-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_dark-3x.png",
+              "https://lf26-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_dark-3x.png",
+              "https://lf3-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_dark-2x.png",
+              "https://lf9-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_dark-2x.png",
+              "https://lf26-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_dark-2x.png"
+            ]
+          },
+          "icon_light": {
+            "url_list": [
+              "https://lf3-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_light-3x.png",
+              "https://lf9-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_light-3x.png",
+              "https://lf26-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_light-3x.png",
+              "https://lf3-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_light-2x.png",
+              "https://lf9-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_light-2x.png",
+              "https://lf26-static.bytednsdoc.com/obj/eden-cn/azlylaup_j_tvjl/ljhwZthlaukjlkulzlp/ecom_window/ecom_window_other_light-2x.png"
+            ]
+          },
+          "sub_title": "6件好物",
+          "title": "进入橱窗",
+          "type": 1
+        }
+      ],
+      "city": "",
+      "commerce_info": {
+        "challenge_list": null,
+        "head_image_list": null,
+        "offline_info_list": [],
+        "smart_phone_list": null,
+        "task_list": null
+      },
+      "commerce_user_info": {
+        "ad_revenue_rits": null,
+        "has_ads_entry": true,
+        "show_star_atlas_cooperation": true,
+        "star_atlas": 1
+      },
+      "commerce_user_level": 0,
+      "country": "",
+      "cover_colour": "#03997706",
+      "cover_url": [
+        {
+          "uri": "douyin-user-image-file/f2196ddaa37f3097932d8a29ff0d0ca5",
+          "url_list": [
+            "AiIEMkIA7Cb3s5c4e7e6g.png"
+          ]
+        },
+        {
+          "uri": "c8510002be9a3a61aad2",
+          "url_list": [
+            "aHzLr77vcdBMUil15rXBa.png"
+          ]
+        }
+      ],
+      "district": "",
+      "favoriting_count": 0,
+      "follow_status": 0,
+      "follower_count": 7078268,
+      "follower_request_status": 0,
+      "follower_status": 0,
+      "following_count": 88,
+      "forward_count": 79,
+      "gender": 2,
+      "ip_location": "IP属地:天津",
+      "max_follower_count": 7078290,
+      "mplatform_followers_count": 7078268,
+      "nickname": "我是香秀🐂🍺",
+      "province": "",
+      "public_collects_count": 0,
+      "share_info": {
+        "bool_persist": 1,
+        "share_desc": "长按复制此条消息,打开抖音搜索,查看TA的更多作品。",
+        "share_image_url": {
+          "uri": "tos-cn-p-0015/oge0HBDnlBbbZHjeDc4WtAI7AA0xb88gd9Ipjc",
+          "url_list": [
+            "5jTb5yW0_50o6UaLR5hvo.png"
+          ]
+        },
+        "share_qrcode_url": {
+          "uri": "216a001823018b74cedd",
+          "url_list": [
+            "https://p3.douyinpic.com/obj/216a001823018b74cedd",
+            "https://p6.douyinpic.com/obj/216a001823018b74cedd",
+            "https://p11.douyinpic.com/obj/216a001823018b74cedd"
+          ]
+        },
+        "share_title": "快来加入抖音,让你发现最有趣的我!",
+        "share_url": "www.iesdouyin.com/share/user/MS4wLjABAAAAe_AKPvxBX0C_4vyLj5Wye-_BU8M0S6tZFZUu61FuycU?did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&sec_uid=MS4wLjABAAAAe_AKPvxBX0C_4vyLj5Wye-_BU8M0S6tZFZUu61FuycU&from_ssr=1&from_aid=6383&u_code=13kgm680k",
+        "share_weibo_desc": "长按复制此条消息,打开抖音搜索,查看TA的更多作品。"
+      },
+      "short_id": "8357999",
+      "signature": "合作:X229896(备注品牌 )",
+      "total_favorited": 202309485,
+      "uid": "59054327754",
+      "unique_id": "",
+      "user_age": -1,
+      "white_cover_url": [
+        {
+          "uri": "douyin-user-image-file/f2196ddaa37f3097932d8a29ff0d0ca5",
+          "url_list": [
+            "N_SVO2HXIpaY04hgsXYDI.png"
+          ]
+        },
+        {
+          "uri": "318f1000413827e122102",
+          "url_list": [
+            "Sfz4PgDDqyNYHkFyXub5g.png"
+          ]
+        }
+      ]
+    },
+    "prevent_download": false,
+    "long_video": null,
+    "aweme_control": {
+      "can_forward": true,
+      "can_share": true,
+      "can_comment": true,
+      "can_show_comment": true
+    },
+    "images": null,
+    "suggest_words": {
+      "suggest_words": [
+        {
+          "words": [
+            {
+              "word": "香秀路人视角",
+              "word_id": "6845265352025183492",
+              "info": "{\"qrec_for_search\":\"{}\"}"
+            }
+          ],
+          "scene": "comment_top_rec",
+          "icon_url": "",
+          "hint_text": "大家都在搜:",
+          "extra_info": "{}"
+        },
+        {
+          "words": [
+            {
+              "word": "香秀路人视角",
+              "word_id": "6845265352025183492",
+              "info": "{\"qrec_for_search\":\"{}\"}"
+            }
+          ],
+          "scene": "detail_inbox_rex",
+          "icon_url": "",
+          "hint_text": "",
+          "extra_info": "{}"
+        }
+      ]
+    }
+  },
+  {
+    "aweme_id": "7058970263043509539",
+    "desc": "今年是我在中国生活的第六年,时间过得好快#模特 #时尚大片",
+    "create_time": 1643544593,
+    "music": {
+      "id": 7053069981588000000,
+      "title": "歌曲6馍",
+      "author": "小表哥",
+      "cover_medium": {
+        "uri": "720x720/aweme-avatar/tos-cn-avt-0015_3c46137ed144fc55d4f86ec8a5d9a7c2",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/aweme/720x720/aweme-avatar/tos-cn-avt-0015_3c46137ed144fc55d4f86ec8a5d9a7c2.jpeg?from=116350172"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "cover_thumb": {
+        "uri": "100x100/aweme-avatar/tos-cn-avt-0015_3c46137ed144fc55d4f86ec8a5d9a7c2",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/aweme/100x100/aweme-avatar/tos-cn-avt-0015_3c46137ed144fc55d4f86ec8a5d9a7c2.jpeg?from=116350172"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "play_url": {
+        "uri": "https://sf5-hl-cdn-tos.douyinstatic.com/obj/ies-music/7053069976533879582.mp3",
+        "url_list": [
+          "https://sf5-hl-cdn-tos.douyinstatic.com/obj/ies-music/7053069976533879582.mp3",
+          "https://sf6-cdn-tos.douyinstatic.com/obj/ies-music/7053069976533879582.mp3"
+        ],
+        "width": 720,
+        "height": 720,
+        "url_key": "7053069981587999496"
+      },
+      "duration": 15,
+      "user_count": 0,
+      "owner_id": "62606014976",
+      "owner_nickname": "小表哥",
+      "is_original": false
+    },
+    "video": {
+      "play_addr": {
+        "uri": "v0d00fg10000c7r7t1jc77u9isqpctf0",
+        "url_list": [
+          "https://www.douyin.com/aweme/v1/play/?video_id=v0d00fg10000c7r7t1jc77u9isqpctf0&line=0&file_id=e0122395b1334f96b5d017a63db15b6f&sign=4d2631c2c60b6d4baa4af480150a73d5&is_play_url=1&source=PackSourceEnum_PUBLISH"
+        ],
+        "width": 1080,
+        "height": 1920,
+        "url_key": "v0d00fg10000c7r7t1jc77u9isqpctf0_h264_1080p_1368771",
+        "data_size": 1887023,
+        "file_hash": "4d2631c2c60b6d4baa4af480150a73d5",
+        "file_cs": "c:0-13217-e2be|d:0-943510-7409,943511-1887022-1666|a:v0d00fg10000c7r7t1jc77u9isqpctf0"
+      },
+      "cover": {
+        "uri": "tos-cn-p-0015/7b60d15d62db46f08f7e94b53f60cc57_1643544598",
+        "url_list": [
+          "KeOt7oHBb6JLrcKBO8Eoq.png"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "height": 1920,
+      "width": 1080,
+      "ratio": "1080p",
+      "use_static_cover": true,
+      "duration": 11029
+    },
+    "share_url": "https://www.iesdouyin.com/share/video/7058970263043509539/?region=CN&mid=7053069981587999496&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&titleType=title&share_sign=3SasaqXD2jVDhD3MFnemB1bF3GtWH1stG4C1OhN_WCI-&share_version=170400&ts=1710489764&from_aid=6383&from_ssr=1",
+    "statistics": {
+      "admire_count": 0,
+      "comment_count": 4096,
+      "digg_count": 152247,
+      "collect_count": 3527,
+      "play_count": 0,
+      "share_count": 1189
+    },
+    "status": {
+      "listen_video_status": 0,
+      "is_delete": false,
+      "allow_share": true,
+      "is_prohibited": false,
+      "in_reviewing": false,
+      "part_see": 0,
+      "private_status": 0,
+      "review_result": {
+        "review_status": 0
+      }
+    },
+    "text_extra": [
+      {
+        "start": 20,
+        "end": 23,
+        "type": 1,
+        "hashtag_name": "模特",
+        "hashtag_id": "1565395368494082",
+        "is_commerce": false,
+        "caption_start": 0,
+        "caption_end": 0
+      },
+      {
+        "start": 24,
+        "end": 29,
+        "type": 1,
+        "hashtag_name": "时尚大片",
+        "hashtag_id": "1585870642948110",
+        "is_commerce": false,
+        "caption_start": 0,
+        "caption_end": 0
+      }
+    ],
+    "is_top": 1,
+    "share_info": {
+      "share_url": "https://www.iesdouyin.com/share/video/7058970263043509539/?region=CN&mid=7053069981587999496&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&titleType=title&share_sign=3SasaqXD2jVDhD3MFnemB1bF3GtWH1stG4C1OhN_WCI-&share_version=170400&ts=1710489764&from_aid=6383&from_ssr=1",
+      "share_link_desc": "8.48 Q@k.Ch 09/15 tRK:/ 今年是我在中国生活的第六年,时间过得好快# 模特 # 时尚大片  %s 复制此链接,打开Dou音搜索,直接观看视频!"
+    },
+    "duration": 11029,
+    "image_infos": null,
+    "risk_infos": {
+      "vote": false,
+      "warn": false,
+      "risk_sink": false,
+      "type": 0,
+      "content": ""
+    },
+    "position": null,
+    "author_user_id": 95947614937,
+    "author": {
+      "avatar_168x168": {
+        "height": 720,
+        "uri": "aweme-avatar/tos-cn-avt-0015_633427f316a0cbf229d95993a422545a",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/img/aweme-avatar/tos-cn-avt-0015_633427f316a0cbf229d95993a422545a~c5_168x168.jpeg?from=2956013662"
+        ],
+        "width": 720
+      },
+      "avatar_300x300": {
+        "height": 720,
+        "uri": "aweme-avatar/tos-cn-avt-0015_633427f316a0cbf229d95993a422545a",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/img/aweme-avatar/tos-cn-avt-0015_633427f316a0cbf229d95993a422545a~c5_300x300.jpeg?from=2956013662"
+        ],
+        "width": 720
+      },
+      "aweme_count": 330,
+      "birthday_hide_level": 0,
+      "can_show_group_card": 1,
+      "city": "",
+      "commerce_info": {
+        "challenge_list": null,
+        "head_image_list": null,
+        "offline_info_list": [],
+        "smart_phone_list": null,
+        "task_list": null
+      },
+      "commerce_user_info": {
+        "ad_revenue_rits": null,
+        "has_ads_entry": true,
+        "show_star_atlas_cooperation": true,
+        "star_atlas": 1
+      },
+      "commerce_user_level": 0,
+      "country": "",
+      "cover_colour": "#03997706",
+      "cover_url": [
+        {
+          "uri": "douyin-user-image-file/215a5e084e4ef0fac70d54b5b6794760",
+          "url_list": [
+            "ugFWsFLtl37YDmOhdWVPP.png"
+          ]
+        },
+        {
+          "uri": "c8510002be9a3a61aad2",
+          "url_list": [
+            "6BjX7F0tH7NR_Ivj97YH_.png"
+          ]
+        }
+      ],
+      "district": "",
+      "favoriting_count": 0,
+      "follow_status": 1,
+      "follower_count": 2601355,
+      "follower_request_status": 0,
+      "follower_status": 0,
+      "following_count": 2,
+      "forward_count": 0,
+      "gender": 2,
+      "ip_location": "IP属地:广东",
+      "max_follower_count": 2681893,
+      "mplatform_followers_count": 2601355,
+      "nickname": "达莎Digi",
+      "province": "",
+      "public_collects_count": 0,
+      "share_info": {
+        "bool_persist": 1,
+        "share_desc": "长按复制此条消息,打开抖音搜索,查看TA的更多作品。",
+        "share_image_url": {
+          "uri": "tos-cn-p-0015/oMinDrDQQBALaq2bjjeaY3Pv4efApBAInCI7BR",
+          "url_list": [
+            "KjoYiM7SWjrqov4pC-xMm.png"
+          ]
+        },
+        "share_qrcode_url": {
+          "uri": "f9be000540dadbec288f",
+          "url_list": [
+            "https://p11.douyinpic.com/obj/f9be000540dadbec288f",
+            "https://p3.douyinpic.com/obj/f9be000540dadbec288f",
+            "https://p26.douyinpic.com/obj/f9be000540dadbec288f"
+          ]
+        },
+        "share_title": "快来加入抖音,让你发现最有趣的我!",
+        "share_url": "www.iesdouyin.com/share/user/MS4wLjABAAAADklus0JC1TslbavJzu9VKuTLteVBN5hELr2YN-mCQPg?from_ssr=1&from_aid=6383&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&sec_uid=MS4wLjABAAAADklus0JC1TslbavJzu9VKuTLteVBN5hELr2YN-mCQPg",
+        "share_weibo_desc": "长按复制此条消息,打开抖音搜索,查看TA的更多作品。"
+      },
+      "short_id": "0",
+      "signature": "模特\n商务:dashastyle(备注品牌)\n本人已婚💍",
+      "total_favorited": 54152232,
+      "uid": "95947614937",
+      "unique_id": "Dashalove",
+      "user_age": 25,
+      "white_cover_url": [
+        {
+          "uri": "douyin-user-image-file/215a5e084e4ef0fac70d54b5b6794760",
+          "url_list": [
+            "dGAmnrwmsaQBEpOli1dGm.png"
+          ]
+        },
+        {
+          "uri": "318f1000413827e122102",
+          "url_list": [
+            "Nuc00o5h98o_IqhHv6uL0.png"
+          ]
+        }
+      ]
+    },
+    "prevent_download": false,
+    "long_video": null,
+    "aweme_control": {
+      "can_forward": true,
+      "can_share": true,
+      "can_comment": true,
+      "can_show_comment": true
+    },
+    "images": null,
+    "suggest_words": {
+      "suggest_words": [
+        {
+          "words": [
+            {
+              "word": "达莎digi香奈儿走秀",
+              "word_id": "7112616440335258918",
+              "info": "{\"qrec_for_search\":\"{}\"}"
+            }
+          ],
+          "scene": "comment_top_rec",
+          "icon_url": "",
+          "hint_text": "大家都在搜:",
+          "extra_info": "{}"
+        },
+        {
+          "words": [
+            {
+              "word": "达莎是什么级别的模特",
+              "word_id": "7022624510902883618",
+              "info": "{\"qrec_for_search\":\"{}\"}"
+            }
+          ],
+          "scene": "detail_inbox_rex",
+          "icon_url": "",
+          "hint_text": "",
+          "extra_info": "{}"
+        }
+      ]
+    }
+  },
+  {
+    "aweme_id": "7013521050063342855",
+    "desc": "“我追随的光 是五角星的星光” 🇨🇳 #国庆  ",
+    "create_time": 1632974402,
+    "music": {
+      "id": 6728019157029506000,
+      "title": "@TOP创意广告创作的原声",
+      "author": "TOP创意广告",
+      "cover_medium": {
+        "uri": "720x720/aweme-avatar/tos-cn-avt-0015_9004e8d44eebec2c2574668dec682d44",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/aweme/720x720/aweme-avatar/tos-cn-avt-0015_9004e8d44eebec2c2574668dec682d44.jpeg?from=116350172"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "cover_thumb": {
+        "uri": "100x100/aweme-avatar/tos-cn-avt-0015_9004e8d44eebec2c2574668dec682d44",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/aweme/100x100/aweme-avatar/tos-cn-avt-0015_9004e8d44eebec2c2574668dec682d44.jpeg?from=116350172"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "play_url": {
+        "uri": "https://sf6-cdn-tos.douyinstatic.com/obj/ies-music/1642621652731965.mp3",
+        "url_list": [
+          "https://sf6-cdn-tos.douyinstatic.com/obj/ies-music/1642621652731965.mp3",
+          "https://sf3-cdn-tos.douyinstatic.com/obj/ies-music/1642621652731965.mp3"
+        ],
+        "width": 720,
+        "height": 720,
+        "url_key": "6728019157029505799"
+      },
+      "duration": 15,
+      "user_count": 0,
+      "owner_id": "95448880209",
+      "owner_nickname": "TOP创意广告",
+      "is_original": false
+    },
+    "video": {
+      "play_addr": {
+        "uri": "v0200fg10000c5aggr3c77ubnjmde3b0",
+        "url_list": [
+          "https://www.douyin.com/aweme/v1/play/?video_id=v0200fg10000c5aggr3c77ubnjmde3b0&line=0&file_id=0f329a8423724dc0aa0a9f28eebd0860&sign=28e7963949089d1a63e4ccb53132dc86&is_play_url=1&source=PackSourceEnum_PUBLISH"
+        ],
+        "width": 1516,
+        "height": 1076,
+        "url_key": "v0200fg10000c5aggr3c77ubnjmde3b0_h264_1080p_3169852",
+        "data_size": 3169852,
+        "file_hash": "28e7963949089d1a63e4ccb53132dc86",
+        "file_cs": "c:0-9554-b8a6|d:0-1584925-4b7b,1584926-3169851-16f4|a:v0200fg10000c5aggr3c77ubnjmde3b0"
+      },
+      "cover": {
+        "uri": "tos-cn-i-0813/fd9bde9fe9074237992696bec164d71f",
+        "url_list": [
+          "X5gTp24tgfdsK51YwNUer.png"
+        ],
+        "width": 720,
+        "height": 720
+      },
+      "height": 1076,
+      "width": 1516,
+      "ratio": "1080p",
+      "use_static_cover": true,
+      "duration": 8000,
+      "horizontal_type": 1
+    },
+    "share_url": "https://www.iesdouyin.com/share/video/7013521050063342855/?region=CN&mid=6728019157029505799&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&titleType=title&share_sign=oqiZpCUYByCyoDhD.SegMZSCr3ZDG6xS2P98HYB9LZE-&share_version=170400&ts=1710491939&from_aid=6383&from_ssr=1",
+    "statistics": {
+      "admire_count": 0,
+      "comment_count": 26954,
+      "digg_count": 2080753,
+      "collect_count": 44160,
+      "play_count": 0,
+      "share_count": 59431
+    },
+    "status": {
+      "listen_video_status": 0,
+      "is_delete": false,
+      "allow_share": true,
+      "is_prohibited": false,
+      "in_reviewing": false,
+      "part_see": 0,
+      "private_status": 0,
+      "review_result": {
+        "review_status": 0
+      }
+    },
+    "text_extra": [
+      {
+        "start": 21,
+        "end": 24,
+        "type": 1,
+        "hashtag_name": "国庆",
+        "hashtag_id": "1579507629054990",
+        "is_commerce": false,
+        "caption_start": 0,
+        "caption_end": 0
+      }
+    ],
+    "is_top": 1,
+    "share_info": {
+      "share_url": "https://www.iesdouyin.com/share/video/7013521050063342855/?region=CN&mid=6728019157029505799&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1&titleType=title&share_sign=oqiZpCUYByCyoDhD.SegMZSCr3ZDG6xS2P98HYB9LZE-&share_version=170400&ts=1710491939&from_aid=6383&from_ssr=1",
+      "share_link_desc": "6.43 04/24 d@n.dn RXZ:/ “我追随的光 是五角星的星光” 🇨🇳 # 国庆   %s 复制此链接,打开Dou音搜索,直接观看视频!"
+    },
+    "duration": 8000,
+    "image_infos": null,
+    "risk_infos": {
+      "vote": false,
+      "warn": false,
+      "risk_sink": false,
+      "type": 0,
+      "content": ""
+    },
+    "position": null,
+    "author_user_id": 24058267831,
+    "author": {
+      "avatar_168x168": {
+        "height": 720,
+        "uri": "aweme-avatar/tos-cn-avt-0015_99d3a4923c94e1e27b16209743eaec24",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/img/aweme-avatar/tos-cn-avt-0015_99d3a4923c94e1e27b16209743eaec24~c5_168x168.jpeg?from=2956013662"
+        ],
+        "width": 720
+      },
+      "avatar_300x300": {
+        "height": 720,
+        "uri": "aweme-avatar/tos-cn-avt-0015_99d3a4923c94e1e27b16209743eaec24",
+        "url_list": [
+          "https://p3-pc.douyinpic.com/img/aweme-avatar/tos-cn-avt-0015_99d3a4923c94e1e27b16209743eaec24~c5_300x300.jpeg?from=2956013662"
+        ],
+        "width": 720
+      },
+      "aweme_count": 359,
+      "birthday_hide_level": 1,
+      "can_show_group_card": 1,
+      "card_entries": [
+        {
+          "goto_url": "aweme://im/FansGroup/GuestState",
+          "icon_dark": {
+            "uri": "https://p3.douyinpic.com/obj/im-resource/old_fans_group_manage_dark.png",
+            "url_list": [
+              "https://p3.douyinpic.com/obj/im-resource/old_fans_group_manage_dark.png",
+              "https://p6.douyinpic.com/obj/im-resource/old_fans_group_manage_dark.png",
+              "https://p9.douyinpic.com/obj/im-resource/old_fans_group_manage_dark.png"
+            ]
+          },
+          "icon_light": {
+            "uri": "https://p3.douyinpic.com/obj/im-resource/old_fans_group_manage_light.png",
+            "url_list": [
+              "https://p3.douyinpic.com/obj/im-resource/old_fans_group_manage_light.png",
+              "https://p6.douyinpic.com/obj/im-resource/old_fans_group_manage_light.png",
+              "https://p9.douyinpic.com/obj/im-resource/old_fans_group_manage_light.png"
+            ]
+          },
+          "sub_title": "2个群聊",
+          "title": "粉丝群",
+          "type": 2
+        }
+      ],
+      "city": "",
+      "commerce_info": {
+        "challenge_list": null,
+        "head_image_list": null,
+        "offline_info_list": [],
+        "smart_phone_list": null,
+        "task_list": null
+      },
+      "commerce_user_info": {
+        "ad_revenue_rits": null,
+        "has_ads_entry": true,
+        "show_star_atlas_cooperation": true,
+        "star_atlas": 1
+      },
+      "commerce_user_level": 0,
+      "country": "",
+      "cover_colour": "#03373EE5",
+      "cover_url": [
+        {
+          "uri": "douyin-user-image-file/d46c49d4b05053c65595ecbe61c6891b",
+          "url_list": [
+            "bR6bvJkjP1rb9VgPazc2s.png"
+          ]
+        },
+        {
+          "uri": "c8510002be9a3a61aad2",
+          "url_list": [
+            "p1grunB9W_XiTEc7PICV1.png"
+          ]
+        }
+      ],
+      "district": "",
+      "favoriting_count": 2311,
+      "follow_status": 0,
+      "follower_count": 32588958,
+      "follower_request_status": 0,
+      "follower_status": 0,
+      "following_count": 176,
+      "forward_count": 5,
+      "gender": 0,
+      "ip_location": "IP属地:上海",
+      "max_follower_count": 34420921,
+      "mplatform_followers_count": 32588958,
+      "nickname": "彭十六elf",
+      "province": "",
+      "public_collects_count": 0,
+      "share_info": {
+        "bool_persist": 1,
+        "share_desc": "长按复制此条消息,打开抖音搜索,查看TA的更多作品。",
+        "share_image_url": {
+          "uri": "tos-cn-p-0015/owsfNETCAfOArLdvZBoycrIjBJDY0ufGeAfDHV",
+          "url_list": [
+            "MubOElriJZkNJ-3dFNjhQ.png"
+          ]
+        },
+        "share_qrcode_url": {
+          "uri": "c15000ddb20b5723744",
+          "url_list": [
+            "https://p6.douyinpic.com/obj/c15000ddb20b5723744",
+            "https://p26.douyinpic.com/obj/c15000ddb20b5723744",
+            "https://p3.douyinpic.com/obj/c15000ddb20b5723744"
+          ]
+        },
+        "share_title": "快来加入抖音,让你发现最有趣的我!",
+        "share_url": "www.iesdouyin.com/share/user/MS4wLjABAAAAAAKy2_R6k-oFWT5E-97gbGZQ1laaweQMWImJDkDaef0?sec_uid=MS4wLjABAAAAAAKy2_R6k-oFWT5E-97gbGZQ1laaweQMWImJDkDaef0&from_ssr=1&from_aid=6383&u_code=13kgm680k&did=MS4wLjABAAAAiOgYyZm8XbWZMr5o3OvhR-TEOuNygb_hQOwkie-VXJpDYaR4vZfpiIGBfAWKCFHB&iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ&with_sec_did=1",
+        "share_weibo_desc": "长按复制此条消息,打开抖音搜索,查看TA的更多作品。"
+      },
+      "short_id": "0",
+      "signature": "记录成长\n做有意义的事情🐰☀️\n分享日常:@六猪变美日记 \n🍠:彭十六elf \n商务v:MUXUAN16e(招摄影&剪辑)\n演出v:ME13919\n何其荣幸 何德何能💜",
+      "total_favorited": 1012905132,
+      "uid": "24058267831",
+      "unique_id": "elfin16",
+      "user_age": -1,
+      "white_cover_url": [
+        {
+          "uri": "douyin-user-image-file/d46c49d4b05053c65595ecbe61c6891b",
+          "url_list": [
+            "IO7207wEo0bNpJ9XW3dSI.png"
+          ]
+        },
+        {
+          "uri": "318f1000413827e122102",
+          "url_list": [
+            "owYg1mq7cB3X43GqIlD8g.png"
+          ]
+        }
+      ]
+    },
+    "prevent_download": false,
+    "long_video": null,
+    "aweme_control": {
+      "can_forward": true,
+      "can_share": true,
+      "can_comment": true,
+      "can_show_comment": true
+    },
+    "images": null,
+    "suggest_words": {
+      "suggest_words": [
+        {
+          "words": [
+            {
+              "word": "马面裙",
+              "word_id": "6585792700228867335",
+              "info": "{\"qrec_for_search\":\"{\\\"query_ecom\\\":\\\"1\\\"}\"}"
+            }
+          ],
+          "scene": "detail_inbox_rex",
+          "icon_url": "",
+          "hint_text": "",
+          "extra_info": "{\"is_life_intent\":1}"
+        }
+      ]
+    }
+  }
+]

+ 3 - 3
src/router/globalRoutes.js

@@ -1,8 +1,8 @@
 export default [
   {
     path: '/',
-    name: 'Home',
-    meta: { title: '首页' },
-    component: () => import('@/views/Home.vue'),
+    name: 'FarmScenery',
+    meta: { title: '农场美景', footerTab: 1 },
+    component: () => import('@/views/home/index.vue'),
   },
 ]

+ 14 - 1
src/router/mainRoutes.js

@@ -1 +1,14 @@
-export default []
+export default [
+    {
+        path: '/guard-map',
+        name: 'GuardMap',
+        meta: { title: '守护地图', footerTab: 2 },
+        component: () => import('@/views/guard-map/index.vue'),
+    },
+    {
+        path: '/my-guard',
+        name: 'MyGuard',
+        meta: { title: '我的守护', footerTab: 3 },
+        component: () => import('@/views/my-guard/index.vue'),
+    },
+]

+ 51 - 0
src/utils/bus.ts

@@ -0,0 +1,51 @@
+export default {
+  eventMap: new Map<string, Array<(val?: unknown) => void>>(),
+  on(eventType: string, cb: (val?: unknown) => void) {
+    const cbs = this.eventMap.get(eventType)
+    if (cbs) {
+      cbs.push(cb)
+    } else {
+      this.eventMap.set(eventType, [cb])
+    }
+  },
+  once(eventType: string, cb: (val?: unknown) => void) {
+    this.eventMap.set(eventType, [cb])
+  },
+  off(eventType: string, fn?: (val?: unknown) => void) {
+    if (!this.eventMap.has(eventType)) return
+    if (fn) {
+      const cbs = this.eventMap.get(eventType)!
+      const rIndex = cbs.findIndex((v) => v === fn)
+      if (rIndex > -1) cbs.splice(rIndex, 1)
+      this.eventMap.set(eventType, cbs)
+    } else {
+      this.eventMap.delete(eventType)
+    }
+  },
+  offAll() {
+    this.eventMap = new Map()
+  },
+  emit(eventType: string, val?: unknown) {
+    const cbs = this.eventMap.get(eventType)
+    if (cbs) cbs.forEach((cb) => cb(val))
+  },
+}
+
+export const EVENT_KEY = {
+  ENTER_FULLSCREEN: 'ENTER_FULLSCREEN',
+  EXIT_FULLSCREEN: 'EXIT_FULLSCREEN',
+  OPEN_COMMENTS: 'OPEN_COMMENTS',
+  CLOSE_COMMENTS: 'CLOSE_COMMENTS',
+  SHOW_SHARE: 'SHOW_SHARE',
+  NAV: 'NAV',
+  GO_USERINFO: 'GO_USERINFO',
+  CURRENT_ITEM: 'CURRENT_ITEM',
+  TOGGLE_CURRENT_VIDEO: 'TOGGLE_CURRENT_VIDEO',
+  SINGLE_CLICK: 'SINGLE_CLICK',
+  SINGLE_CLICK_BROADCAST: 'SINGLE_CLICK_BROADCAST',
+  ITEM_TOGGLE: 'ITEM_TOGGLE',
+  ITEM_PLAY: 'ITEM_PLAY',
+  ITEM_STOP: 'ITEM_STOP',
+  UPDATE_ITEM: 'UPDATE_ITEM',
+  REMOVE_MUTED: 'REMOVE_MUTED',
+}

+ 21 - 0
src/utils/const_var.ts

@@ -0,0 +1,21 @@
+export const SlideType = {
+  HORIZONTAL: 0,
+  VERTICAL: 1,
+  VERTICAL_INFINITE: 2,
+}
+
+export const SlideItemPlayStatus = {
+  Play: 'Play',
+  Stop: 'Stop',
+  Pause: 'Pause',
+}
+
+export const DefaultUser = {
+  nickname: '',
+  unique_id: '',
+  uid: '',
+  avatar_168x168: { url_list: [''] },
+  cover_url: [{ url_list: [] }],
+  follow_status: 0,
+  is_follow: false,
+}

+ 35 - 0
src/utils/dom.ts

@@ -0,0 +1,35 @@
+export function _css(el: HTMLElement, key: string, value?: string | number) {
+  const reg = /^-?\d+.?\d*(px|pt|em|rem|vw|vh|%|rpx|ms)$/i
+  if (value === undefined) {
+    let val: string | null = null
+    if ('getComputedStyle' in window) {
+      val = window.getComputedStyle(el, null)[key as keyof CSSStyleDeclaration] as string
+    } else {
+      val = (el as HTMLElement & { currentStyle?: Record<string, string> }).currentStyle?.[key] ?? null
+    }
+    return reg.test(val as string) ? parseFloat(val as string) : val
+  }
+  if (
+    ['top', 'left', 'bottom', 'right', 'width', 'height', 'font-size', 'margin', 'padding'].includes(
+      key,
+    )
+  ) {
+    if (!reg.test(String(value))) {
+      if (!String(value).includes('calc')) {
+        value = `${value}px`
+      }
+    }
+  }
+  if (key === 'transform') {
+    const style = el.style as CSSStyleDeclaration & Record<string, string>
+    style.webkitTransform =
+      style.MsTransform =
+      style.msTransform =
+      style.MozTransform =
+      style.OTransform =
+      style.transform =
+        String(value)
+  } else {
+    ;(el.style as CSSStyleDeclaration & Record<string, string>)[key] = String(value)
+  }
+}

+ 8 - 0
src/utils/hooks/useNav.ts

@@ -0,0 +1,8 @@
+import { useRouter } from 'vue-router'
+
+export function useNav() {
+  const router = useRouter()
+  return (path: string, query: Record<string, string> = {}) => {
+    router.push({ path, query })
+  }
+}

+ 42 - 0
src/utils/index.ts

@@ -0,0 +1,42 @@
+export function _stopPropagation(e: Event) {
+  e.stopImmediatePropagation()
+  e.stopPropagation()
+  e.preventDefault()
+}
+
+export function _notice(val: string) {
+  const div = document.createElement('div')
+  div.classList.add('global-notice')
+  div.textContent = val
+  document.body.append(div)
+  setTimeout(() => {
+    document.body.removeChild(div)
+  }, 1000)
+}
+
+export function _no() {
+  _notice('未实现')
+}
+
+/** 与 douyin-vue 一致:相对路径封面走 CDN */
+const IMG_BASE = 'https://dy.ttentau.top/imgs/images/'
+
+export function _formatNumber(num?: number) {
+  if (!num) return '0'
+  if (num > 100000000) return (num / 100000000).toFixed(1) + '亿'
+  if (num > 10000) return (num / 10000).toFixed(1) + '万'
+  return String(num)
+}
+
+export function _checkImgUrl(url?: string) {
+  if (!url) return ''
+  if (
+    url.includes('assets/img') ||
+    url.includes('file://') ||
+    url.includes('data:image') ||
+    url.startsWith('http')
+  ) {
+    return url
+  }
+  return IMG_BASE + url
+}

+ 196 - 0
src/utils/slide.ts

@@ -0,0 +1,196 @@
+import bus from '@/utils/bus'
+import { _stopPropagation } from '@/utils/index'
+import { SlideType } from '@/utils/const_var'
+import { nextTick } from 'vue'
+import { _css } from '@/utils/dom'
+
+type SlidePointerEvent = PointerEvent & { touches: TouchList }
+
+function checkEvent(e: PointerEvent): e is SlidePointerEvent {
+  const isMobile = /Mobi|Android|iPhone/i.test(navigator.userAgent)
+  if (!isMobile || (isMobile && e instanceof PointerEvent)) {
+    const pe = e as SlidePointerEvent
+    pe.touches = [
+      {
+        clientX: e.clientX,
+        clientY: e.clientY,
+        pageX: e.pageX,
+        pageY: e.pageY,
+      },
+    ] as unknown as TouchList
+  }
+  return true
+}
+
+export function slideInit(el: HTMLElement, state: Record<string, unknown>) {
+  state.wrapper = state.wrapper || { width: 0, height: 0, childrenLength: 0 }
+  const wrapper = state.wrapper as { width: number; height: number; childrenLength: number }
+  wrapper.width = _css(el, 'width') as number
+  wrapper.height = _css(el, 'height') as number
+  nextTick(() => {
+    wrapper.childrenLength = el.children.length
+  })
+  const t = getSlideOffset(state, el)
+  let dx1 = 0
+  let dx2 = 0
+  if (state.type === SlideType.HORIZONTAL) dx1 = t
+  else dx2 = t
+  _css(el, 'transform', `translate3d(${dx1}px, ${dx2}px, 0)`)
+}
+
+export function canSlide(state: Record<string, unknown>) {
+  if (state.needCheck) {
+    const move = state.move as { x: number; y: number }
+    const judgeValue = state.judgeValue as number
+    if (Math.abs(move.x) > judgeValue || Math.abs(move.y) > judgeValue) {
+      const angle = (Math.abs(move.x) * 10) / (Math.abs(move.y) * 10)
+      state.next = state.type === SlideType.HORIZONTAL ? angle > 1 : angle <= 1
+      state.needCheck = false
+    } else {
+      return false
+    }
+  }
+  return state.next
+}
+
+function canNext(state: Record<string, unknown>, isNext: boolean) {
+  const wrapper = state.wrapper as { childrenLength: number }
+  const localIndex = state.localIndex as number
+  return !((localIndex === 0 && !isNext) || (localIndex === wrapper.childrenLength - 1 && isNext))
+}
+
+export function slideTouchStart(e: PointerEvent, el: HTMLElement, state: Record<string, unknown>) {
+  if (!checkEvent(e)) return
+  const pe = e as SlidePointerEvent
+  _css(el, 'transition-duration', '0ms')
+  const start = state.start as { x: number; y: number; time: number }
+  start.x = pe.touches[0].pageX
+  start.y = pe.touches[0].pageY
+  start.time = Date.now()
+  state.isDown = true
+}
+
+export function slideTouchMove(
+  e: PointerEvent,
+  el: HTMLElement,
+  state: Record<string, unknown>,
+  canNextCb: typeof canNext | null = null,
+  notNextCb: (() => void) | null = null,
+) {
+  if (!checkEvent(e)) return
+  if (!state.isDown) return
+  const pe = e as SlidePointerEvent
+  const move = state.move as { x: number; y: number }
+  const start = state.start as { x: number; y: number }
+  move.x = pe.touches[0].pageX - start.x
+  move.y = pe.touches[0].pageY - start.y
+  const canSlideRes = canSlide(state)
+  const isNext = state.type === SlideType.HORIZONTAL ? move.x < 0 : move.y < 0
+  if (canSlideRes) {
+    if (!canNextCb) canNextCb = canNext
+    if (canNextCb(state, isNext)) {
+      ;(window as Window).isMoved = true
+      _stopPropagation(e)
+      if (state.type === SlideType.HORIZONTAL) {
+        bus.emit((state.name as string) + '-moveX', move.x)
+      }
+      const t = getSlideOffset(state, el) + (isNext ? (state.judgeValue as number) : -(state.judgeValue as number))
+      let dx1 = 0
+      let dx2 = 0
+      if (state.type === SlideType.HORIZONTAL) dx1 = t + move.x
+      else dx2 = t + move.y
+      _css(el, 'transition-duration', '0ms')
+      _css(el, 'transform', `translate3d(${dx1}px, ${dx2}px, 0)`)
+    } else {
+      notNextCb?.()
+    }
+  }
+}
+
+export function slideTouchEnd(
+  e: PointerEvent,
+  state: Record<string, unknown>,
+  canNextCb: typeof canNext | null = null,
+  nextCb: ((isNext: boolean) => void) | null = null,
+  notNextCb: (() => void) | null = null,
+) {
+  if (!checkEvent(e)) return
+  if (!state.isDown) return
+  if (state.next) {
+    const move = state.move as { x: number; y: number }
+    const wrapper = state.wrapper as { width: number; height: number }
+    const isHorizontal = state.type === SlideType.HORIZONTAL
+    const isNext = isHorizontal ? move.x < 0 : move.y < 0
+    if (!canNextCb) canNextCb = canNext
+    if (canNextCb(state, isNext)) {
+      const endTime = Date.now()
+      const start = state.start as { time: number }
+      let gapTime = endTime - start.time
+      const distance = isHorizontal ? move.x : move.y
+      const judgeValue = isHorizontal ? wrapper.width : wrapper.height
+      if (Math.abs(distance) < 20) gapTime = 1000
+      if (Math.abs(distance) > judgeValue / 3) gapTime = 100
+      if (gapTime < 150) {
+        const localIndex = state.localIndex as number
+        if (isNext) state.localIndex = localIndex + 1
+        else state.localIndex = localIndex - 1
+        return nextCb?.(isNext)
+      }
+    } else {
+      return notNextCb?.()
+    }
+  } else {
+    notNextCb?.()
+  }
+}
+
+export function slideReset(
+  e: PointerEvent,
+  el: HTMLElement,
+  state: Record<string, unknown>,
+  emit: ((event: 'update:index', index: number) => void) | null = null,
+) {
+  if (!checkEvent(e)) return
+  _css(el, 'transition-duration', '300ms')
+  const t = getSlideOffset(state, el)
+  let dx1 = 0
+  let dx2 = 0
+  if (state.type === SlideType.HORIZONTAL) {
+    bus.emit((state.name as string) + '-end', state.localIndex)
+    dx1 = t
+  } else {
+    bus.emit((state.name as string) + '-end')
+    dx2 = t
+  }
+  _css(el, 'transform', `translate3d(${dx1}px, ${dx2}px, 0)`)
+  const start = state.start as { x: number; y: number; time: number }
+  const move = state.move as { x: number; y: number }
+  start.x = start.y = start.time = move.x = move.y = 0
+  state.next = false
+  state.needCheck = true
+  state.isDown = false
+  setTimeout(() => {
+    ;(window as Window).isMoved = false
+  }, 200)
+  emit?.('update:index', state.localIndex as number)
+}
+
+export function getSlideOffset(state: Record<string, unknown>, el: HTMLElement) {
+  const localIndex = state.localIndex as number
+  if (state.type === SlideType.HORIZONTAL) {
+    const widths: number[] = []
+    Array.from(el.children).forEach((v) => widths.push(v.getBoundingClientRect().width))
+    const slice = widths.slice(0, localIndex)
+    if (slice.length) return -slice.reduce((a, b) => a + b, 0)
+    return 0
+  }
+  const wrapper = state.wrapper as { height: number }
+  if (state.type === SlideType.VERTICAL_INFINITE) {
+    return -localIndex * wrapper.height
+  }
+  const heights: number[] = []
+  Array.from(el.children).forEach((v) => heights.push(v.getBoundingClientRect().height))
+  const slice = heights.slice(0, localIndex)
+  if (slice.length) return -slice.reduce((a, b) => a + b, 0)
+  return 0
+}

+ 47 - 0
src/views/guard-map/index.vue

@@ -0,0 +1,47 @@
+<template>
+  <div class="tab-page">
+    <div class="page-body">
+      <h1>守护地图</h1>
+      <p class="desc">地图功能开发中</p>
+    </div>
+    <BaseFooter />
+  </div>
+</template>
+
+<script setup lang="ts">
+import BaseFooter from '@/components/BaseFooter.vue'
+</script>
+
+<style scoped lang="less">
+.tab-page {
+  width: 100%;
+  height: 100%;
+  min-height: calc(var(--vh, 1vh) * 100);
+  background: var(--main-bg);
+  position: relative;
+  overflow: hidden;
+}
+
+.page-body {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: calc(var(--vh, 1vh) * 100 - 80rem);
+  padding: 20rem;
+  box-sizing: border-box;
+
+  h1 {
+    margin: 0 0 12rem;
+    font-size: 20rem;
+    color: #fff;
+    font-weight: 500;
+  }
+
+  .desc {
+    margin: 0;
+    font-size: 14rem;
+    color: rgba(255, 255, 255, 0.5);
+  }
+}
+</style>

+ 218 - 0
src/views/home/components/IndicatorHome.vue

@@ -0,0 +1,218 @@
+<template>
+  <div class="indicator-home">
+    <div class="notice" :style="noticeStyle"><span>下拉刷新内容</span></div>
+    <div ref="toolbar" class="toolbar" :style="toolbarStyle">
+      <Icon
+        icon="tabler:menu-deep"
+        class="search"
+        style="transform: rotateY(180deg)"
+        @click="$emit('showSlidebar')"
+      />
+      <div class="tab-ctn">
+        <div ref="tabs" class="tabs">
+          <div
+            v-for="(label, i) in tabLabels"
+            :key="label"
+            class="tab"
+            :class="{ active: index === i }"
+            @click.stop="change(i)"
+          >
+            <span>{{ label }}</span>
+          </div>
+        </div>
+        <div ref="indicator" class="indicator" />
+      </div>
+      <Icon icon="ion:search" class="search" @click="_no" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Icon } from '@iconify/vue'
+import { computed, onMounted, onUnmounted, ref } from 'vue'
+import bus from '@/utils/bus'
+import { _css } from '@/utils/dom'
+import { _no } from '@/utils/index'
+
+const props = defineProps({
+  loading: { type: Boolean, default: false },
+  name: { type: String, default: '' },
+  index: { type: Number, default: 0 },
+})
+
+const emit = defineEmits<{ 'update:index': [number]; showSlidebar: [] }>()
+
+const tabLabels = ['热点', '长视频', '关注', '经验', '推荐']
+const tabs = ref<HTMLElement | null>(null)
+const indicator = ref<HTMLElement | null>(null)
+const lefts = ref<number[]>([])
+const indicatorSpace = ref(0)
+const moveY = ref(0)
+const judgeValue = 20
+const homeRefresh = 60
+
+const transform = computed(
+  () =>
+    `translate3d(0, ${moveY.value - judgeValue > homeRefresh ? homeRefresh : moveY.value - judgeValue}px, 0)`,
+)
+
+const toolbarStyle = computed(() => {
+  if (props.loading) {
+    return { opacity: 1, transitionDuration: '300ms', transform: 'translate3d(0, 0, 0)' }
+  }
+  if (moveY.value) {
+    return {
+      opacity: 1 - (moveY.value - judgeValue) / (homeRefresh / 2),
+      transform: transform.value,
+    }
+  }
+  return { opacity: 1, transitionDuration: '300ms', transform: 'translate3d(0, 0, 0)' }
+})
+
+const noticeStyle = computed(() => {
+  if (props.loading) return { opacity: 0 }
+  if (moveY.value) {
+    return {
+      opacity: (moveY.value - judgeValue) / (homeRefresh / 2) - 0.5,
+      transform: transform.value,
+    }
+  }
+  return { opacity: 0 }
+})
+
+function change(index: number) {
+  emit('update:index', index)
+  if (!indicator.value) return
+  _css(indicator.value, 'transition-duration', '300ms')
+  _css(indicator.value, 'left', lefts.value[index] + 'px')
+}
+
+function initTabs() {
+  if (!tabs.value || !indicator.value) return
+  const indicatorWidth = _css(indicator.value, 'width') as number
+  const positions: number[] = []
+  for (let i = 0; i < tabs.value.children.length; i++) {
+    const item = tabs.value.children[i] as HTMLElement
+    const tabWidth = _css(item, 'width') as number
+    positions.push(
+      item.getBoundingClientRect().x -
+        (tabs.value.children[0] as HTMLElement).getBoundingClientRect().x +
+        (tabWidth * 0.5 - indicatorWidth / 2),
+    )
+  }
+  lefts.value = positions
+  indicatorSpace.value = positions[1] - positions[0]
+  _css(indicator.value, 'transition-duration', '300ms')
+  _css(indicator.value, 'left', positions[props.index] + 'px')
+}
+
+function move(val?: unknown) {
+  const e = val as number
+  if (!indicator.value) return
+  _css(indicator.value, 'transition-duration', '0ms')
+  _css(
+    indicator.value,
+    'left',
+    lefts.value[props.index] - e / (window.innerWidth / indicatorSpace.value) + 'px',
+  )
+}
+
+function end(val?: unknown) {
+  const index = val as number
+  moveY.value = 0
+  if (!indicator.value) return
+  _css(indicator.value, 'transition-duration', '300ms')
+  _css(indicator.value, 'left', lefts.value[index] + 'px')
+  setTimeout(() => {
+    if (indicator.value) _css(indicator.value, 'transition-duration', '0ms')
+  }, 300)
+}
+
+onMounted(() => {
+  initTabs()
+  bus.on(props.name + '-moveX', move)
+  bus.on(props.name + '-moveY', (e) => {
+    moveY.value = e as number
+  })
+  bus.on(props.name + '-end', end)
+})
+
+onUnmounted(() => {
+  bus.off(props.name + '-moveX', move)
+  bus.off(props.name + '-moveY')
+  bus.off(props.name + '-end', end)
+})
+</script>
+
+<style scoped lang="less">
+.indicator-home {
+  position: absolute;
+  font-size: 16rem;
+  top: 0;
+  left: 0;
+  z-index: 2;
+  width: 100%;
+  color: white;
+  height: var(--home-header-height);
+  font-weight: bold;
+
+  .notice {
+    opacity: 0;
+    top: 0;
+    position: absolute;
+    width: 100vw;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+
+  .toolbar {
+    z-index: 2;
+    position: relative;
+    width: 100%;
+    height: 100%;
+    box-sizing: border-box;
+    padding: 0 15rem;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .tab-ctn {
+      width: 80%;
+      position: relative;
+
+      .tabs {
+        display: flex;
+        justify-content: space-between;
+
+        .tab {
+          transition: color 0.3s;
+          color: rgba(255, 255, 255, 0.7);
+          font-size: 17rem;
+          cursor: pointer;
+
+          &.active {
+            color: white;
+          }
+        }
+      }
+
+      .indicator {
+        position: absolute;
+        bottom: -6rem;
+        height: 2.6rem;
+        width: 26rem;
+        background: #fff;
+        border-radius: 5rem;
+      }
+    }
+
+    .search {
+      color: white;
+      font-size: 24rem;
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 126 - 0
src/views/home/components/VideoFeed.vue

@@ -0,0 +1,126 @@
+<template>
+  <SlideVertical
+    v-model:index="currentIndex"
+    class="video-feed"
+    name="home-feed"
+  >
+    <SlideItem
+      v-for="(item, index) in list"
+      :key="item.aweme_id"
+      class="feed-item"
+    >
+      <BaseVideo
+        :item="item"
+        :index="index"
+        :position="{ uniqueId: UNIQUE_ID, index }"
+        :is-play="active && currentIndex === index"
+        @update:item="(val) => onItemUpdate(index, val)"
+      />
+    </SlideItem>
+  </SlideVertical>
+</template>
+
+<script setup lang="ts">
+import { onMounted, onUnmounted, ref, watch } from 'vue'
+import SlideVertical from '@/components/slide/SlideVertical.vue'
+import SlideItem from '@/components/slide/SlideItem.vue'
+import BaseVideo from '@/components/video/BaseVideo.vue'
+import { recommendVideos, type RecommendVideo } from '@/mock/homeData'
+import bus, { EVENT_KEY } from '@/utils/bus'
+
+const UNIQUE_ID = 'home'
+
+const props = defineProps({
+  active: { type: Boolean, default: false },
+})
+
+const list = ref<RecommendVideo[]>([...recommendVideos])
+const currentIndex = ref(0)
+
+function onItemUpdate(index: number, val: unknown) {
+  list.value[index] = val as RecommendVideo
+}
+
+function onUpdateItem(val?: unknown) {
+  const payload = val as {
+    position?: { uniqueId: string; index: number }
+    item?: RecommendVideo
+  }
+  if (payload?.position?.uniqueId === UNIQUE_ID && payload.item) {
+    list.value[payload.position.index] = payload.item
+  }
+}
+
+function broadcastPlay(index: number) {
+  bus.emit(EVENT_KEY.SINGLE_CLICK_BROADCAST, {
+    uniqueId: UNIQUE_ID,
+    index,
+    type: EVENT_KEY.ITEM_PLAY,
+  })
+}
+
+function broadcastStop(index: number) {
+  bus.emit(EVENT_KEY.SINGLE_CLICK_BROADCAST, {
+    uniqueId: UNIQUE_ID,
+    index,
+    type: EVENT_KEY.ITEM_STOP,
+  })
+}
+
+function togglePlay() {
+  if (!props.active) return
+  bus.emit(EVENT_KEY.SINGLE_CLICK_BROADCAST, {
+    uniqueId: UNIQUE_ID,
+    index: currentIndex.value,
+    type: EVENT_KEY.ITEM_TOGGLE,
+  })
+}
+
+watch(currentIndex, (newVal, oldVal) => {
+  if (!props.active) return
+  bus.emit(EVENT_KEY.CURRENT_ITEM, list.value[newVal])
+  broadcastPlay(newVal)
+  if (oldVal !== undefined && oldVal !== newVal) {
+    setTimeout(() => broadcastStop(oldVal), 200)
+  }
+})
+
+watch(
+  () => props.active,
+  (newVal) => {
+    const t = newVal ? 0 : 200
+    if (newVal) {
+      bus.emit(EVENT_KEY.CURRENT_ITEM, list.value[currentIndex.value])
+    }
+    setTimeout(() => {
+      if (newVal) broadcastPlay(currentIndex.value)
+      else broadcastStop(currentIndex.value)
+    }, t)
+  },
+  { immediate: true },
+)
+
+onMounted(() => {
+  bus.on(EVENT_KEY.TOGGLE_CURRENT_VIDEO, togglePlay)
+  bus.on(EVENT_KEY.UPDATE_ITEM, onUpdateItem)
+})
+
+onUnmounted(() => {
+  bus.off(EVENT_KEY.TOGGLE_CURRENT_VIDEO, togglePlay)
+  bus.off(EVENT_KEY.UPDATE_ITEM, onUpdateItem)
+})
+</script>
+
+<style scoped lang="less">
+.video-feed {
+  height: 100%;
+  width: 100%;
+  background: #000;
+
+  :deep(.feed-item) {
+    position: relative;
+    height: 100%;
+    width: 100%;
+  }
+}
+</style>

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

@@ -0,0 +1,367 @@
+<template>
+  <div id="home-index" class="test-slide-wrapper">
+    <SlideHorizontal v-model:index="state.baseIndex" name="first">
+      <SlideItem class="sidebar">
+        <div class="header">
+          <div class="left">下午好</div>
+          <div class="right" @click="nav('/home/live')">
+            <Icon icon="iconamoon:scanner" />
+            <span>扫一扫</span>
+          </div>
+        </div>
+        <div class="card">
+          <div class="header">
+            <div class="left">常用小程序</div>
+            <div class="right">
+              <span>全部</span>
+              <Icon icon="icon-park-outline:right" />
+            </div>
+          </div>
+          <div class="content">
+            <div
+              v-for="mp in miniPrograms"
+              :key="mp.id"
+              class="item"
+              @click="_no"
+            >
+              <img class="xcx" :src="mp.icon" alt="" />
+              <span>{{ mp.name }}</span>
+            </div>
+          </div>
+        </div>
+
+        <div class="card">
+          <div class="header">
+            <div class="left">最近常看</div>
+            <div class="right">
+              <span>全部</span>
+              <Icon icon="icon-park-outline:right" />
+            </div>
+          </div>
+          <div class="content">
+            <div
+              v-for="user in recentUsers"
+              :key="user.uid"
+              class="item avatar"
+              @click="goUser(user)"
+            >
+              <img :src="user.avatar" alt="" />
+              <span>{{ user.nickname }}</span>
+            </div>
+          </div>
+        </div>
+
+        <div class="card">
+          <div class="header">
+            <div class="left">常用功能</div>
+            <div class="right" />
+          </div>
+          <div class="content">
+            <div class="item" @click="_no">
+              <Icon icon="ion:wallet-outline" />
+              <span>我的钱包</span>
+            </div>
+            <div class="item" @click="_no">
+              <Icon icon="mingcute:coupon-line" />
+              <span>券包</span>
+            </div>
+            <div class="item" @click="_no">
+              <Icon icon="icon-park-outline:bytedance-applets" />
+              <span>小程序</span>
+            </div>
+            <div class="item" @click="_no">
+              <Icon icon="solar:history-linear" />
+              <span>观看历史</span>
+            </div>
+            <div class="item" @click="_no">
+              <Icon icon="fluent:content-settings-24-regular" />
+              <span>内容偏好</span>
+            </div>
+            <div class="item" @click="_no">
+              <Icon icon="iconoir:cloud-download" />
+              <span>离线模式</span>
+            </div>
+            <div class="item" @click="_no">
+              <Icon icon="ep:setting" />
+              <span>设置</span>
+            </div>
+            <div class="item" @click="_no">
+              <Icon icon="icon-park-outline:baggage-delay" />
+              <span>稍后再看</span>
+            </div>
+          </div>
+        </div>
+      </SlideItem>
+      <SlideItem>
+        <IndicatorHome
+          v-if="!state.fullScreen"
+          :loading="loading"
+          name="second"
+          @show-slidebar="state.baseIndex = 0"
+          v-model:index="state.navIndex"
+        />
+        <SlideHorizontal
+          v-model:index="state.navIndex"
+          class="first-horizontal-item"
+          name="second"
+          :change-active-index-use-anim="false"
+        >
+          <Slide0 :active="state.navIndex === 0 && state.baseIndex === 1" />
+          <SlideItem>
+            <LongVideo :active="state.navIndex === 1 && state.baseIndex === 1" />
+          </SlideItem>
+          <Slide2 :active="state.navIndex === 2 && state.baseIndex === 1" />
+          <SlideItem>
+            <Community :active="state.navIndex === 3 && state.baseIndex === 1" />
+          </SlideItem>
+          <Slide4 :active="state.navIndex === 4 && state.baseIndex === 1" />
+        </SlideHorizontal>
+
+        <BaseFooter />
+        <BaseMask
+          v-if="state.baseIndex === 0"
+          mode="white"
+          style="position: absolute"
+          @click="state.baseIndex = 1"
+        />
+      </SlideItem>
+      <SlideItem>
+        <UserPanel
+          v-model:current-item="state.currentItem"
+          :active="state.baseIndex === 2"
+          @toggle-can-move="(e: boolean) => (state.canMove = e)"
+          @back="state.baseIndex = 1"
+          @show-follow-setting="state.showFollowSetting = true"
+          @show-follow-setting2="state.showFollowSetting2 = true"
+        />
+      </SlideItem>
+    </SlideHorizontal>
+
+    <Comment
+      page-id="home-index"
+      :video-id="state.currentItem.aweme_id"
+      v-model="state.commentVisible"
+      @close="closeComments"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Icon } from '@iconify/vue'
+import { onMounted, onUnmounted, reactive, ref } from 'vue'
+import SlideHorizontal from '@/components/slide/SlideHorizontal.vue'
+import SlideItem from '@/components/slide/SlideItem.vue'
+import Comment from '@/components/Comment.vue'
+import IndicatorHome from './components/IndicatorHome.vue'
+import bus, { EVENT_KEY } from '@/utils/bus'
+import { useNav } from '@/utils/hooks/useNav'
+import UserPanel from '@/components/UserPanel.vue'
+import Community from './slide/Community.vue'
+import Slide0 from './slide/Slide0.vue'
+import Slide2 from './slide/Slide2.vue'
+import Slide4 from './slide/Slide4.vue'
+import { _no, _notice } from '@/utils'
+import LongVideo from './slide/LongVideo.vue'
+import BaseMask from '@/components/BaseMask.vue'
+import BaseFooter from '@/components/BaseFooter.vue'
+import { miniPrograms, recentUsers, recommendVideos } from '@/mock/homeData'
+
+const nav = useNav()
+const loading = ref(false)
+
+type VideoItem = (typeof recommendVideos)[number]
+
+const state = reactive({
+  active: true,
+  baseIndex: 1,
+  navIndex: 4,
+  canMove: true,
+  showFollowSetting: false,
+  showFollowSetting2: false,
+  commentVisible: false,
+  fullScreen: false,
+  currentItem: {
+    ...recommendVideos[0],
+    isRequest: false,
+    aweme_list: [] as unknown[],
+  } as VideoItem & { isRequest: boolean; aweme_list: unknown[] },
+})
+
+function goUser(user: (typeof recentUsers)[number]) {
+  const video = recommendVideos.find((v) => v.author.uid === user.uid) || recommendVideos[0]
+  state.currentItem = { ...video, isRequest: false, aweme_list: [] }
+  state.baseIndex = 2
+}
+
+function setCurrentItem(val?: unknown) {
+  const item = val as VideoItem
+  if (!state.active) return
+  if (state.baseIndex !== 1) return
+  if (state.currentItem.author?.uid !== item.author?.uid) {
+    state.currentItem = {
+      ...item,
+      isRequest: false,
+      aweme_list: [],
+    }
+  }
+}
+
+onMounted(() => {
+  bus.on(EVENT_KEY.ENTER_FULLSCREEN, () => {
+    if (!state.active) return
+    state.fullScreen = true
+  })
+  bus.on(EVENT_KEY.EXIT_FULLSCREEN, () => {
+    if (!state.active) return
+    state.fullScreen = false
+  })
+  bus.on(EVENT_KEY.OPEN_COMMENTS, () => {
+    if (!state.active) return
+    bus.emit(EVENT_KEY.ENTER_FULLSCREEN)
+    state.commentVisible = true
+  })
+  bus.on(EVENT_KEY.CLOSE_COMMENTS, () => {
+    if (!state.active) return
+    bus.emit(EVENT_KEY.EXIT_FULLSCREEN)
+    state.commentVisible = false
+  })
+  bus.on(EVENT_KEY.SHOW_SHARE, () => {
+    if (!state.active) return
+    _notice('分享面板待接入')
+  })
+  bus.on(EVENT_KEY.NAV, (val?: unknown) => {
+    if (!state.active) return
+    const { path, query } = val as { path: string; query?: Record<string, string> }
+    nav(path, query || {})
+  })
+  bus.on(EVENT_KEY.GO_USERINFO, () => {
+    if (!state.active) return
+    state.baseIndex = 2
+  })
+  bus.on(EVENT_KEY.CURRENT_ITEM, setCurrentItem)
+})
+
+onUnmounted(() => {
+  bus.offAll()
+})
+
+function closeComments() {
+  bus.emit(EVENT_KEY.CLOSE_COMMENTS)
+}
+</script>
+
+<style scoped lang="less">
+.test-slide-wrapper {
+  font-size: 14rem;
+  width: 100%;
+  height: 100%;
+  background: black;
+  overflow: hidden;
+
+  .sidebar {
+    touch-action: pan-y;
+    width: 80%;
+    height: calc(var(--vh, 1vh) * 100);
+    overflow: auto;
+    background: rgb(22, 22, 22);
+    padding: 10rem;
+    padding-bottom: 20rem;
+    box-sizing: border-box;
+
+    & > .header {
+      font-size: 16rem;
+      display: flex;
+      color: white;
+      justify-content: space-between;
+      align-items: center;
+
+      .right {
+        border-radius: 20rem;
+        padding: 8rem 15rem;
+        background: rgb(36, 36, 36);
+        display: flex;
+        align-items: center;
+        font-size: 14rem;
+        gap: 10rem;
+        cursor: pointer;
+
+        svg {
+          font-size: 18rem;
+        }
+      }
+    }
+
+    .card {
+      margin-top: 10rem;
+      border-radius: 12rem;
+      padding: 15rem;
+      background: rgb(29, 29, 29);
+
+      .header {
+        margin-bottom: 8rem;
+        font-size: 14rem;
+        display: flex;
+        color: white;
+        justify-content: space-between;
+        align-items: center;
+
+        .right {
+          display: flex;
+          align-items: center;
+          font-size: 12rem;
+          gap: 4rem;
+          color: gray;
+          cursor: pointer;
+
+          svg {
+            font-size: 16rem;
+          }
+        }
+      }
+
+      .content {
+        color: white;
+        display: grid;
+        grid-template-columns: 1fr 1fr 1fr;
+
+        .item {
+          min-height: 20vw;
+          display: flex;
+          flex-direction: column;
+          justify-content: center;
+          align-items: center;
+          font-size: 14rem;
+          gap: 8rem;
+          cursor: pointer;
+
+          svg {
+            font-size: 28rem;
+          }
+
+          .xcx {
+            border-radius: 12rem;
+            width: 50rem;
+            height: 50rem;
+          }
+        }
+
+        .avatar {
+          height: 25vw;
+
+          img {
+            border-radius: 50%;
+            width: 50rem;
+          }
+        }
+      }
+    }
+  }
+}
+
+.first-horizontal-item {
+  width: 100%;
+  height: calc(var(--vh, 1vh) * 100 - var(--footer-height)) !important;
+  overflow: hidden;
+  border-radius: 10rem;
+}
+</style>

+ 57 - 0
src/views/home/slide/Community.vue

@@ -0,0 +1,57 @@
+<script setup lang="ts">
+import SlideItem from '@/components/slide/SlideItem.vue'
+import { communityPosts, formatCount } from '@/mock/homeData'
+
+defineProps({ active: { type: Boolean, default: false } })
+</script>
+
+<template>
+  <SlideItem>
+    <div class="community">
+      <div v-for="item in communityPosts" :key="item.id" class="post">
+        <img :src="item.cover" class="cover" alt="" />
+        <div class="text">
+          <p class="title">{{ item.title }}</p>
+          <p class="meta">@{{ item.author }} · {{ formatCount(item.digg_count) }}赞</p>
+        </div>
+      </div>
+    </div>
+  </SlideItem>
+</template>
+
+<style scoped lang="less">
+.community {
+  height: 100%;
+  overflow-y: auto;
+  background: #111;
+  padding: 10rem;
+
+  .post {
+    display: flex;
+    gap: 12rem;
+    margin-bottom: 12rem;
+    background: rgb(29, 29, 29);
+    border-radius: 8rem;
+    padding: 10rem;
+    color: white;
+
+    .cover {
+      width: 72rem;
+      height: 72rem;
+      border-radius: 6rem;
+      object-fit: cover;
+    }
+
+    .title {
+      font-size: 14rem;
+      margin: 0 0 8rem;
+    }
+
+    .meta {
+      font-size: 12rem;
+      opacity: 0.6;
+      margin: 0;
+    }
+  }
+}
+</style>

+ 74 - 0
src/views/home/slide/LongVideo.vue

@@ -0,0 +1,74 @@
+<script setup lang="ts">
+import { Icon } from '@iconify/vue'
+import SlideItem from '@/components/slide/SlideItem.vue'
+import { formatCount, longVideos } from '@/mock/homeData'
+
+defineProps({ active: { type: Boolean, default: false } })
+</script>
+
+<template>
+  <SlideItem>
+    <div class="long-video">
+      <div v-for="item in longVideos" :key="item.id" class="card">
+        <img :src="item.cover" class="cover" alt="" />
+        <div class="body">
+          <p class="title">{{ item.title }}</p>
+          <p class="sub">@{{ item.author }} · {{ item.duration }}</p>
+          <span class="play">
+            <Icon icon="solar:play-circle-linear" />
+            {{ formatCount(item.play_count) }}
+          </span>
+        </div>
+      </div>
+    </div>
+  </SlideItem>
+</template>
+
+<style scoped lang="less">
+.long-video {
+  height: 100%;
+  overflow-y: auto;
+  background: #111;
+  padding: 10rem;
+
+  .card {
+    display: flex;
+    gap: 12rem;
+    margin-bottom: 12rem;
+    background: rgb(29, 29, 29);
+    border-radius: 8rem;
+    overflow: hidden;
+
+    .cover {
+      width: 120rem;
+      height: 68rem;
+      object-fit: cover;
+      flex-shrink: 0;
+    }
+
+    .body {
+      padding: 8rem 10rem 8rem 0;
+      color: white;
+
+      .title {
+        font-size: 14rem;
+        margin: 0 0 6rem;
+      }
+
+      .sub {
+        font-size: 12rem;
+        opacity: 0.6;
+        margin: 0 0 6rem;
+      }
+
+      .play {
+        font-size: 12rem;
+        display: flex;
+        align-items: center;
+        gap: 4rem;
+        opacity: 0.8;
+      }
+    }
+  }
+}
+</style>

+ 59 - 0
src/views/home/slide/Slide0.vue

@@ -0,0 +1,59 @@
+<script setup lang="ts">
+import SlideItem from '@/components/slide/SlideItem.vue'
+import { formatCount, hotFeed } from '@/mock/homeData'
+import { _checkImgUrl } from '@/utils/index'
+
+defineProps({ active: { type: Boolean, default: false } })
+</script>
+
+<template>
+  <SlideItem>
+    <div class="hot-feed">
+      <div v-for="item in hotFeed" :key="item.aweme_id" class="item">
+        <img :src="_checkImgUrl(item.video.cover.url_list[0])" class="cover" alt="" />
+        <div class="info">
+          <p class="desc">{{ item.desc }}</p>
+          <p class="meta">@{{ item.author.nickname }} · {{ formatCount(item.statistics.digg_count) }}赞</p>
+        </div>
+      </div>
+    </div>
+  </SlideItem>
+</template>
+
+<style scoped lang="less">
+.hot-feed {
+  height: 100%;
+  overflow-y: auto;
+  background: #000;
+  padding: 10rem;
+
+  .item {
+    margin-bottom: 12rem;
+    border-radius: 8rem;
+    overflow: hidden;
+    background: rgb(29, 29, 29);
+
+    .cover {
+      width: 100%;
+      height: 160rem;
+      object-fit: cover;
+    }
+
+    .info {
+      padding: 10rem 12rem;
+      color: white;
+
+      .desc {
+        margin: 0 0 6rem;
+        font-size: 14rem;
+      }
+
+      .meta {
+        margin: 0;
+        font-size: 12rem;
+        opacity: 0.6;
+      }
+    }
+  }
+}
+</style>

+ 58 - 0
src/views/home/slide/Slide2.vue

@@ -0,0 +1,58 @@
+<script setup lang="ts">
+import SlideItem from '@/components/slide/SlideItem.vue'
+import { followFeed, formatCount } from '@/mock/homeData'
+
+defineProps({ active: { type: Boolean, default: false } })
+</script>
+
+<template>
+  <SlideItem>
+    <div class="follow-feed">
+      <div v-for="item in followFeed" :key="item.aweme_id" class="item">
+        <img :src="item.video.cover.url_list[0]" class="cover" alt="" />
+        <div class="info">
+          <p class="desc">{{ item.desc }}</p>
+          <p class="meta">@{{ item.author.nickname }} · {{ formatCount(item.statistics.digg_count) }}赞</p>
+        </div>
+      </div>
+    </div>
+  </SlideItem>
+</template>
+
+<style scoped lang="less">
+.follow-feed {
+  height: 100%;
+  overflow-y: auto;
+  background: #000;
+  padding: 10rem;
+
+  .item {
+    margin-bottom: 12rem;
+    border-radius: 8rem;
+    overflow: hidden;
+    background: rgb(29, 29, 29);
+
+    .cover {
+      width: 100%;
+      height: 140rem;
+      object-fit: cover;
+    }
+
+    .info {
+      padding: 10rem 12rem;
+      color: white;
+
+      .desc {
+        margin: 0 0 6rem;
+        font-size: 14rem;
+      }
+
+      .meta {
+        margin: 0;
+        font-size: 12rem;
+        opacity: 0.6;
+      }
+    }
+  }
+}
+</style>

+ 12 - 0
src/views/home/slide/Slide4.vue

@@ -0,0 +1,12 @@
+<script setup lang="ts">
+import SlideItem from '@/components/slide/SlideItem.vue'
+import VideoFeed from '../components/VideoFeed.vue'
+
+defineProps({ active: { type: Boolean, default: false } })
+</script>
+
+<template>
+  <SlideItem>
+    <VideoFeed :active="active" />
+  </SlideItem>
+</template>

+ 47 - 0
src/views/my-guard/index.vue

@@ -0,0 +1,47 @@
+<template>
+  <div class="tab-page">
+    <div class="page-body">
+      <h1>我的守护</h1>
+      <p class="desc">守护记录开发中</p>
+    </div>
+    <BaseFooter />
+  </div>
+</template>
+
+<script setup lang="ts">
+import BaseFooter from '@/components/BaseFooter.vue'
+</script>
+
+<style scoped lang="less">
+.tab-page {
+  width: 100%;
+  height: 100%;
+  min-height: calc(var(--vh, 1vh) * 100);
+  background: var(--main-bg);
+  position: relative;
+  overflow: hidden;
+}
+
+.page-body {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: calc(var(--vh, 1vh) * 100 - 80rem);
+  padding: 20rem;
+  box-sizing: border-box;
+
+  h1 {
+    margin: 0 0 12rem;
+    font-size: 20rem;
+    color: #fff;
+    font-weight: 500;
+  }
+
+  .desc {
+    margin: 0;
+    font-size: 14rem;
+    color: rgba(255, 255, 255, 0.5);
+  }
+}
+</style>

+ 1 - 0
tsconfig.app.json

@@ -8,6 +8,7 @@
       "@/*": ["src/*"]
     },
     "allowJs": true,
+    "resolveJsonModule": true,
     "checkJs": false,
     "ignoreDeprecations": "6.0",
 

+ 7 - 0
vite.config.ts

@@ -9,6 +9,13 @@ export default defineConfig(({ mode }) => {
 
   return {
     plugins: [vue()],
+    css: {
+      preprocessorOptions: {
+        less: {
+          javascriptEnabled: true,
+        },
+      },
+    },
     resolve: {
       alias: {
         '@': path.resolve(__dirname, 'src'),