feat:版本迭代

This commit is contained in:
jiangrui
2025-02-20 12:00:19 +08:00
parent fd110590af
commit 510fdc48f6
86 changed files with 5045 additions and 1161 deletions

View File

@@ -0,0 +1,155 @@
<template>
<div class="aside-menu">
<div class="logo">
<img :src="logo" class="logo-img" />
<div class="logo-text">Cloud Saver</div>
</div>
<el-menu
:default-active="currentMenu?.index || '1'"
:default-openeds="currentMenuOpen"
class="el-menu-vertical"
@open="handleOpen"
@close="handleClose"
>
<template v-for="menu in menuList">
<el-sub-menu :index="menu.index" v-if="menu.children">
<template #title>
<el-icon><component :is="menu.icon" /></el-icon>
<span>{{ menu.title }}</span>
</template>
<el-menu-item
v-for="child in menu.children"
:index="child.index"
:key="child.index"
@click="handleMenuClick(child)"
>
{{ child.title }}
</el-menu-item>
</el-sub-menu>
<el-menu-item
v-else
:index="menu.index"
@click="handleMenuClick(menu)"
:disabled="menu.disabled"
>
<el-icon><component :is="menu.icon" /></el-icon>
<span>{{ menu.title }}</span>
</el-menu-item>
</template>
</el-menu>
</div>
</template>
<script lang="ts" setup>
import { Search, Film, Setting } from "@element-plus/icons-vue";
import logo from "@/assets/images/logo.png";
import { useRouter, useRoute } from "vue-router";
import { computed } from "vue";
const router = useRouter();
const route = useRoute();
interface MenuItem {
index: string;
title: string;
icon?: any;
router?: string;
children?: MenuItem[];
disabled?: boolean;
}
const menuList: MenuItem[] = [
{
index: "2",
title: "资源搜索",
icon: Search,
router: "/",
},
{
index: "1",
title: "豆瓣榜单",
icon: Film,
children: [
{
index: "1-1",
title: "热门电影",
router: "/douban?type=movie",
},
{
index: "1-2",
title: "热门电视剧",
router: "/douban?type=tv",
},
{
index: "1-3",
title: "最新电影",
router: "/douban?type=movie&tag=最新",
},
{
index: "1-4",
title: "热门综艺",
router: "/douban?type=tv&tag=综艺",
},
],
},
{
index: "3",
title: "设置",
icon: Setting,
router: "/setting",
disabled: false,
},
];
const currentMenu = computed(() => {
console.log("route", decodeURIComponent(route.fullPath));
return menuList
.reduce((pre: MenuItem[], cur: MenuItem) => {
if (!cur.children) {
pre.push(cur);
} else {
pre.push(...cur.children);
}
return pre;
}, [])
.find((x) => x.router === decodeURIComponent(route.fullPath));
});
const currentMenuOpen = computed(() => {
if (currentMenu.value && currentMenu.value.index.length > 1) {
console.log([currentMenu.value.index.split("-")[0]]);
return [currentMenu.value.index.split("-")[0]];
} else {
return [];
}
});
const handleOpen = (key: string, keyPath: string[]) => {
console.log(key, keyPath);
};
const handleClose = (key: string, keyPath: string[]) => {
console.log(key, keyPath);
};
const handleMenuClick = (menu: any) => {
console.log(menu);
if (menu.router) {
router.push(menu.router);
}
};
</script>
<style lang="scss" scoped>
.el-menu-vertical {
width: 100%;
height: 100vh;
}
.logo {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 0;
.logo-img {
width: 30px;
height: 30px;
margin-right: 15px;
}
.logo-text {
font-size: 20px;
}
}
</style>

View File

