panhui 2 ani în urmă
părinte
comite
59eb7e04ba

+ 1 - 0
.env.development

@@ -1,4 +1,5 @@
 BASE_URL=/h5/
 VITE_BASE_URL=/h5/
 VITE_HTTP_BASE_URL=https://paimaide.izouma.com/
+VITE_HTTP_FILE_URL=https://zm-shorts.oss-cn-hangzhou.aliyuncs.com/
 #本地新建一个.env.development.local文件,用于覆盖上面的配置,不要直接修改.env.development文件

+ 13 - 0
package-lock.json

@@ -11,8 +11,10 @@
         "@vueuse/core": "^10.6.1",
         "axios": "^1.6.2",
         "less": "^4.2.0",
+        "mitt": "^3.0.1",
         "pinia": "^2.1.7",
         "qs": "^6.11.2",
+        "resolve-url": "^0.2.1",
         "vant": "^4.8.0",
         "vue": "^3.3.4",
         "vue-i18n": "^9.8.0",
@@ -2600,6 +2602,11 @@
         "node": "*"
       }
     },
+    "node_modules/mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
+    },
     "node_modules/ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -3264,6 +3271,12 @@
         "node": ">=4"
       }
     },
+    "node_modules/resolve-url": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
+      "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==",
+      "deprecated": "https://github.com/lydell/resolve-url#deprecated"
+    },
     "node_modules/reusify": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",

+ 3 - 0
package.json

