mirror of
https://github.com/jiangrui1994/CloudSaver.git
synced 2026-01-12 08:08:46 +08:00
refactor:pc views
This commit is contained in:
@@ -1,26 +1,46 @@
|
||||
<template>
|
||||
<div class="folder-select">
|
||||
<div class="folder-select-header">
|
||||
当前位置:<el-icon style="margin: 0 5px"><Folder /></el-icon
|
||||
>{{ selectedFolder?.path?.map((x: Folder) => x.name).join("/") }}
|
||||
<div class="folder-header">
|
||||
<div class="folder-path">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
<template v-if="folderPath.length">
|
||||
<span
|
||||
v-for="(folder, index) in folderPath"
|
||||
:key="folder.cid"
|
||||
class="path-item"
|
||||
@click="handlePathClick(index)"
|
||||
>
|
||||
<span class="folder-name">{{ folder.name }}</span>
|
||||
<el-icon v-if="index < folderPath.length - 1"><ArrowRight /></el-icon>
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="root-path" @click="handlePathClick(-1)">根目录</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="folders"
|
||||
:props="defaultProps"
|
||||
node-key="cid"
|
||||
:load="loadNode"
|
||||
lazy
|
||||
highlight-current
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ node }">
|
||||
<span class="folder-node">
|
||||
|
||||
<div class="folder-list">
|
||||
<div v-if="!folders.length" class="empty-folder">
|
||||
<el-empty description="暂无文件夹" />
|
||||
</div>
|
||||
<div
|
||||
v-for="folder in folders"
|
||||
:key="folder.cid"
|
||||
class="folder-item"
|
||||
:class="{ 'is-selected': folder.cid === selectedFolder?.cid }"
|
||||
@click="handleFolderClick(folder)"
|
||||
>
|
||||
<div class="folder-info">
|
||||
<el-icon><Folder /></el-icon>
|
||||
{{ node.label }}
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
<span class="folder-name">{{ folder.name }}</span>
|
||||
</div>
|
||||
<el-icon class="arrow-icon"><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<el-icon class="loading-icon"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -28,12 +48,9 @@
|
||||
import { ref, defineProps } from "vue";
|
||||
import { cloud115Api } from "@/api/cloud115";
|
||||
import { quarkApi } from "@/api/quark";
|
||||
import type { TreeInstance } from "element-plus";
|
||||
import type { Folder } from "@/types";
|
||||
import { type RequestResult } from "@/types/response";
|
||||
import { useResourceStore } from "@/stores/resource";
|
||||
import type { Folder as FolderType } from "@/types";
|
||||
import { Folder, FolderOpened, ArrowRight, Loading } from "@element-plus/icons-vue";
|
||||
|
||||
const resourceStore = useResourceStore();
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -43,96 +60,218 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const treeRef = ref<TreeInstance>();
|
||||
const folders = ref<Folder[]>([]);
|
||||
const selectedFolder = ref<Folder | null>(null);
|
||||
const loading = ref(false);
|
||||
const folders = ref<FolderType[]>([]);
|
||||
const selectedFolder = ref<FolderType | null>(null);
|
||||
const folderPath = ref<FolderType[]>([{ name: "根目录", cid: "0" }]);
|
||||
const emit = defineEmits<{
|
||||
(e: "select", folderId: string): void;
|
||||
(e: "close"): void;
|
||||
}>();
|
||||
|
||||
const defaultProps = {
|
||||
label: "name",
|
||||
children: "children",
|
||||
isLeaf: "leaf",
|
||||
};
|
||||
|
||||
const cloudTypeApiMap = {
|
||||
pan115: cloud115Api,
|
||||
quark: quarkApi,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const loadNode = async (node: any, resolve: (list: Folder[]) => void) => {
|
||||
const getList = async (cid: string = "0") => {
|
||||
const api = cloudTypeApiMap[props.cloudType as keyof typeof cloudTypeApiMap];
|
||||
loading.value = true;
|
||||
try {
|
||||
let res: RequestResult<Folder[]> = { code: 0, data: [] as Folder[], message: "" };
|
||||
resourceStore.setLoadTree(true);
|
||||
if (node.level === 0) {
|
||||
if (api.getFolderList) {
|
||||
// 使用类型保护检查方法是否存在
|
||||
res = await api.getFolderList();
|
||||
}
|
||||
} else {
|
||||
if (api.getFolderList) {
|
||||
// 使用类型保护检查方法是否存在
|
||||
res = await api.getFolderList(node.data.cid);
|
||||
}
|
||||
}
|
||||
const res = await api.getFolderList?.(cid);
|
||||
if (res?.code === 0) {
|
||||
resolve(res.data.length ? res.data : []);
|
||||
folders.value = res.data || [];
|
||||
} else {
|
||||
throw new Error(res.message);
|
||||
throw new Error(res?.message);
|
||||
}
|
||||
resourceStore.setLoadTree(false);
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? `${error.message}` : "获取目录失败");
|
||||
// 关闭模态框
|
||||
ElMessage.error(error instanceof Error ? error.message : "获取目录失败");
|
||||
emit("close");
|
||||
resourceStore.setLoadTree(false);
|
||||
resolve([]);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleNodeClick = (data: Folder) => {
|
||||
selectedFolder.value = {
|
||||
...data,
|
||||
path: data.path ? [...data.path, data] : [data],
|
||||
};
|
||||
emit("select", data.cid);
|
||||
const handleFolderClick = async (folder: FolderType) => {
|
||||
selectedFolder.value = folder;
|
||||
folderPath.value = [...folderPath.value, folder];
|
||||
emit("select", folder.cid);
|
||||
await getList(folder.cid);
|
||||
};
|
||||
|
||||
const handlePathClick = async (index: number) => {
|
||||
if (index < 0) {
|
||||
// 点击根目录
|
||||
folderPath.value = [{ name: "根目录", cid: "0" }];
|
||||
selectedFolder.value = null;
|
||||
await getList("0");
|
||||
} else {
|
||||
// 点击路径中的某个文件夹
|
||||
const targetFolder = folderPath.value[index];
|
||||
folderPath.value = folderPath.value.slice(0, index + 1);
|
||||
selectedFolder.value = targetFolder;
|
||||
await getList(targetFolder.cid);
|
||||
emit("select", targetFolder.cid);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化加载
|
||||
getList();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/common.scss";
|
||||
|
||||
.folder-select {
|
||||
position: relative;
|
||||
min-height: 300px;
|
||||
max-height: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px;
|
||||
|
||||
.folder-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: var(--theme-radius);
|
||||
|
||||
.folder-path {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--theme-text-regular);
|
||||
font-size: 14px;
|
||||
overflow-x: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 16px;
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
|
||||
.path-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
|
||||
.folder-name {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.folder-name {
|
||||
color: var(--theme-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.root-path {
|
||||
color: var(--theme-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.folder-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
|
||||
.folder-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--theme-radius);
|
||||
cursor: pointer;
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--theme-primary);
|
||||
|
||||
.el-icon {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.folder-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
color: var(--theme-text-regular);
|
||||
}
|
||||
|
||||
.folder-name {
|
||||
color: var(--theme-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 16px;
|
||||
color: var(--theme-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.folder-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.empty-folder {
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
@include flex-center;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(4px);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.folder-path {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content) {
|
||||
height: 32px;
|
||||
}
|
||||
.folder-select-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid #e5e6e8;
|
||||
border-radius: 8px;
|
||||
color: var(--theme-text-regular);
|
||||
|
||||
.loading-icon {
|
||||
font-size: 20px;
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
from {
|
||||
transform: rotate(0);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,61 +1,155 @@
|
||||
<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 v-show="group.displayList" class="card-item-list">
|
||||
<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 v-if="resource.tags && resource.tags.length" class="tags-list">
|
||||
<span>标签:</span>
|
||||
<div class="resource-card">
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog
|
||||
v-model="showDetail"
|
||||
:title="currentResource?.title"
|
||||
width="700px"
|
||||
class="resource-detail-dialog"
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-if="currentResource" class="detail-content">
|
||||
<div class="detail-cover">
|
||||
<el-image
|
||||
class="cover-image"
|
||||
:src="`/tele-images/?url=${encodeURIComponent(currentResource.image as string)}`"
|
||||
fit="cover"
|
||||
/>
|
||||
<el-tag
|
||||
class="cloud-type"
|
||||
:type="store.tagColor[currentResource.cloudType as keyof TagColor]"
|
||||
effect="dark"
|
||||
round
|
||||
>
|
||||
{{ currentResource.cloudType }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<h3 class="detail-title">
|
||||
<el-link :href="currentResource.cloudLinks[0]" target="_blank" :underline="false">
|
||||
{{ currentResource.title }}
|
||||
</el-link>
|
||||
</h3>
|
||||
<div class="detail-description" v-html="currentResource.content" />
|
||||
<div v-if="currentResource.tags?.length" class="detail-tags">
|
||||
<span class="tags-label">标签:</span>
|
||||
<div class="tags-list">
|
||||
<el-tag
|
||||
v-for="item in resource.tags"
|
||||
:key="item"
|
||||
class="resource_tag"
|
||||
@click="searchMovieforTag(item)"
|
||||
v-for="tag in currentResource.tags"
|
||||
:key="tag"
|
||||
class="tag-item"
|
||||
@click="searchMovieforTag(tag)"
|
||||
>
|
||||
{{ item }}
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="item-footer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="currentResource && handleSave(currentResource)"
|
||||
>转存</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<div v-for="group in store.resources" :key="group.id" class="resource-group">
|
||||
<div class="group-header">
|
||||
<el-link
|
||||
class="group-title"
|
||||
:href="`https://t.me/s/${group.id}`"
|
||||
target="_blank"
|
||||
:underline="false"
|
||||
>
|
||||
<el-image :src="group.channelInfo.channelLogo" class="channel-logo" fit="cover" lazy />
|
||||
<span>{{ group.channelInfo.name }}</span>
|
||||
<span class="item-count">({{ group.list.length }})</span>
|
||||
</el-link>
|
||||
|
||||
<el-tooltip effect="dark" :content="group.displayList ? '收起' : '展开'" placement="top">
|
||||
<el-button class="toggle-btn" type="text" @click="group.displayList = !group.displayList">
|
||||
<el-icon :class="{ 'is-active': group.displayList }">
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div v-show="group.displayList" class="group-content">
|
||||
<div class="card-grid">
|
||||
<el-card
|
||||
v-for="resource in group.list"
|
||||
:key="resource.messageId"
|
||||
class="resource-card-item"
|
||||
:body-style="{ padding: '0' }"
|
||||
>
|
||||
<div class="card-wrapper">
|
||||
<div class="card-cover">
|
||||
<el-image
|
||||
class="cover-image"
|
||||
:src="`/tele-images/?url=${encodeURIComponent(resource.image as string)}`"
|
||||
fit="cover"
|
||||
lazy
|
||||
:alt="resource.title"
|
||||
@click="showResourceDetail(resource)"
|
||||
/>
|
||||
<el-tag
|
||||
class="cloud-type"
|
||||
:type="store.tagColor[resource.cloudType as keyof TagColor]"
|
||||
effect="dark"
|
||||
round
|
||||
size="small"
|
||||
>
|
||||
{{ resource.cloudType }}
|
||||
</el-tag>
|
||||
<el-button @click="handleSave(resource)">转存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="card-body">
|
||||
<el-link
|
||||
class="card-title"
|
||||
:href="resource.cloudLinks[0]"
|
||||
target="_blank"
|
||||
:underline="false"
|
||||
>
|
||||
{{ resource.title }}
|
||||
</el-link>
|
||||
|
||||
<div
|
||||
class="card-content"
|
||||
@click="showResourceDetail(resource)"
|
||||
v-html="resource.content"
|
||||
/>
|
||||
|
||||
<div v-if="resource.tags?.length" class="card-tags">
|
||||
<span class="tags-label">标签:</span>
|
||||
<div class="tags-list">
|
||||
<el-tag
|
||||
v-for="tag in resource.tags"
|
||||
:key="tag"
|
||||
class="tag-item"
|
||||
@click="searchMovieforTag(tag)"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<el-button type="primary" @click="handleSave(resource)">转存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="group.displayList" class="load-more">
|
||||
<el-button @click="handleLoadMore(group.id)"> 加载更多 </el-button>
|
||||
|
||||
<div class="load-more">
|
||||
<el-button :loading="group.loading" @click="handleLoadMore(group.id)">
|
||||
<el-icon><Plus /></el-icon>
|
||||
加载更多
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,19 +157,28 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useResourceStore } from "@/stores/resource";
|
||||
import { computed } from "vue";
|
||||
import { ref } from "vue";
|
||||
import type { ResourceItem, TagColor } from "@/types";
|
||||
import { ArrowDown, Plus } from "@element-plus/icons-vue";
|
||||
|
||||
const store = useResourceStore();
|
||||
|
||||
const location = computed(() => window.location);
|
||||
const showDetail = ref(false);
|
||||
const currentResource = ref<ResourceItem | null>(null);
|
||||
|
||||
const emit = defineEmits(["save", "loadMore", "searchMovieforTag"]);
|
||||
|
||||
const handleSave = (resource: ResourceItem) => {
|
||||
if (showDetail.value) {
|
||||
showDetail.value = false;
|
||||
}
|
||||
emit("save", resource);
|
||||
};
|
||||
|
||||
const showResourceDetail = (resource: ResourceItem) => {
|
||||
currentResource.value = resource;
|
||||
showDetail.value = true;
|
||||
};
|
||||
|
||||
const searchMovieforTag = (tag: string) => {
|
||||
emit("searchMovieforTag", tag);
|
||||
};
|
||||
@@ -85,105 +188,391 @@ const handleLoadMore = (channelId: string) => {
|
||||
};
|
||||
</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;
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/common.scss";
|
||||
|
||||
.resource-card {
|
||||
position: relative;
|
||||
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;
|
||||
|
||||
// 资源组
|
||||
.resource-group {
|
||||
background: var(--theme-card-bg);
|
||||
backdrop-filter: var(--theme-blur);
|
||||
-webkit-backdrop-filter: var(--theme-blur);
|
||||
margin-bottom: 24px;
|
||||
border-radius: var(--theme-radius);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
}
|
||||
.header-icon {
|
||||
cursor: pointer;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
|
||||
// 组标题
|
||||
.group-header {
|
||||
@include flex-center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
|
||||
.group-title {
|
||||
@include flex-center;
|
||||
gap: 12px;
|
||||
font-size: 16px;
|
||||
color: var(--theme-text-primary);
|
||||
transition: var(--theme-transition);
|
||||
|
||||
.channel-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--theme-shadow-sm);
|
||||
}
|
||||
|
||||
.item-count {
|
||||
font-size: 13px;
|
||||
color: var(--theme-text-secondary);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
color: var(--theme-text-regular);
|
||||
transition: var(--theme-transition);
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&.is-active {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组内容
|
||||
.group-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
// 卡片网格
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
gap: 24px;
|
||||
grid-auto-rows: min-content;
|
||||
}
|
||||
|
||||
// 资源卡片
|
||||
.resource-card-item {
|
||||
border-radius: var(--theme-radius);
|
||||
transition: var(--theme-transition);
|
||||
overflow: hidden;
|
||||
max-width: 460px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--theme-shadow);
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-cover {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 180px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: var(--theme-radius);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.cloud-type {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.card-title {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: var(--theme-text-primary);
|
||||
word-break: break-word;
|
||||
height: 3em;
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text-regular);
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
margin-top: auto;
|
||||
max-height: 88px;
|
||||
overflow: hidden;
|
||||
|
||||
.tags-label {
|
||||
font-size: 13px;
|
||||
color: var(--theme-text-secondary);
|
||||
margin-right: 8px;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tags-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
max-height: 72px;
|
||||
overflow: hidden;
|
||||
|
||||
.tag-item {
|
||||
cursor: pointer;
|
||||
transition: var(--theme-transition);
|
||||
margin: 0;
|
||||
height: 24px;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
border-color: var(--theme-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
@include flex-center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
|
||||
.el-button {
|
||||
padding: 6px 16px;
|
||||
font-size: 14px;
|
||||
height: 32px;
|
||||
min-width: 80px;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--theme-shadow-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
.load-more {
|
||||
@include flex-center;
|
||||
position: relative;
|
||||
padding: 32px 0 8px;
|
||||
margin-top: 16px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
var(--el-border-color-lighter) 20%,
|
||||
var(--el-border-color-lighter) 80%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.el-button {
|
||||
min-width: 160px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
color: var(--theme-text-regular);
|
||||
background: var(--theme-card-bg);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
transition: var(--theme-transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.6s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
border-color: var(--theme-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
|
||||
&::after {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
color: var(--theme-text-secondary);
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-right: 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 详情弹窗样式
|
||||
.resource-detail-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.detail-cover {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
border-radius: var(--theme-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cloud-type {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.detail-title {
|
||||
font-size: 18px;
|
||||
margin: 0 0 16px;
|
||||
line-height: 1.5;
|
||||
color: var(--theme-text-primary);
|
||||
}
|
||||
|
||||
.detail-description {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--theme-text-regular);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-tags {
|
||||
.tags-label {
|
||||
font-size: 13px;
|
||||
color: var(--theme-text-secondary);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.tags-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
|
||||
.tag-item {
|
||||
cursor: pointer;
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
border-color: var(--theme-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.item-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,74 +1,178 @@
|
||||
<template>
|
||||
<div class="folder-select">
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="resourceStore.shareInfo.list"
|
||||
:props="defaultProps"
|
||||
:default-checked-keys="resourceStore.shareInfo.list?.map((x) => x.fileId) || []"
|
||||
node-key="fileId"
|
||||
show-checkbox
|
||||
highlight-current
|
||||
@check-change="handleCheckChange"
|
||||
>
|
||||
<template #default="{ node }">
|
||||
<span class="folder-node">
|
||||
<el-icon><Folder /></el-icon>
|
||||
{{ node.data.fileName }}
|
||||
<span v-if="node.data.fileSize" style="font-weight: bold"
|
||||
>({{ formattedFileSize(node.data.fileSize) }})</span
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
<div class="resource-select">
|
||||
<div class="select-header">
|
||||
<div class="select-info">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>已选择 {{ selectedCount }} 个文件</span>
|
||||
<span v-if="totalSize" class="total-size">({{ formattedFileSize(totalSize) }})</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button type="text" @click="handleSelectAll(!hasSelectedAll)">
|
||||
{{ hasSelectedAll ? "取消全选" : "全选" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-list">
|
||||
<div
|
||||
v-for="file in resourceStore.shareInfo.list"
|
||||
:key="file.fileId"
|
||||
class="file-item"
|
||||
:class="{ 'is-checked': isChecked(file.fileId) }"
|
||||
@click="toggleSelect(file)"
|
||||
>
|
||||
<el-checkbox :model-value="isChecked(file.fileId)" @click.stop>
|
||||
<div class="file-info">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span class="file-name">{{ file.fileName }}</span>
|
||||
<span v-if="file.fileSize" class="file-size">
|
||||
{{ formattedFileSize(file.fileSize) }}
|
||||
</span>
|
||||
</div>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useResourceStore } from "@/stores/resource";
|
||||
import { formattedFileSize } from "@/utils/index";
|
||||
import { computed } from "vue";
|
||||
import type { ShareInfo } from "@/types";
|
||||
import { Document } from "@element-plus/icons-vue";
|
||||
|
||||
const resourceStore = useResourceStore();
|
||||
|
||||
const defaultProps = {
|
||||
isLeaf: "leaf",
|
||||
const selectedCount = computed(
|
||||
() => resourceStore.resourceSelect.filter((x) => x.isChecked).length
|
||||
);
|
||||
|
||||
const totalSize = computed(() =>
|
||||
resourceStore.resourceSelect
|
||||
.filter((x) => x.isChecked)
|
||||
.reduce((sum, item) => sum + (item.fileSize || 0), 0)
|
||||
);
|
||||
|
||||
const totalFiles = computed(() => resourceStore.shareInfo.list.length);
|
||||
|
||||
const hasSelectedAll = computed(() => selectedCount.value === totalFiles.value);
|
||||
|
||||
const isChecked = (fileId: string) => {
|
||||
return resourceStore.resourceSelect.find((x) => x.fileId === fileId)?.isChecked;
|
||||
};
|
||||
|
||||
const handleCheckChange = (data: ShareInfo) => {
|
||||
const toggleSelect = (file: ShareInfo) => {
|
||||
let resourceSelect = [...resourceStore.resourceSelect];
|
||||
resourceSelect.forEach((x) => {
|
||||
if (x.fileId === data.fileId) x.isChecked = !x.isChecked;
|
||||
});
|
||||
const item = resourceSelect.find((x) => x.fileId === file.fileId);
|
||||
if (item) {
|
||||
item.isChecked = !item.isChecked;
|
||||
resourceStore.setSelectedResource(resourceSelect);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
const resourceSelect = resourceStore.shareInfo.list.map((file) => ({
|
||||
fileId: file.fileId,
|
||||
fileName: file.fileName,
|
||||
fileSize: file.fileSize,
|
||||
isChecked: checked,
|
||||
}));
|
||||
resourceStore.setSelectedResource(resourceSelect);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.folder-select {
|
||||
min-height: 300px;
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/responsive.scss";
|
||||
|
||||
.resource-select {
|
||||
min-height: 200px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.folder-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.folder-path {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.folder-select-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid #e5e6e8;
|
||||
border-radius: 8px;
|
||||
.select-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: var(--theme-radius);
|
||||
|
||||
.select-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--theme-text-regular);
|
||||
font-size: 14px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.total-size {
|
||||
color: var(--theme-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
font-size: 13px;
|
||||
padding: 4px 8px;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
|
||||
.file-item {
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--theme-radius);
|
||||
cursor: pointer;
|
||||
transition: var(--theme-transition);
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--theme-text-primary);
|
||||
font-size: 14px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
color: var(--theme-text-regular);
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
@include text-ellipsis;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: var(--theme-text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -61,14 +61,6 @@
|
||||
<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>
|
||||
@@ -195,4 +187,10 @@ const searchMovieforTag = (tag: string) => {
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.resource-table {
|
||||
position: relative;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user