@@ -30,6 +30,7 @@
import { quarkApi } from "@/api/quark";
import type { TreeInstance } from "element-plus";
import type { Folder } from "@/types";
import { type RequestResult } from "@/types/response";
import { ElMessage } from "element-plus";
const props = defineProps({
@@ -58,13 +59,10 @@
quark: quarkApi,
};
const loadNode = async (node: any, resolve: (data: Folder[]) => void) => {
const loadNode = async (node: any, resolve: (list: Folder[]) => void) => {
const api = cloudTypeApiMap[props.cloudType as keyof typeof cloudTypeApiMap];
try {
let res: {
data: Folder[];
error?: string;
} = { data: [] };
let res: RequestResult<Folder[]> = { code: 0, data: [] as Folder[], message: "" };
if (node.level === 0) {
if (api.getFolderList) {
// 使
@@ -76,11 +74,10 @@
res = await api.getFolderList(node.data.cid);
}
}
if (res.data?.length > 0) {
resolve(res.data);
if (res?.code === 0) {
resolve(res.data.length ? res.data : []);
} else {
resolve([]);
throw new Error(res.error);
throw new Error(res.message);
}
} catch (error) {
ElMessage.error(error instanceof Error ? `${error.message}` : "获取目录失败");

View File

@@ -0,0 +1,189 @@
<template>
<div class="resource-card-list">
<div v-for="group in store.resources" :key="group.id" class="resource-list">
<div class="group-header">
<el-link :href="`https://t.me/s/${group.id}`" target="_blank" :underline="false">
<el-image :src="group.channelInfo.channelLogo" class="channel-logo" fit="cover" lazy />
{{ group.channelInfo.name }}</el-link
>
<el-icon class="header-icon" @click="group.displayList = !group.displayList"
><ArrowDown
/></el-icon>
</div>
<div class="card-item-list" v-show="group.displayList">
<div v-for="resource in group.list" :key="resource.messageId" class="card-item-content">
<el-card class="card-item">
<el-image
class="card-item-image"
:src="`/tele-images/?url=${encodeURIComponent(resource.image as string)}`"
fit="cover"
lazy
:alt="resource.title"
hide-on-click-modal
:preview-src-list="[
`${location.origin}/tele-images/?url=${encodeURIComponent(resource.image as string)}`,
]"
/>
<el-link :href="resource.cloudLinks[0]" target="_blank" :underline="false"
><div class="item-name">{{ resource.title }}</div></el-link
>
<div class="item-description" v-html="resource.content"></div>
<div class="tags-list" v-if="resource.tags && resource.tags.length">
<span>标签</span>
<el-tag
v-for="item in resource.tags"
class="resource_tag"
:key="item"
@click="searchMovieforTag(item)"
>
{{ item }}
</el-tag>
</div>
<template #footer>
<div class="item-footer">
<el-tag
:type="store.tagColor[resource.cloudType as keyof TagColor]"
effect="dark"
round
>
{{ resource.cloudType }}
</el-tag>
<el-button @click="handleSave(resource)">转存</el-button>
</div>
</template>
</el-card>
</div>
</div>
<div class="load-more" v-show="group.displayList">
<el-button @click="handleLoadMore(group.id)"> 加载更多 </el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useResourceStore } from "@/stores/resource";
import { computed } from "vue";
import type { ResourceItem, TagColor } from "@/types";
const store = useResourceStore();
const location = computed(() => window.location);
const emit = defineEmits(["save", "loadMore", "searchMovieforTag"]);
const handleSave = (resource: ResourceItem) => {
emit("save", resource);
};
const searchMovieforTag = (tag: string) => {
emit("searchMovieforTag", tag);
};
const handleLoadMore = (channelId: string) => {
emit("loadMore", channelId);
};
</script>
<style scoped>
.resource-list {
margin-bottom: 30px;
padding: 20px;
border-radius: 15px;
background-color: var(--theme-other_background);
&:last-child {
margin-bottom: 0;
}
}
.card-item-list {
display: grid;
grid-template-columns: repeat(auto-fill, 220px);
grid-row-gap: 30px;
justify-content: space-between;
margin-top: 20px;
/* grid-column-gap: auto-fill; */
/* flex-wrap: wrap; */
}
.card-item-content {
/* height: 520px; */
}
.channel-logo {
height: 40px;
width: 40px;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin-right: 10px;
}
.load-more {
margin-top: 40px;
width: 100%;
text-align: center;
}
.card-item {
max-width: 480px;
height: 100%;
border-radius: 20px;
}
.card-item-image {
border-radius: 20px;
width: 100%;
height: 220px;
}
.item-name,
.item-description {
max-width: 100%;
margin: 15px 0;
-webkit-box-orient: vertical;
display: -webkit-box;
-webkit-line-clamp: 2;
overflow: hidden;
white-space: all;
}
.item-description {
-webkit-line-clamp: 4;
margin-top: 0;
height: 100px;
}
.item-name {
height: 58px;
font-size: 18px;
}
.tags-list {
display: flex;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
height: 58px;
overflow: hidden;
}
.resource_tag {
cursor: pointer;
margin-right: 10px;
margin-bottom: 5px;
}
.group-header {
height: 50px;
line-height: 50px;
text-align: left;
padding: 0 15px;
font-size: 20px;
display: flex;
align-items: center;
justify-content: space-between;
/* text-align: center; */
.el-link {
font-size: 22px;
}
.header-icon {
cursor: pointer;
width: 50px;
height: 50px;
}
}
.item-footer {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,197 @@
<template>
<el-table
v-loading="store.loading"
class="resource-list-table"
:data="store.resources"
style="width: 100%"
row-key="id"
:default-expand-all="true"
>
<el-table-column type="expand">
<template #default="props">
<el-table :data="props.row.list" style="width: 100%">
<el-table-column label="图片" width="180">
<template #default="{ row }">
<el-image
class="table-item-image"
v-if="row.image"
:src="`/tele-images/?url=${encodeURIComponent(row.image as string)}`"
hide-on-click-modal
:preview-src-list="[
`${location.origin}/tele-images/?url=${encodeURIComponent(row.image as string)}`,
]"
:zoom-rate="1.2"
:max-scale="7"
:min-scale="0.2"
:initial-index="4"
preview-teleported
:z-index="999"
fit="cover"
width="60"
height="90"
/>
<el-icon v-else size="20"><Close /></el-icon>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" width="180">
<template #default="{ row }">
<el-link :href="row.cloudLinks[0]" target="_blank">
{{ row.title }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="title" label="描述">
<template #default="{ row }">
<div v-html="row.content" class="item-description"></div>
</template>
</el-table-column>
<el-table-column prop="tags" label="标签">
<template #default="{ row }">
<div class="tags-list" v-if="row.tags.length > 0">
<span>标签</span>
<el-tag
v-for="item in row.tags"
class="resource_tag"
:key="item"
@click="searchMovieforTag(item)"
>
{{ item }}
</el-tag>
</div>
<span v-else></span>
</template>
</el-table-column>
<!-- <el-table-column label="地址">
<template #default="{ row }">
<el-link :href="row.cloudLinks[0]" target="_blank">
{{ row.cloudLinks[0] }}
</el-link>
</template>
</el-table-column> -->
<el-table-column label="云盘类型" width="120">
<template #default="{ row }">
<el-tag :type="store.tagColor[row.cloudType as keyof TagColor]" effect="dark" round>
{{ row.cloudType }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button @click="handleSave(row)">转存</el-button>
</template>
</el-table-column>
</el-table>
<div class="load-more">
<el-button :loading="props.row.loading" @click="handleLoadMore(props.row.id)">
加载更多
</el-button>
</div>
</template>
</el-table-column>
<el-table-column label="来源" prop="channel">
<template #default="{ row }">
<div class="group-header">
<el-image :src="row.channelInfo.channelLogo" class="channel-logo" fit="cover" lazy />
<span>{{ row.channelInfo.name }}</span>
<span class="item-count">({{ row.list.length }})</span>
</div>
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
import { useResourceStore } from "@/stores/resource";
import type { Resource, TagColor } from "@/types";
import { computed } from "vue";
const store = useResourceStore();
const emit = defineEmits(["save", "loadMore", "searchMovieforTag"]);
const location = computed(() => window.location);
const handleSave = (resource: Resource) => {
emit("save", resource);
};
// 添加加载更多处理函数
const handleLoadMore = (channelId: string) => {
emit("loadMore", channelId);
};
const searchMovieforTag = (tag: string) => {
emit("searchMovieforTag", tag);
};
</script>
<style scoped>
.resource-list-table {
border-radius: 15px;
}
.group-header {
display: flex;
align-items: center;
gap: 8px;
}
.channel-logo {
width: 20px;
height: 20px;
margin-right: 10px;
border-radius: 50%;
overflow: hidden;
}
.table-item-image {
border-radius: 20px;
width: 100%;
height: 220px;
}
.item-count {
color: #909399;
font-size: 0.9em;
}
.tags-list {
display: flex;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
}
.resource_tag {
cursor: pointer;
margin-right: 10px;
margin-bottom: 5px;
}
.item-description {
max-width: 100%;
margin: 15px 0;
-webkit-box-orient: vertical;
display: -webkit-box;
-webkit-line-clamp: 4;
overflow: hidden;
white-space: all;
}
:deep(.el-table__expand-column) {
.cell {
padding: 0 !important;
}
}
:deep(.el-table__expanded-cell) {
padding: 20px !important;
}
:deep(.el-table__expand-icon) {
height: 23px;
line-height: 23px;
}
.load-more {
display: flex;
justify-content: center;
padding: 16px 0;
}
</style>

View File

@@ -1,221 +0,0 @@
<template>
<div class="resource-list">
<el-table
v-loading="store.loading"
:data="groupedResources"
style="width: 100%"
row-key="id"
:default-expand-all="true"
>
<el-table-column type="expand">
<template #default="props">
<el-table :data="props.row.items" style="width: 100%">
<el-table-column label="图片" width="90">
<template #default="{ row }">
<el-image
v-if="row.image"
:src="row.image"
:preview-src-list="[row.image]"
:zoom-rate="1.2"
:max-scale="7"
:min-scale="0.2"
:initial-index="4"
preview-teleported
:z-index="999"
fit="cover"
width="60"
height="90"
/>
<el-icon v-else size="20"><Close /></el-icon>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" width="180" />
<el-table-column label="地址">
<template #default="{ row }">
<el-link :href="row.cloudLinks[0]" target="_blank">
{{ row.cloudLinks[0] }}
</el-link>
</template>
</el-table-column>
<el-table-column label="云盘类型" width="120">
<template #default="{ row }">
<el-tag
:type="tagColor[row.cloudType as keyof typeof tagColor]"
effect="dark"
round
>
{{ row.cloudType }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button @click="handleSave(row)">转存</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="props.row.hasMore" class="load-more">
<el-button :loading="props.row.loading" @click="handleLoadMore(props.row.channel)">
加载更多
</el-button>
</div>
</template>
</el-table-column>
<el-table-column label="来源" prop="channel">
<template #default="{ row }">
<div class="group-header">
<span>{{ row.channel }}</span>
<span class="item-count">({{ row.items.length }})</span>
</div>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="folderDialogVisible" title="选择保存目录" v-if="currentResource">
<template #header="{ titleId }">
<div class="my-header">
<div :id="titleId">
<el-tag
:type="tagColor[currentResource.cloudType as keyof typeof tagColor]"
effect="dark"
round
>
{{ currentResource.cloudType }}
</el-tag>
选择保存目录
</div>
</div>
</template>
<folder-select
v-if="folderDialogVisible"
@select="handleFolderSelect"
@close="folderDialogVisible = false"
:cloudType="currentResource.cloudType"
/>
<div class="dialog-footer">
<el-button @click="folderDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveBtnClick">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import { useResourceStore } from "@/stores/resource";
import FolderSelect from "./FolderSelect.vue";
import type { Resource } from "@/types";
const tagColor = {
baiduPan: "primary",
weiyun: "info",
aliyun: "warning",
pan115: "danger",
quark: "success",
};
const store = useResourceStore();
const folderDialogVisible = ref(false);
const currentResource = ref<Resource | null>(null);
const currentFolderId = ref<string | null>(null);
// 按来源分组的数据
const groupedResources = computed(() => {
const groups = store.resources.reduce(
(acc, curr) => {
const channel = curr.channel;
const channelId = curr.channelId;
if (!acc[channel]) {
acc[channel] = {
channel,
items: [],
hasMore: true,
loading: false, // 添加 loading 状态
id: channelId || "", // 用于row-key
};
}
acc[channel].items.push(curr);
return acc;
},
{} as Record<
string,
{ channel: string; items: Resource[]; id: string; hasMore: boolean; loading: boolean }
>
);
return Object.values(groups);
});
const handleSave = (resource: Resource) => {
currentResource.value = resource;
folderDialogVisible.value = true;
};
const handleFolderSelect = async (folderId: string) => {
if (!currentResource.value) return;
currentFolderId.value = folderId;
};
const handleSaveBtnClick = async () => {
if (!currentResource.value || !currentFolderId.value) return;
folderDialogVisible.value = false;
await store.saveResource(currentResource.value, currentFolderId.value);
};
// 添加加载更多处理函数
const handleLoadMore = async (channel: string) => {
const group = groupedResources.value.find((g) => g.channel === channel);
if (!group || group.loading) return;
group.loading = true;
try {
const lastMessageId = group.items[group.items.length - 1].messageId;
store.searchResources("", false, true, group.id, lastMessageId);
} finally {
group.loading = false;
}
};
</script>
<style scoped>
.resource-list {
margin-top: 20px;
}
.dialog-footer {
display: flex;
align-items: center;
justify-content: flex-end;
}
.group-header {
display: flex;
align-items: center;
gap: 8px;
}
.item-count {
color: #909399;
font-size: 0.9em;
}
:deep(.el-table__expand-column) {
.cell {
padding: 0 !important;
}
}
:deep(.el-table__expanded-cell) {
padding: 20px !important;
}
:deep(.el-table__expand-icon) {
height: 23px;
line-height: 23px;
}
.load-more {
display: flex;
justify-content: center;
padding: 16px 0;
}
</style>

View File

@@ -1,70 +1,115 @@
<template>
<div class="search-bar">
<el-input
v-model="keyword"
placeholder="请输入搜索关键词与输入链接直接解析"
class="input-with-select"
@keyup.enter="handleSearch"
style="margin-bottom: 8px"
>
<template #append>
<el-button type="success" @click="handleSearch">{{ searchBtnText }}</el-button>
</template>
</el-input>
<el-alert
title="可直接输入链接进行资源解析,也可进行资源搜索!"
type="info"
show-icon
:closable="false"
/>
<div class="search-new">
<el-button type="primary" @click="handleSearchNew">最新资源</el-button>
<div class="switch-source">
<el-switch v-model="backupPlan" /><span class="label">使用rsshub(较慢)</span>
</div>
<div class="search-bar__input">
<input
v-model="keyword"
type="text"
placeholder="请输入搜索关键词或输入链接直接解析"
class="input-with-select"
@keyup.enter="handleSearch"
/>
<el-icon class="search_icon" size="28px" @click="handleSearch"><Search /></el-icon class="search_icon">
</div>
<div class="logout" alt="退出登录" @click="logout">
<el-tooltip
class="box-item"
effect="dark"
content="退出登录"
placement="bottom"
>
<el-icon><SwitchButton class="logout-icon" /></el-icon>
</el-tooltip>
</div>
</div>
</template>
<script setup lang="ts">
import { effect, ref } from "vue";
import { computed, ref,watch } from "vue";
import { useResourceStore } from "@/stores/resource";
import { useRoute,useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();
const resourcStore = useResourceStore();
const keyword = ref("");
const backupPlan = ref(false);
const store = useResourceStore();
const searchBtnText = ref("搜索");
effect(() => {
// 监听搜索关键词的变化,如果存在,则自动触发搜索
if (keyword.value && keyword.value.startsWith("http")) {
searchBtnText.value = "解析";
} else {
searchBtnText.value = "搜索";
}
});
const routeKeyword = computed(() => route.query.keyword as string);
const logout = () => {
localStorage.removeItem("token");
router.push({ path: "/login" });
};
const handleSearch = async () => {
// 如果搜索内容是一个https的链接则尝试解析链接
if (keyword.value.startsWith("http")) {
store.parsingCloudLink(keyword.value);
resourcStore.parsingCloudLink(keyword.value);
return;
}
if (!keyword.value.trim()) {
return;
}
await store.searchResources(keyword.value, backupPlan.value);
await resourcStore.searchResources(keyword.value);
if(route.path !== '/'){
router.push({ path: '/' });
}
};
const handleSearchNew = async () => {
keyword.value = "";
await store.searchResources("", backupPlan.value);
};
watch(() => routeKeyword.value, () => {
if(routeKeyword.value){
keyword.value = routeKeyword.value;
handleSearch();
} else {
keyword.value = ''
}
});
</script>
<style scoped>
<style scoped lang="scss">
.search-bar {
padding: 20px;
display: flex;
align-items: center;
justify-content: space-between;
.logout{
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 28px;
margin-left: 15px;
cursor: pointer;
}
.search-bar__input {
width: 100%;
margin: 15px auto;
position: relative;
background-color: var(--theme-other_background);
box-shadow: 0 4px 10px rgba(225, 225, 225, 0.3);
height: 40px;
border-radius: 40px;
display: flex;
align-items: center;
}
.input-with-select {
width: 100%;
height: 100%;
background: none;
padding-left: 24px;
box-sizing: border-box;
border-radius: 40px;
line-height: 100%;
border: unset;
font-size: 18px;
}
.search_icon {
width: 64px;
height: 64px;
cursor: pointer;
}
.search-bar_tips{
width: 100%;
text-align: center;
margin: 20px auto;
}
}
.search-new {
@@ -79,4 +124,7 @@
.switch-source .label {
margin-left: 5px;
}
.display-style {
margin-left: 20px;
}
</style>