@@ -12,11 +12,14 @@
   "dependencies": {
     "@vueuse/core": "^10.6.1",
     "axios": "^1.6.2",
+    "date-fns": "^2.30.0",
     "less": "^4.2.0",
     "mitt": "^3.0.1",
     "pinia": "^2.1.7",
     "qs": "^6.11.2",
+    "resolve-url": "^0.2.1",
     "vant": "^4.8.0",
+    "viplayer": "^1.1.8",
     "vue": "^3.3.4",
     "vue-i18n": "^9.8.0",
     "vue-router": "^4.2.5"

BIN
src/assets/icon-qiandao.png


BIN
src/assets/icon-yaoqing.png


BIN
src/assets/icon_bofang.png


BIN
src/assets/icon_jindoujilu.png


+ 53 - 4
src/components/SignInfo.vue

@@ -1,13 +1,48 @@
 <template>
-    <div class="sign-info w-[28px]">
-        <div class="sign-content">
+    <div class="sign-info w-[28px]" :class="{ isSigned: info.isSigned }">
+        <div class="sign-content animate__animated" :class="{ animate__flipInX: info.isSigned }">
             <div class="val text-xs text-black">+10</div>
-            <img src="@/assets/icon_jindou.png" class="w-[18px] h-[18px]" alt="" />
+
+            <div v-if="info.isSigned" class="w-[18px] h-[18px] flex items-center justify-center">
+                <van-icon name="success" size="16" class="mt-[3px]" color="#706557" />
+            </div>
+            <img src="@/assets/icon_jindou.png" v-else class="w-[18px] h-[18px]" alt="" />
         </div>
-        <div class="time text-[10px] text-[#130D00] text-center">今天</div>
+        <div class="time text-[10px] text-[#130D00] text-center">{{ time }}</div>
     </div>
 </template>
 
+<script setup>
+import '@/styles/animate.css'
+import { format, isToday, isTomorrow } from 'date-fns'
+import { ref, computed } from 'vue'
+
+const props = defineProps({
+    info: {
+        type: Object,
+        default: () => {
+            return {}
+        }
+    }
+})
+
+const time = computed(() => {
+    if (props.info.date) {
+        if (props.info.isSigned) {
+            return '已签'
+        }
+        if (isToday(new Date(props.info.date))) {
+            return '今天'
+        } else if (isTomorrow(new Date(props.info.date))) {
+            return '明天'
+        } else {
+            return format(new Date(props.info.date), 'MM.dd', new Date())
+        }
+    }
+    return ''
+})
+</script>
+
 <style lang="less" scoped>
 .sign-info {
     display: inline-block;
@@ -22,4 +57,18 @@
     justify-content: center;
     align-items: center;
 }
+
+.isSigned {
+    .sign-content {
+        background: #e1ccb0;
+
+        .val {
+            opacity: 0.5;
+        }
+    }
+
+    .time {
+        color: #00000080;
+    }
+}
 </style>

+ 137 - 23
src/components/VideoHistory.vue

@@ -1,31 +1,61 @@
 <template>
     <van-popup class="histroy-page" v-model:show="show" round position="bottom" :style="{ maxHeight: '70%' }">
-        <div class="title text-white flex overflow-hidden whitespace-nowrap">
-            <div class="van-ellipsis">{{ seriesInfo.title }}</div>
-            <div>-第{{ curEpInfo.episodeNum }}集</div>
-        </div>
-        <div class="btns">
-            <van-button type="primary" size="mini" round>1-5</van-button>
-            <van-button type="primary" size="mini" round>6-10</van-button>
+        <div class="sticky">
+            <div class="title text-white flex overflow-hidden whitespace-nowrap">
+                <div class="van-ellipsis">{{ seriesInfo.title }}</div>
+                <div>-第{{ curEpInfo.episodeNum }}集</div>
+            </div>
+            <div class="btns">
+                <van-button
+                    type="primary"
+                    :color="i === page ? '#7968FB' : '#3C328A'"
+                    size="mini"
+                    round
+                    v-for="i in curPage"
+                    :key="i"
+                    @click="changePage(i)"
+                    >{{ 15 * (i - 1) + 1 + '-' + 15 * i }}</van-button
+                >
+
+                <van-button
+                    @click="changePage(curPage + 1)"
+                    :color="curPage + 1 === page ? '#7968FB' : '#3C328A'"
+                    type="primary"
+                    size="mini"
+                    round
+                >
+                    {{ 15 * curPage + 1 + '-' + (15 * curPage + laNum) }}
+                </van-button>
+            </div>
         </div>
 
-        <div class="video-list">
-            <div class="video-info">
-                <van-image
-                    width="calc(33vw - 20.66px)"
-                    height="140"
-                    fit="cover"
-                    radius="12"
-                    src="https://p9.itc.cn/images01/20200808/f27ca5bb0a1c4e2d9b9df893f273a83c.jpeg"
-                />
+        <div class="video-list px-[18px] pt-[15px]">
+            <div
+                class="video-info flex flex-col items-center"
+                :class="{ active: item.episodeNum === curEpInfo.episodeNum }"
+                v-for="(item, index) in episodesList"
+                :key="index"
+                @click="play(item)"
+            >
+                <div class="video-content">
+                    <van-image block width="calc(33vw - 20.66px)" height="140" fit="cover" radius="12" :src="file" />
+                    <div class="video-box" v-if="item.episodeNum === curEpInfo.episodeNum">
+                        <img src="@/assets/icon_bofang.png" alt="" />
+                        <div>正在播放</div>
+                    </div>
+                </div>
+
+                <div class="text-xs text-white pt-[8px] pb-[10px]">第{{ item.episodeNum }}集</div>
             </div>
         </div>
     </van-popup>
 </template>
 
 <script setup>
-import { ref } from 'vue'
+import { ref, computed } from 'vue'
 import { http } from '@/plugins/http'
+import resolveUrl from 'resolve-url'
+const fileURL = import.meta.env.VITE_HTTP_FILE_URL
 const show = ref(false)
 const props = defineProps({
     curEpInfo: {
@@ -41,26 +71,55 @@ const props = defineProps({
         }
     }
 })
-// const emit = defineEmits(['update:show'])
+const emit = defineEmits(['playVideo'])
+const page = ref(0)
 function changeShow(val) {
     show.value = val
+
     if (val) {
-        getEpisodes(1)
+        page.value = Math.ceil(props.curEpInfo.episodeNum / 15)
+        getEpisodes(page.value)
     }
 }
 defineExpose({ changeShow })
 
 const total = ref(0)
+const episodesList = ref([])
 function getEpisodes(page = 1) {
     http.get('/api/episodes', {
         seriesId: props.seriesInfo.id,
         orderBy: 'episodeNum',
         order: 'episodeNum',
-        page: page
+        page: page,
+        pageSize: 15
     }).then(res => {
         total.value = res.meta.total
+        episodesList.value = res.data
     })
 }
+
+const curPage = computed(() => {
+    return Math.floor(total.value / 15)
+})
+const laNum = computed(() => {
+    return total.value % 15
+})
+
+const file = computed(() => {
+    return resolveUrl(fileURL, props.seriesInfo.cover)
+})
+
+function changePage(_page) {
+    if (page.value !== _page) {
+        page.value = _page
+        getEpisodes(_page)
+    }
+}
+
+function play(playInfo) {
+    show.value = false
+    emit('playVideo', playInfo.episodeNum)
+}
 </script>
 
 <style lang="less" scoped>
@@ -68,16 +127,71 @@ function getEpisodes(page = 1) {
     background: linear-gradient(180deg, rgba(17, 19, 30, 0.8) 0%, rgba(17, 19, 30, 0) 100%);
     border-radius: 16px 16px 0px 0px;
     backdrop-filter: blur(4px);
-    padding: 21px 18px;
+    // padding: 21px 18px;
+    padding-bottom: 21px;
+    .sticky {
+        position: sticky;
+        top: 0;
+        z-index: 20;
+        background: rgba(17, 19, 30, 0.8);
+        padding: 21px 18px 10px;
+    }
 }
 
 .btns {
     .van-button {
         width: 72px;
         font-size: 14px;
+        margin-right: 16px;
+        margin-bottom: 5px;
+    }
+    // .van-button + .van-button {
+    //     margin-left: 16px;
+    // }
+}
+
+.video-info {
+    width: calc(33vw - 20.66px);
+    display: inline-flex;
+    margin-right: 13px;
+    &:nth-child(3n) {
+        margin-right: 0;
+    }
+    .video-content {
+        border-radius: 12px;
+        border: 1px solid #7968fb;
+        overflow: hidden;
     }
-    .van-button + .van-button {
-        margin-left: 16px;
+
+    &.active {
+        .video-content {
+            position: relative;
+            .video-box {
+                position: absolute;
+                top: 0;
+                left: 0;
+                right: 0;
+                bottom: 0;
+                background: rgba(17, 19, 30, 0.5);
+
+                display: flex;
+                flex-direction: column;
+                justify-content: center;
+                align-items: center;
+
+                img {
+                    width: 24px;
+                    height: 24px;
+                }
+                div {
+                    font-size: 12px;
+                    font-weight: 400;
+                    color: #ffffff;
+                    line-height: 18px;
+                    text-shadow: 0px 1px 1px rgba(0, 0, 0, 0.5);
+                }
+            }
+        }
     }
 }
 </style>

+ 21 - 3
src/components/VideoLine.vue

@@ -1,12 +1,12 @@
 <template>
     <div class="video-info flex" @click="goDetail">
-        <van-image width="110" height="70" fit="cover" radius="12" :src="info.cover" />
+        <van-image width="110" height="70" fit="cover" radius="12" :src="img" />
 
         <div class="content">
             <div class="title text-white van-multi-ellipsis--l2 text-sm">{{ info.title }}</div>
-            <div class="text-[#00FFE4] text-xs" v-if="history">观看至第1集</div>
+            <div class="text-[#00FFE4] text-xs" v-if="history">观看至第{{ episodeNum }}集</div>
             <div class="text text-xs text-[#61657A] flex justify-between">
-                <span>虐恋·{{ info.totalEpisodes }}集</span>
+                <span>{{ category ? category + '·' : '' }}{{ info.totalEpisodes }}集</span>
                 <span> {{ info.playCount }}播放</span>
             </div>
         </div>
@@ -15,6 +15,10 @@
 
 <script setup>
 import { useRouter } from 'vue-router'
+import resolveUrl from 'resolve-url'
+import { computed } from 'vue'
+
+const fileURL = import.meta.env.VITE_HTTP_FILE_URL
 const props = defineProps({
     history: {
         type: Boolean,
@@ -25,7 +29,21 @@ const props = defineProps({
         default: () => {
             return {}
         }
+    },
+    episodeNum: {
+        type: Number,
+        default: 0
+    }
+})
+
+const category = computed(() => {
+    if (props.info.categories && props.info.categories.length > 0) {
+        return props.info.categories[0].name
     }
+    return ''
+})
+const img = computed(() => {
+    return resolveUrl(fileURL, props.info.cover)
 })
 const router = useRouter()
 function goDetail() {

+ 27 - 3
src/components/VideoSmall.vue

@@ -1,26 +1,50 @@
 <template>
     <div @click="goDetail" class="video-info inline-block">
         <div class="top relative">
-            <van-image width="calc(33vw - 21.33px)" height="140" fit="cover" radius="12" :src="info.cover" />
+            <van-image width="calc(33vw - 21.33px)" height="140" fit="cover" radius="12" :src="img" />
             <div class="top-text text-[10px] text-white p-[8px] absolute bottom-0 left-0 right-0">
-                {{ info.playCount }}播放
+                {{ history ? `观看至第${episodeNum}集` : `${info.playCount}播放` }}
             </div>
         </div>
         <div class="content">
             <div class="text-sm text-white van-ellipsis">{{ info.title }}</div>
-            <div class="text-xs text-[#61657A]">虐恋·{{ info.totalEpisodes }}集</div>
+            <div class="text-xs text-[#61657A]">{{ category ? category + '·' : '' }}{{ info.totalEpisodes }}集</div>
         </div>
     </div>
 </template>
 <script setup>
 import { useRouter } from 'vue-router'
+import resolveUrl from 'resolve-url'
+import { computed } from 'vue'
+
+const fileURL = import.meta.env.VITE_HTTP_FILE_URL
+
 const props = defineProps({
     info: {
         type: Object,
         default: () => {
             return {}
         }
+    },
+    history: {
+        type: Boolean,
+        default: false
+    },
+    episodeNum: {
+        type: Number,
+        default: 0
+    }
+})
+
+const img = computed(() => {
+    return resolveUrl(fileURL, props.info.cover)
+})
+
+const category = computed(() => {
+    if (props.info.categories && props.info.categories.length > 0) {
+        return props.info.categories[0].name
     }
+    return ''
 })
 
 const router = useRouter()

+ 3 - 2
src/locales/zh-tw.json

@@ -1,6 +1,6 @@
 {
   "mine": {
-    "wallet": "充值記錄",
+    "wallet": "金豆記錄",
     "record": "消費記錄",
     "connect": "聯系客服",
     "history": "觀看歷史",
@@ -10,7 +10,8 @@
     "money": "我的金豆",
     "nickname": "用戶昵稱",
     "id": "看官ID",
-    "mine": "我的"
+    "mine": "我的",
+    "notLogin": "未登录"
   },
   "zouma": "走馬短劇",
   "home": {

+ 1 - 3
src/router/index.js

@@ -32,9 +32,7 @@ const router = createRouter({
                     path: '/mine',
                     name: 'mine',
                     component: () => import('@/views/MineView.vue'),
-                    meta: {
-                        allowGuest: true
-                    }
+                    meta: {}
                 }
             ]
         },

+ 1 - 1
src/stores/user.js

@@ -70,7 +70,7 @@ export const useUserStore = defineStore('user', () => {
     }
     const get = async () => {
         if (!http.token.value) return Promise.reject()
-        return http.get('/user/my').then(res => {
+        return http.get('/api/users/my').then(res => {
             user.value = res
         })
     }

+ 18 - 2
src/views/HistoryView.vue

@@ -13,8 +13,16 @@
             </van-sticky>
 
             <div class="pb-6 px-[18px]">
-                <div class="hot-list">
-                    <video-line history v-for="i in 10" :key="i"></video-line>
+                <van-empty description="暂无记录" v-if="histories.length == 0 && !loading" />
+
+                <div class="hot-list" v-else>
+                    <video-line
+                        v-for="(item, index) in histories"
+                        :key="index"
+                        :info="item.series"
+                        history
+                        :episodeNum="item.episode.episodeNum"
+                    ></video-line>
                 </div>
             </div>
         </div>
@@ -24,6 +32,7 @@
 import VideoLine from '../components/VideoLine.vue'
 import { useRouter } from 'vue-router'
 import { ref } from 'vue'
+import { http } from '@/plugins/http'
 
 const router = useRouter()
 function back() {
@@ -34,6 +43,13 @@ const isFixed = ref(false)
 function changeSticky(fixed) {
     isFixed.value = fixed
 }
+
+const histories = ref([])
+const loading = ref(true)
+http.get('/api/playHistories').then(res => {
+    histories.value = res.data
+    loading.value = false
+})
 </script>
 
 <style lang="less" scoped>

+ 16 - 9
src/views/HomeView.vue

@@ -13,7 +13,7 @@
             </van-swipe-item>
         </van-swipe>
 
-        <div class="py-6 px-[18px]">
+        <div class="py-6 px-[18px]" v-if="seriesList.length > 0">
             <div class="news-title text-white text-lg AlimamaShuHeiTi relative">
                 <span class="z-[1] relative">{{ $t('home.new') }}</span>
                 <img class="w-78px] h-[16px] absolute bottom-0 left-0 z-0" src="@/assets/png-xiantiao1.png" alt="" />
@@ -23,7 +23,7 @@
             </div>
         </div>
 
-        <div class="hot">
+        <div class="hot" v-if="hotList.length > 0">
             <div class="hot-title">{{ $t('home.hot') }}</div>
             <img src="@/assets/hotImg.png" alt="" class="hot-img" />
             <div class="hot-list">
@@ -47,7 +47,7 @@
                 <img class="w-[78px] h-[16px] absolute bottom-0 left-0 z-0" src="@/assets/png-xiantiao1.png" alt="" />
             </div>
             <div class="perfect-list">
-                <video-small v-for="(item, index) in seriesList" :key="index" :info="item"></video-small>
+                <video-small v-for="(item, index) in allList" :key="index" :info="item"></video-small>
             </div>
         </div>
     </div>
@@ -61,9 +61,10 @@ import { ref } from 'vue'
 
 const seriesList = ref([])
 const hotList = ref([])
+const allList=ref([])
 http.get('/api/series').then(res => {
-    seriesList.value = res.data
-    hotList.value = res.data.slice(0, 3)
+    allList.value = res.data
+    // hotList.value = res.data.slice(0, 3)
 })
 
 async function getSeries(data = {}) {
@@ -78,10 +79,16 @@ http.get('/api/categories').then(res => {
         let data = await getSeries({
             categories: item.id
         })
-        categories.value.push({
-            ...item,
-            datas: data
-        })
+        if (item.id === 6) {
+            seriesList.value = data
+        } else if (item.id === 5) {
+            hotList.value = data.slice(0, 3)
+        } else {
+            categories.value.push({
+                ...item,
+                datas: data
+            })
+        }
     })
 })
 </script>

+ 59 - 6
src/views/MineView.vue

@@ -7,8 +7,8 @@
             <div class="user-box flex px-[18px] pb-[28px]">
                 <van-image :src="defaultImg" width="60" height="60" type="contain" />
                 <div class="user-info ml-[12px]">
-                    <div class="text-[20px] text-white">{{ $t('mine.nickname') }}</div>
-                    <div class="text-xs text-white">{{ $t('mine.id') }}: 338392</div>
+                    <div class="text-[20px] text-white">{{ user ? user.username : $t('mine.notLogin') }}</div>
+                    <div class="text-xs text-white" v-if="user">{{ $t('mine.id') }}:{{ user.id }}</div>
                 </div>
             </div>
 
@@ -31,10 +31,25 @@
 
                     <div class="sign-box">
                         <div class="sign-list">
-                            <sign-info v-for="i in 7" :key="i"></sign-info>
+                            <sign-info v-for="(item, index) in signInfos" :info="item" :key="index"></sign-info>
                         </div>
                         <div class="sign-btn w-[220px] mx-auto">
-                            <van-button round block size="small" color="linear-gradient(180deg, #FF7340 , #FF3E3E )">
+                            <van-button
+                                round
+                                block
+                                size="small"
+                                color="linear-gradient(180deg, #FF8D63 , #FF7459 )"
+                                v-if="todayInfo && todayInfo.isSigned"
+                                >今日已簽到</van-button
+                            >
+                            <van-button
+                                round
+                                block
+                                size="small"
+                                @click="sign"
+                                v-else
+                                color="linear-gradient(180deg, #FF7340 , #FF3E3E )"
+                            >
                                 {{ $t('mine.sign') }}</van-button
                             >
                         </div>
@@ -62,7 +77,7 @@
                     </van-cell>
                     <van-cell :title="$t('mine.wallet')" :to="{ path: '/wallet' }" is-link>
                         <template #icon>
-                            <img src="@/assets/icon_chongzhijilu.png" class="w-[24px] h-[24px] mr-[10px]" alt="" />
+                            <img src="@/assets/icon_jindoujilu.png" class="w-[24px] h-[24px] mr-[10px]" alt="" />
                         </template>
                     </van-cell>
                     <van-cell :title="$t('mine.record')" :to="{ path: '/record' }" is-link>
@@ -90,7 +105,12 @@ import defaultImg from '@/assets/png-touxiang.png'
 import SignInfo from '@/components/SignInfo.vue'
 import BuyPage from '@/components/BuyPage.vue'
 import ConnectPage from '@/components/ConnectPage.vue'
-import { ref } from 'vue'
+import { ref, onMounted } from 'vue'
+import { useUserStore } from '../stores/user'
+import { http } from '@/plugins/http'
+import { showNotify } from 'vant'
+import { useI18n } from 'vue-i18n'
+import { isSameDay } from 'date-fns'
 
 const buyPageRef = ref(null)
 function buy() {
@@ -101,6 +121,39 @@ const connectPageRef = ref(null)
 function connect() {
     connectPageRef.value.changeShow(true)
 }
+
+const { user } = useUserStore()
+
+onMounted(() => {
+    if (user) {
+        getSign()
+    }
+})
+
+const signInfos = ref([])
+const todayInfo = ref({})
+function getSign() {
+    http.get('/api/signInRecords').then(res => {
+        signInfos.value = res
+        todayInfo.value = res.find(item => {
+            return isSameDay(new Date(item.date), new Date())
+        })
+    })
+}
+
+const { t } = useI18n()
+function sign() {
+    http.post('/api/signInRecords')
+        .then(res => {
+            getSign()
+            showNotify({ type: 'success', message: '签到成功' })
+        })
+        .catch(e => {
+            if (e && e.message) {
+                showNotify({ type: 'warning', message: e.message })
+            }
+        })
+}
 </script>
 
 <style lang="less" scoped>

+ 112 - 11
src/views/VideoView.vue

@@ -1,9 +1,24 @@
 <template>
-    <div class="view van-safe-area-bottom">
+    <div class="view van-safe-area-bottom h-full">
         <van-sticky @change="changeSticky">
-            <van-nav-bar :class="{ isFixed: isFixed }" title="第一集" left-text="" left-arrow @click-left="back" />
+            <van-nav-bar
+                :class="{ isFixed: isFixed }"
+                :title="`第${curEpInfo.episodeNum}集`"
+                left-text=""
+                left-arrow
+                @click-left="back"
+            />
         </van-sticky>
 
+        <!-- <van-image
+            width="100%"
+            height="calc(100vh - 95px - env(safe-area-inset-bottom) - env(safe-area-inset-top))"
+            :src="poster"
+            type="cover"
+        /> -->
+
+        <video ref="videoRef" controls muted class="video" :src="curEpInfo.playUrl" :poster="poster"></video>
+
         <div class="right-fixed">
             <div class="right-btn" @click="collect">
                 <img
@@ -30,21 +45,32 @@
             >
         </div>
 
-        <video-history ref="videoHistoryRef" :seriesInfo="seriesInfo" :curEpInfo="curEpInfo"></video-history>
+        <video-history
+            ref="videoHistoryRef"
+            @playVideo="playVideo"
+            :seriesInfo="seriesInfo"
+            :curEpInfo="curEpInfo"
+        ></video-history>
     </div>
 </template>
 
 <script setup>
 import { useRouter } from 'vue-router'
-import { ref } from 'vue'
+import { ref, reactive, onUnmounted, onMounted } from 'vue'
 import moreImg from '@/assets/icon_inter_white.png'
 import VideoHistory from '../components/VideoHistory.vue'
 import { http } from '@/plugins/http'
-import { useRoute } from 'vue-router'
+import { useRoute, onBeforeRouteLeave } from 'vue-router'
 import '@/styles/animate.css'
 import collectActiveImg from '@/assets/icon_dianliang.png'
 import collectImg from '@/assets/icon_weidianliang.png'
 import { showNotify } from 'vant'
+import 'viplayer/dist/index.css'
+import { videoPlay } from 'viplayer'
+import resolveUrl from 'resolve-url'
+import { nextTick } from 'vue'
+
+const poster = ref('')
 
 const router = useRouter()
 function back() {
@@ -61,22 +87,55 @@ const seriesId = ref(0)
 if (route.query.id) {
     seriesId.value = route.query.id
 }
+
 const seriesInfo = ref({})
+const fileURL = import.meta.env.VITE_HTTP_FILE_URL
 http.get('/api/series/' + seriesId.value).then(res => {
     seriesInfo.value = res
+    // options.value.poster = re  solveUrl(fileURL, res.cover)
+    poster.value = resolveUrl(fileURL, res.cover)
+    console.log(poster.value)
 })
 
-const curEpInfo = ref({})
-http.get('/api/episodes', {
-    seriesId: seriesId.value,
-    orderBy: 'episodeNum',
-    order: 'episodeNum'
+http.get('/api/playHistories', {
+    seriesId: seriesId.value
 }).then(res => {
     if (res.data.length > 0) {
-        curEpInfo.value = res.data[0]
+        getEpisodes(0, res.data[0].duration, res.data[0].episodeId)
+    } else {
+        getEpisodes()
     }
 })
 
+const curEpInfo = ref({})
+async function getEpisodes(episodeNum = 1, duration = 0, episodeId = 0) {
+    let data = {
+        seriesId: seriesId.value,
+        orderBy: 'episodeNum',
+        order: 'episodeNum'
+    }
+    if (!episodeNum) {
+        data.id = episodeId
+    } else {
+        data.episodeNum = episodeNum
+    }
+    const res = await http.get('/api/episodes', data)
+    if (res.data.length > 0) {
+        await play(res.data[0].id, duration)
+    }
+}
+
+async function play(curEpId = 0, duration = 0) {
+    await http.get('/api/episodes/' + curEpId).then(res => {
+        curEpInfo.value = res
+        nextTick(() => {
+            videoRef.value.currentTime = duration
+            videoRef.value.play()
+        })
+        // poster.value = resolveUrl(fileURL, res.cover)
+    })
+}
+
 http.get('/api/collections/' + seriesId.value).then(res => {
     isCollected.value = true
 })
@@ -115,9 +174,46 @@ const videoHistoryRef = ref(null)
 function showVideoHistory() {
     videoHistoryRef.value.changeShow(true)
 }
+
+const videoRef = ref(null)
+onBeforeRouteLeave((to, from, next) => {
+    saveHistories(videoRef.value.currentTime).finally(() => {
+        next()
+    })
+})
+
+async function saveHistories(time) {
+    await http.post(
+        '/api/playHistories',
+        {
+            seriesId: seriesId.value,
+            episodeId: curEpInfo.value.id,
+            duration: time
+        },
+        { body: 'json' }
+    )
+}
+
+onMounted(() => {
+    nextTick(() => {
+        videoRef.value.addEventListener('ended', () => {
+            console.log('播放结束')
+            getEpisodes(curEpInfo.value.episodeNum + 1).then(() => {
+                saveHistories(videoRef.value.currentTime)
+            })
+        })
+    })
+})
+
+function playVideo(num) {
+    getEpisodes(num, 0)
+}
 </script>
 
 <style lang="less" scoped>
+.van-image {
+    --van-image-placeholder-background: transparent;
+}
 .van-nav-bar {
     --van-nav-bar-background: transparent;
     --van-border-width: 0;
@@ -161,4 +257,9 @@ function showVideoHistory() {
         }
     }
 }
+
+.video {
+    width: 100%;
+    height: calc(100vh - 95px - env(safe-area-inset-bottom) - env(safe-area-inset-top));
+}
 </style>

+ 52 - 11
src/views/WalletView.vue

@@ -13,18 +13,21 @@
             </van-sticky>
 
             <div class="py-[14px] px-[18px]">
-                <div
-                    v-for="i in 10"
-                    :key="i"
-                    class="wallet bg-[#20223C] flex h-[68px] items-center px-[14px] rounded-lg"
-                >
-                    <img class="w-[36px] h-[36px]" src="@/assets/icon-tixian.png" alt="" />
-                    <div class="flex-1 ml-[12px]">
-                        <div class="text-sm text-white">充值成功</div>
-                        <div class="text-xs text-[#797A8A]">11-20 12:10</div>
+                <van-empty description="暂无记录" v-if="list.length == 0 && !loading" />
+                <template v-else>
+                    <div
+                        v-for="(item, index) in list"
+                        :key="index"
+                        class="wallet bg-[#20223C] flex h-[68px] items-center px-[14px] rounded-lg"
+                    >
+                        <img class="w-[36px] h-[36px]" :src="getIcon(item.type)" alt="" />
+                        <div class="flex-1 ml-[12px]">
+                            <div class="text-sm text-white">{{ item.description }}</div>
+                            <div class="text-xs text-[#797A8A]">{{ formateDay(item.createdAt) }}</div>
+                        </div>
+                        <div class="text-[18px] text-white AlimamaShuHeiTi">{{ getAmount(item.amount) }}</div>
                     </div>
-                    <div class="text-[18px] text-white AlimamaShuHeiTi">+19.90</div>
-                </div>
+                </template>
             </div>
         </div>
     </div>
@@ -32,6 +35,11 @@
 <script setup>
 import { useRouter } from 'vue-router'
 import { ref } from 'vue'
+import { http } from '@/plugins/http'
+import { format } from 'date-fns'
+import tixianImg from '@/assets/icon-tixian.png'
+import qiandaoImg from '@/assets/icon-qiandao.png'
+import yaoqingImg from '@/assets/icon-yaoqing.png'
 
 const router = useRouter()
 function back() {
@@ -42,6 +50,39 @@ const isFixed = ref(false)
 function changeSticky(fixed) {
     isFixed.value = fixed
 }
+
+const list = ref([])
+const loading = ref(true)
+http.get('/api/balanceRecords').then(res => {
+    list.value = res.data
+    loading.value = false
+})
+
+function formateDay(day) {
+    console.log(new Date(day))
+    return format(new Date(day), 'MM-dd hh:mm', new Date())
+}
+
+function getAmount(amount) {
+    let _amount = Number(amount)
+    return (_amount > 0 ? '+' : '') + _amount
+}
+
+function getIcon(type) {
+    let img = tixianImg
+    switch (type) {
+        case 'reward':
+            img = qiandaoImg
+            break
+        case 'recharge':
+            img = tixianImg
+            break
+        case 'purchase':
+            img = yaoqingImg
+            break
+    }
+    return img
+}
 </script>
 
 <style lang="less" scoped>

+ 27 - 4
src/views/WatchingView.vue

@@ -3,7 +3,7 @@
         <img src="@/assets/png-zhuiju-bg.png" class="top-bg !absolute w-full h-[200px] top-0 left-0 !z-0" alt="" />
         <div class="watching-content">
             <div class="title p-[18px] text-white text-[26px] AlimamaShuHeiTi">{{ $t('watching.watching') }}</div>
-            <div class="pb-6 px-[18px]">
+            <div class="pb-6 px-[18px]" v-if="histories.length > 0">
                 <div class="news-title text-white text-lg AlimamaShuHeiTi relative">
                     <span class="z-[1] relative">{{ $t('watching.recently') }}</span>
                     <img
@@ -13,7 +13,13 @@
                     />
                 </div>
                 <div class="news-list">
-                    <video-small v-for="i in 10" :key="i"></video-small>
+                    <video-small
+                        v-for="(item, index) in histories"
+                        :key="index"
+                        :info="item.series"
+                        history
+                        :episodeNum="item.episode.episodeNum"
+                    ></video-small>
                 </div>
             </div>
 
@@ -26,8 +32,16 @@
                         alt=""
                     />
                 </div>
-                <div class="hot-list">
-                    <video-line history v-for="i in 10" :key="i"></video-line>
+                <van-empty description="暂无记录" v-if="histories.length == 0 && !loading" />
+
+                <div class="hot-list" v-else>
+                    <video-line
+                        v-for="(item, index) in histories"
+                        :key="index"
+                        :info="item.series"
+                        history
+                        :episodeNum="item.episode.episodeNum"
+                    ></video-line>
                 </div>
             </div>
         </div>
@@ -36,6 +50,15 @@
 <script setup>
 import VideoSmall from '../components/VideoSmall.vue'
 import VideoLine from '../components/VideoLine.vue'
+import { http } from '@/plugins/http'
+import { ref } from 'vue'
+
+const histories = ref([])
+const loading = ref(true)
+http.get('/api/playHistories').then(res => {
+    histories.value = res.data
+    loading.value = false
+})
 </script>
 
 <style lang="less" scoped>

+ 160 - 2
yarn.lock

@@ -17,6 +17,18 @@
   resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz"
   integrity sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==
 
+"@babel/parser@^7.23.5":
+  version "7.23.5"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.5.tgz#37dee97c4752af148e1d38c34b856b2507660563"
+  integrity sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==
+
+"@babel/runtime@^7.21.0":
+  version "7.23.5"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.5.tgz#11edb98f8aeec529b82b211028177679144242db"
+  integrity sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==
+  dependencies:
+    regenerator-runtime "^0.14.0"
+
 "@esbuild/android-arm64@0.18.20":
   version "0.18.20"
   resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622"
@@ -294,6 +306,16 @@
   resolved "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.5.0.tgz"
   integrity sha512-a2WSpP8X8HTEww/U00bU4mX1QpLINNuz/2KMNpLsdu3BzOpak3AGI1CJYBTXcc4SPhaD0eNRUp7IyQK405L5dQ==
 
+"@vue/compiler-core@3.3.10":
+  version "3.3.10"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.3.10.tgz#9ca4123a1458df43db641aaa8b7d1e636aa22545"
+  integrity sha512-doe0hODR1+i1menPkRzJ5MNR6G+9uiZHIknK3Zn5OcIztu6GGw7u0XUzf3AgB8h/dfsZC9eouzoLo3c3+N/cVA==
+  dependencies:
+    "@babel/parser" "^7.23.5"
+    "@vue/shared" "3.3.10"
+    estree-walker "^2.0.2"
+    source-map-js "^1.0.2"
+
 "@vue/compiler-core@3.3.8":
   version "3.3.8"
   resolved "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.8.tgz"
@@ -304,6 +326,14 @@
     estree-walker "^2.0.2"
     source-map-js "^1.0.2"
 
+"@vue/compiler-dom@3.3.10":
+  version "3.3.10"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.3.10.tgz#183811252be6aff4ac923f783124bb1590301907"
+  integrity sha512-NCrqF5fm10GXZIK0GrEAauBqdy+F2LZRt3yNHzrYjpYBuRssQbuPLtSnSNjyR9luHKkWSH8we5LMB3g+4z2HvA==
+  dependencies:
+    "@vue/compiler-core" "3.3.10"
+    "@vue/shared" "3.3.10"
+
 "@vue/compiler-dom@3.3.8":
   version "3.3.8"
   resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.8.tgz"
@@ -312,6 +342,22 @@
     "@vue/compiler-core" "3.3.8"
     "@vue/shared" "3.3.8"
 
+"@vue/compiler-sfc@3.3.10":
+  version "3.3.10"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.3.10.tgz#8eb97d42f276089ec58fd0565ef3a813bceeaa87"
+  integrity sha512-xpcTe7Rw7QefOTRFFTlcfzozccvjM40dT45JtrE3onGm/jBLZ0JhpKu3jkV7rbDFLeeagR/5RlJ2Y9SvyS0lAg==
+  dependencies:
+    "@babel/parser" "^7.23.5"
+    "@vue/compiler-core" "3.3.10"
+    "@vue/compiler-dom" "3.3.10"
+    "@vue/compiler-ssr" "3.3.10"
+    "@vue/reactivity-transform" "3.3.10"
+    "@vue/shared" "3.3.10"
+    estree-walker "^2.0.2"
+    magic-string "^0.30.5"
+    postcss "^8.4.32"
+    source-map-js "^1.0.2"
+
 "@vue/compiler-sfc@3.3.8":
   version "3.3.8"
   resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.8.tgz"
@@ -328,6 +374,14 @@
     postcss "^8.4.31"
     source-map-js "^1.0.2"
 
+"@vue/compiler-ssr@3.3.10":
+  version "3.3.10"
+  resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.3.10.tgz#5a1b14a358cb3960a4edbce0ade90548e452fcaa"
+  integrity sha512-12iM4jA4GEbskwXMmPcskK5wImc2ohKm408+o9iox3tfN9qua8xL0THIZtoe9OJHnXP4eOWZpgCAAThEveNlqQ==
+  dependencies:
+    "@vue/compiler-dom" "3.3.10"
+    "@vue/shared" "3.3.10"
+
 "@vue/compiler-ssr@3.3.8":
   version "3.3.8"
   resolved "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.8.tgz"
@@ -349,6 +403,17 @@
     eslint-config-prettier "^8.8.0"
     eslint-plugin-prettier "^5.0.0"
 
+"@vue/reactivity-transform@3.3.10":
+  version "3.3.10"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.3.10.tgz#b045776cc954bb57883fd305db7a200d42993768"
+  integrity sha512-0xBdk+CKHWT+Gev8oZ63Tc0qFfj935YZx+UAynlutnrDZ4diFCVFMWixn65HzjE3S1iJppWOo6Tt1OzASH7VEg==
+  dependencies:
+    "@babel/parser" "^7.23.5"
+    "@vue/compiler-core" "3.3.10"
+    "@vue/shared" "3.3.10"
+    estree-walker "^2.0.2"
+    magic-string "^0.30.5"
+
 "@vue/reactivity-transform@3.3.8":
   version "3.3.8"
   resolved "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.8.tgz"
@@ -360,6 +425,13 @@
     estree-walker "^2.0.2"
     magic-string "^0.30.5"
 
+"@vue/reactivity@3.3.10":
+  version "3.3.10"
+  resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.3.10.tgz#78fe3da319276d9e6d0f072037532928c472a287"
+  integrity sha512-H5Z7rOY/JLO+e5a6/FEXaQ1TMuOvY4LDVgT+/+HKubEAgs9qeeZ+NhADSeEtrNQeiKLDuzeKc8v0CUFpB6Pqgw==
+  dependencies:
+    "@vue/shared" "3.3.10"
+
 "@vue/reactivity@3.3.8":
   version "3.3.8"
   resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.8.tgz"
@@ -367,6 +439,14 @@
   dependencies:
     "@vue/shared" "3.3.8"
 
+"@vue/runtime-core@3.3.10":
+  version "3.3.10"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.3.10.tgz#d7b78c5c0500b856cf9447ef81d4a1b1438fd5bb"
+  integrity sha512-DZ0v31oTN4YHX9JEU5VW1LoIVgFovWgIVb30bWn9DG9a7oA415idcwsRNNajqTx8HQJyOaWfRKoyuP2P2TYIag==
+  dependencies:
+    "@vue/reactivity" "3.3.10"
+    "@vue/shared" "3.3.10"
+
 "@vue/runtime-core@3.3.8":
   version "3.3.8"
   resolved "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.8.tgz"
@@ -375,6 +455,15 @@
     "@vue/reactivity" "3.3.8"
     "@vue/shared" "3.3.8"
 
+"@vue/runtime-dom@3.3.10":
+  version "3.3.10"
+  resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.3.10.tgz#130dfffb8fee8051671aaf80c5104d2020544950"
+  integrity sha512-c/jKb3ny05KJcYk0j1m7Wbhrxq7mZYr06GhKykDMNRRR9S+/dGT8KpHuNQjv3/8U4JshfkAk6TpecPD3B21Ijw==
+  dependencies:
+    "@vue/runtime-core" "3.3.10"
+    "@vue/shared" "3.3.10"
+    csstype "^3.1.2"
+
 "@vue/runtime-dom@3.3.8":
   version "3.3.8"
   resolved "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.8.tgz"
@@ -384,6 +473,14 @@
     "@vue/shared" "3.3.8"
     csstype "^3.1.2"
 
+"@vue/server-renderer@3.3.10":
+  version "3.3.10"
+  resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.3.10.tgz#f23d151f0e5021ebdc730052d9934c9178486742"
+  integrity sha512-0i6ww3sBV3SKlF3YTjSVqKQ74xialMbjVYGy7cOTi7Imd8ediE7t72SK3qnvhrTAhOvlQhq6Bk6nFPdXxe0sAg==
+  dependencies:
+    "@vue/compiler-ssr" "3.3.10"
+    "@vue/shared" "3.3.10"
+
 "@vue/server-renderer@3.3.8":
   version "3.3.8"
   resolved "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.8.tgz"
@@ -392,6 +489,11 @@
     "@vue/compiler-ssr" "3.3.8"
     "@vue/shared" "3.3.8"
 
+"@vue/shared@3.3.10":
+  version "3.3.10"
+  resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.3.10.tgz#1583a8d85a957d8b819078c465d2a11db7914b2f"
+  integrity sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw==
+
 "@vue/shared@3.3.8", "@vue/shared@^3.0.0":
   version "3.3.8"
   resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz"
@@ -661,6 +763,13 @@ csstype@^3.1.2:
   resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz"
   integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
 
+date-fns@^2.30.0:
+  version "2.30.0"
+  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
+  integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
+  dependencies:
+    "@babel/runtime" "^7.21.0"
+
 debug@^3.2.6:
   version "3.2.7"
   resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"
@@ -1137,6 +1246,11 @@ hasown@^2.0.0:
   dependencies:
     function-bind "^1.1.2"
 
+hls.js@^1.4.10:
+  version "1.4.12"
+  resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.4.12.tgz#2022daa29d10c662387d80a5297f8330f8ef5ee2"
+  integrity sha512-1RBpx2VihibzE3WE9kGoVCtrhhDWTzydzElk/kyRbEOLnb1WIE+3ZabM/L8BqKFTCL3pUy4QzhXgD1Q6Igr1JA==
+
 human-signals@^2.1.0:
   version "2.1.0"
   resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz"
@@ -1437,7 +1551,7 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2:
 
 mitt@^3.0.1:
   version "3.0.1"
-  resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
+  resolved "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz"
   integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
 
 ms@2.1.2, ms@^2.1.1:
@@ -1454,7 +1568,7 @@ mz@^2.7.0:
     object-assign "^4.0.1"
     thenify-all "^1.0.0"
 
-nanoid@^3.3.6:
+nanoid@^3.3.6, nanoid@^3.3.7:
   version "3.3.7"
   resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz"
   integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
@@ -1704,6 +1818,15 @@ postcss@^8.4.23, postcss@^8.4.27, postcss@^8.4.31:
     picocolors "^1.0.0"
     source-map-js "^1.0.2"
 
+postcss@^8.4.32:
+  version "8.4.32"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9"
+  integrity sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==
+  dependencies:
+    nanoid "^3.3.7"
+    picocolors "^1.0.0"
+    source-map-js "^1.0.2"
+
 prelude-ls@^1.2.1:
   version "1.2.1"
   resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
@@ -1762,11 +1885,21 @@ readdirp@~3.6.0:
   dependencies:
     picomatch "^2.2.1"
 
+regenerator-runtime@^0.14.0:
+  version "0.14.0"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
+  integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
+
 resolve-from@^4.0.0:
   version "4.0.0"
   resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
 
+resolve-url@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz"
+  integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==
+
 resolve@^1.1.7, resolve@^1.22.2:
   version "1.22.8"
   resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz"
@@ -1979,6 +2112,11 @@ thenify-all@^1.0.0:
   dependencies:
     any-promise "^1.0.0"
 
+throttle-debounce@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-5.0.0.tgz#a17a4039e82a2ed38a5e7268e4132d6960d41933"
+  integrity sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==
+
 titleize@^3.0.0:
   version "3.0.0"
   resolved "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz"
@@ -2047,6 +2185,15 @@ vant@^4.8.0:
     "@vant/use" "^1.6.0"
     "@vue/shared" "^3.0.0"
 
+viplayer@^1.1.8:
+  version "1.1.8"
+  resolved "https://registry.yarnpkg.com/viplayer/-/viplayer-1.1.8.tgz#a77fb0974d55d0386d47dc42ae93e23054baeb8d"
+  integrity sha512-j/geLKH7CzUlKLwTJTGRlz1QKnORKTwvl/SxXHqUVqAU6y66T0vrOBfVqVLuheNR4c1odt3CYnExxy/A3VyWAw==
+  dependencies:
+    hls.js "^1.4.10"
+    throttle-debounce "^5.0.0"
+    vue "^3.2.13"
+
 vite@^4.4.11:
   version "4.5.0"
   resolved "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz"
@@ -2092,6 +2239,17 @@ vue-router@^4.2.5:
   dependencies:
     "@vue/devtools-api" "^6.5.0"
 
+vue@^3.2.13:
+  version "3.3.10"
+  resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.10.tgz#6e19c1982ee655a14babe1610288b90005f02ab1"
+  integrity sha512-zg6SIXZdTBwiqCw/1p+m04VyHjLfwtjwz8N57sPaBhEex31ND0RYECVOC1YrRwMRmxFf5T1dabl6SGUbMKKuVw==
+  dependencies:
+    "@vue/compiler-dom" "3.3.10"
+    "@vue/compiler-sfc" "3.3.10"
+    "@vue/runtime-dom" "3.3.10"
+    "@vue/server-renderer" "3.3.10"
+    "@vue/shared" "3.3.10"
+
 vue@^3.3.4:
   version "3.3.8"
   resolved "https://registry.npmjs.org/vue/-/vue-3.3.8.tgz"