Spaces:
Sleeping
Sleeping
| <template> | |
| <div ref="listRef" class="resource-list"> | |
| <!-- 头部刷新区 --> | |
| <van-cell-group :border="false" class="resource-list__header"> | |
| <van-cell center clickable @click="refreshResources"> | |
| <template #icon> | |
| <van-icon name="replay" class="header__icon" /> | |
| </template> | |
| <template #title> | |
| <div class="header__content"> | |
| <span class="content__title">最新资源</span> | |
| <span class="content__tip">(点击可获取最新资源)</span> | |
| </div> | |
| </template> | |
| <template #label> | |
| <span class="header__time">上次刷新:{{ resourceStore.lastUpdateTime }}</span> | |
| </template> | |
| </van-cell> | |
| </van-cell-group> | |
| <!-- 资源列表 --> | |
| <van-tabs | |
| v-model:active="currentTab" | |
| swipeable | |
| animated | |
| class="resource-list__tabs" | |
| :border="false" | |
| > | |
| <van-tab | |
| v-for="item in resourceStore.resources" | |
| :key="item.id" | |
| :name="item.id" | |
| :title="item.channelInfo.name" | |
| > | |
| <ResourceCard | |
| :current-channel-id="currentTab" | |
| @save="handleSave" | |
| @jump="handleJump" | |
| @search-moviefor-tag="searchMovieforTag" | |
| /> | |
| </van-tab> | |
| </van-tabs> | |
| <!-- 保存弹窗 --> | |
| <van-popup | |
| v-model:show="saveDialogVisible" | |
| round | |
| closeable | |
| position="bottom" | |
| :style="{ height: '80%', transform: 'translateZ(1px)' }" | |
| class="save-popup" | |
| > | |
| <div class="save-popup__container"> | |
| <!-- 弹窗头部 --> | |
| <div class="save-popup__header"> | |
| <van-tag :color="getTagColor(currentResource?.cloudType)" round size="medium"> | |
| {{ currentResource?.cloudType }} | |
| </van-tag> | |
| <span class="header__title">{{ saveDialogMap[saveDialogStep].title }}</span> | |
| <span | |
| v-if="resourceStore.shareInfo.fileSize && saveDialogStep === 1" | |
| class="header__size" | |
| > | |
| {{ formattedFileSize(resourceStore.shareInfo.fileSize) }} | |
| </span> | |
| </div> | |
| <!-- 弹窗内容 --> | |
| <div class="save-popup__content"> | |
| <van-empty v-if="resourceStore.loadTree && saveDialogStep === 1" class="content__loading"> | |
| <template #image> | |
| <van-loading size="24px" vertical>加载中...</van-loading> | |
| </template> | |
| </van-empty> | |
| <resource-select | |
| v-if="saveDialogVisible && saveDialogStep === 1 && resourceStore.resourceSelect.length" | |
| :cloud-type="currentResource?.cloudType" | |
| /> | |
| <folder-select | |
| v-if="saveDialogVisible && saveDialogStep === 2 && currentResource" | |
| :cloud-type="currentResource.cloudType" | |
| @select="handleFolderSelect" | |
| @close="saveDialogVisible = false" | |
| /> | |
| </div> | |
| <!-- 弹窗底部 --> | |
| <div class="save-popup__footer"> | |
| <van-cell class="footer__path" :border="false"> | |
| <template #title> | |
| <div class="path__label">保存至:</div> | |
| </template> | |
| <template #value> | |
| <div class="path__value"> | |
| <van-icon name="folder-o" class="value__icon" /> | |
| <span | |
| class="value__text" | |
| :class="{ 'value__text--placeholder': !currentFolderPath }" | |
| > | |
| {{ getCurrentFolderName }} | |
| </span> | |
| </div> | |
| </template> | |
| </van-cell> | |
| <van-button | |
| round | |
| block | |
| type="primary" | |
| size="large" | |
| :loading="isLoading" | |
| @click="handleConfirmClick" | |
| > | |
| {{ saveDialogMap[saveDialogStep].buttonText }} | |
| </van-button> | |
| </div> | |
| </div> | |
| </van-popup> | |
| </div> | |
| </template> | |
| <script setup lang="ts"> | |
| import { ref, watch, onMounted, onUnmounted, computed, onBeforeUnmount } from "vue"; | |
| import { useRouter } from "vue-router"; | |
| import { showToast } from "vant"; | |
| import { useResourceStore } from "@/stores/resource"; | |
| import { formattedFileSize, throttle } from "@/utils/index"; | |
| import type { Folder, ResourceItem } from "@/types"; | |
| import FolderSelect from "@/components/mobile/FolderSelect.vue"; | |
| import ResourceSelect from "@/components/mobile/ResourceSelect.vue"; | |
| import ResourceCard from "@/components/mobile/ResourceCard.vue"; | |
| // 状态管理 | |
| const router = useRouter(); | |
| const resourceStore = useResourceStore(); | |
| // 响应式数据 | |
| const saveDialogVisible = ref(false); | |
| const currentResource = ref<ResourceItem | null>(null); | |
| const currentFolderId = ref<string | null>(null); | |
| const currentFolderPath = ref<Folder[] | null>(null); | |
| const saveDialogStep = ref<1 | 2>(1); | |
| const currentTab = ref<string>(""); | |
| const isLoading = ref(false); | |
| const listRef = ref<HTMLElement | null>(null); | |
| // 计算属性 | |
| const getCurrentFolderName = computed(() => { | |
| return currentFolderPath.value | |
| ? currentFolderPath.value[currentFolderPath.value.length - 1]?.name | |
| : "待选择"; | |
| }); | |
| // 常量定义 | |
| const saveDialogMap = { | |
| 1: { title: "选择资源", buttonText: "下一步" }, | |
| 2: { title: "选择保存目录", buttonText: "保存" }, | |
| }; | |
| // 标签颜色映射 | |
| const getTagColor = (type?: string) => { | |
| const colorMap: Record<string, string> = { | |
| pan115: "#07c160", | |
| quark: "#1989fa", | |
| }; | |
| return colorMap[type || ""] || "#ff976a"; | |
| }; | |
| // 监听器 | |
| watch( | |
| () => resourceStore.resources, | |
| () => { | |
| if (resourceStore.resources.length > 0) { | |
| currentTab.value = resourceStore.resources[0].id; | |
| } | |
| } | |
| ); | |
| // 方法定义 | |
| const refreshResources = () => { | |
| resourceStore.searchResources("", false); | |
| }; | |
| const handleSave = async (resource: ResourceItem) => { | |
| currentResource.value = resource; | |
| saveDialogVisible.value = true; | |
| saveDialogStep.value = 1; | |
| if (!(await resourceStore.getResourceListAndSelect(resource))) { | |
| saveDialogVisible.value = false; | |
| } | |
| }; | |
| const handleJump = (resource: ResourceItem) => { | |
| window.open(resource.cloudLinks[0], "_blank"); | |
| }; | |
| const handleFolderSelect = (folders: Folder[] | null) => { | |
| if (!currentResource.value) return; | |
| currentFolderPath.value = folders; | |
| currentFolderId.value = folders?.[folders.length - 1]?.cid || "0"; | |
| }; | |
| const handleConfirmClick = async () => { | |
| if (saveDialogStep.value === 1) { | |
| if (!resourceStore.resourceSelect.some((x) => x.isChecked)) { | |
| showToast("请选择要保存的资源"); | |
| return; | |
| } | |
| saveDialogStep.value = 2; | |
| } else { | |
| handleSaveBtnClick(); | |
| } | |
| }; | |
| const handleSaveBtnClick = async () => { | |
| if (!currentResource.value || !currentFolderId.value) return; | |
| saveDialogVisible.value = false; | |
| await resourceStore.saveResource(currentResource.value, currentFolderId.value); | |
| }; | |
| const searchMovieforTag = (tag: string) => { | |
| router.push({ path: "/resource", query: { keyword: tag } }); | |
| }; | |
| // 使用节流包装加载更多函数 | |
| const throttledLoadMore = throttle((channelId: string) => { | |
| resourceStore.searchResources("", true, channelId); | |
| }, 2000); | |
| // 滚动加载 | |
| const doScroll = () => { | |
| const appElement = document.querySelector("#app") as HTMLElement; | |
| if (appElement) { | |
| const { scrollHeight, scrollTop, clientHeight } = appElement; | |
| if (scrollHeight - (clientHeight + scrollTop) <= 1) { | |
| throttledLoadMore(currentTab.value); | |
| } | |
| } | |
| }; | |
| // 生命周期 | |
| onMounted(() => { | |
| const appElement = document.querySelector("#app"); | |
| if (appElement) { | |
| appElement.addEventListener("scroll", doScroll); | |
| } | |
| }); | |
| onUnmounted(() => { | |
| const appElement = document.querySelector("#app"); | |
| if (appElement) { | |
| appElement.removeEventListener("scroll", doScroll); | |
| } | |
| }); | |
| // 监听标签页切换 | |
| watch(currentTab, () => { | |
| const appElement = document.querySelector("#app"); | |
| if (appElement) { | |
| appElement.scrollTo(0, 0); | |
| } | |
| }); | |
| // 页面进入 设置缓存的数据源 | |
| onMounted(() => { | |
| const lastResourceList = localStorage.getItem("last_resource_list"); | |
| if (lastResourceList) { | |
| resourceStore.resources = JSON.parse(lastResourceList).list; | |
| } | |
| }); | |
| // 页面销毁 清除搜索词 | |
| onBeforeUnmount(() => { | |
| resourceStore.keyword = ""; | |
| }); | |
| </script> | |
| <style lang="scss" scoped> | |
| .resource-list { | |
| min-height: 100%; | |
| background: var(--van-background); | |
| padding-bottom: 20px; | |
| &__header { | |
| margin-bottom: 8px; | |
| background: var(--theme-other_background); | |
| :deep(.van-cell) { | |
| padding: 12px 16px; | |
| min-height: 24px; | |
| } | |
| .header__icon { | |
| font-size: 30px; | |
| color: var(--theme-theme); | |
| margin-right: 10px; | |
| line-height: 1; | |
| } | |
| .header__content { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| .content__title { | |
| font-size: 15px; | |
| font-weight: 500; | |
| line-height: 1.4; | |
| } | |
| .content__tip { | |
| font-size: 12px; | |
| color: var(--van-gray-6); | |
| background: var(--van-gray-1); | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| line-height: 1.4; | |
| } | |
| } | |
| .header__time { | |
| font-size: 12px; | |
| color: var(--van-gray-6); | |
| line-height: 1.4; | |
| margin-top: 2px; | |
| } | |
| } | |
| &__tabs { | |
| :deep(.van-tabs__wrap) { | |
| background: var(--theme-other_background); | |
| } | |
| :deep(.van-tab) { | |
| font-size: 14px; | |
| padding: 0 20px; | |
| height: 44px; | |
| line-height: 44px; | |
| } | |
| :deep(.van-tabs__line) { | |
| background: var(--theme-theme); | |
| } | |
| :deep(.van-tabs__content) { | |
| padding: 8px 0; | |
| } | |
| } | |
| } | |
| .save-popup { | |
| &__container { | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| padding-bottom: calc(env(safe-area-inset-bottom) + 50px); | |
| } | |
| &__header { | |
| flex-shrink: 0; | |
| padding: 16px; | |
| border-bottom: 0.5px solid var(--van-gray-3); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding-right: 40px; | |
| .header__title { | |
| font-size: 16px; | |
| font-weight: 500; | |
| } | |
| .header__size { | |
| margin-left: auto; | |
| font-size: 13px; | |
| color: var(--van-gray-6); | |
| max-width: 120px; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| } | |
| &__content { | |
| flex: 1; | |
| overflow-y: auto; | |
| background: var(--van-background-2); | |
| .content__loading { | |
| padding: 32px 0; | |
| } | |
| } | |
| &__footer { | |
| flex-shrink: 0; | |
| padding: 12px 16px 16px; | |
| background: var(--theme-other_background); | |
| border-top: 0.5px solid var(--van-gray-3); | |
| padding-bottom: calc(16px + env(safe-area-inset-bottom)); | |
| .footer__path { | |
| margin: 0 0 12px; | |
| :deep(.van-cell__title) { | |
| flex: none; | |
| padding-right: 8px; | |
| display: flex; | |
| align-items: center; | |
| } | |
| :deep(.van-cell__value) { | |
| flex: 1; | |
| } | |
| .path__label { | |
| font-size: 14px; | |
| color: var(--van-text-color); | |
| font-weight: 500; | |
| white-space: nowrap; | |
| } | |
| .path__value { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| padding: 6px 12px; | |
| background: var(--van-gray-1); | |
| border-radius: 4px; | |
| width: 100%; | |
| box-sizing: border-box; | |
| .value__icon { | |
| font-size: 16px; | |
| color: var(--theme-theme); | |
| flex-shrink: 0; | |
| } | |
| .value__text { | |
| font-size: 14px; | |
| color: var(--van-text-color); | |
| flex: 1; | |
| min-width: 0; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| text-align: left; | |
| display: block; | |
| &--placeholder { | |
| color: var(--van-gray-6); | |
| } | |
| } | |
| } | |
| } | |
| :deep(.van-cell__value) { | |
| flex: 1; | |
| text-align: left; | |
| } | |
| .van-button { | |
| height: 44px; | |
| font-size: 16px; | |
| font-weight: 500; | |
| } | |
| } | |
| :deep(.van-popup) { | |
| z-index: 2001 ; | |
| } | |
| } | |
| // 全局样式优化 | |
| :deep(.van-cell) { | |
| padding: 16px 20px; | |
| &::after { | |
| display: none; | |
| } | |
| } | |
| :deep(.van-popup) { | |
| max-height: 90vh; | |
| } | |
| :deep(.van-overlay) { | |
| background-color: rgba(0, 0, 0, 0.7); | |
| } | |
| </style> | |