feat:增加转存资源列表展示与选择

This commit is contained in:
jiangrui
2025-02-25 12:53:27 +08:00
parent 8668bce863
commit c290f5a6d9
10 changed files with 226 additions and 45 deletions

View File

@@ -5,5 +5,5 @@
// Generated by unplugin-auto-import
export {}
declare global {
const ElMessage: (typeof import("element-plus/es"))["ElMessage"]
const ElMessage: typeof import('element-plus/es')['ElMessage']
}

View File

@@ -8,6 +8,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
AsideMenu: typeof import('./src/components/AsideMenu.vue')['default']
copy: typeof import('./src/components/Home/FolderSelect copy.vue')['default']
DoubanMovie: typeof import('./src/components/Home/DoubanMovie.vue')['default']
ElAside: typeof import('element-plus/es')['ElAside']
ElBacktop: typeof import('element-plus/es')['ElBacktop']
@@ -39,6 +40,7 @@ declare module 'vue' {
ElTree: typeof import('element-plus/es')['ElTree']
FolderSelect: typeof import('./src/components/Home/FolderSelect.vue')['default']
ResourceCard: typeof import('./src/components/Home/ResourceCard.vue')['default']
ResourceSelect: typeof import('./src/components/Home/ResourceSelect.vue')['default']
ResourceTable: typeof import('./src/components/Home/ResourceTable.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

View File

@@ -31,6 +31,9 @@ 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";
const resourceStore = useResourceStore();
import { ElMessage } from "element-plus";
const props = defineProps({
@@ -59,10 +62,12 @@ const cloudTypeApiMap = {
quark: quarkApi,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const loadNode = async (node: any, resolve: (list: Folder[]) => void) => {
const api = cloudTypeApiMap[props.cloudType as keyof typeof cloudTypeApiMap];
try {
let res: RequestResult<Folder[]> = { code: 0, data: [] as Folder[], message: "" };
resourceStore.setLoadTree(true);
if (node.level === 0) {
if (api.getFolderList) {
// 使用类型保护检查方法是否存在
@@ -79,10 +84,12 @@ const loadNode = async (node: any, resolve: (list: Folder[]) => void) => {
} else {
throw new Error(res.message);
}
resourceStore.setLoadTree(false);
} catch (error) {
ElMessage.error(error instanceof Error ? `${error.message}` : "获取目录失败");
// 关闭模态框
emit("close");
resourceStore.setLoadTree(false);
resolve([]);
}
};

View File

@@ -0,0 +1,82 @@
<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>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useResourceStore } from "@/stores/resource";
import { formattedFileSize } from "@/utils/index";
import type { ShareInfo } from "@/types";
const resourceStore = useResourceStore();
const selectedResource = ref<ShareInfo[]>([]);
const defaultProps = {
isLeaf: "leaf",
};
const handleCheckChange = (data: ShareInfo) => {
selectedResource.value = [...resourceStore.resourceSelect, ...selectedResource.value];
if (selectedResource.value.findIndex((x) => x.fileId === data.fileId) === -1) {
selectedResource.value.push(data);
} else {
selectedResource.value = selectedResource.value.filter((x) => x.fileId !== data.fileId);
}
resourceStore.setSelectedResource(selectedResource.value);
};
</script>
<style scoped>
.folder-select {
min-height: 300px;
max-height: 500px;
overflow-y: auto;
}
.folder-node {
display: flex;
align-items: center;
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;
}
</style>

View File

@@ -7,6 +7,7 @@ import type {
ShareInfoResponse,
Save115FileParams,
SaveQuarkFileParams,
ShareInfo,
ResourceItem,
} from "@/types";
import { ElMessage } from "element-plus";
@@ -101,13 +102,18 @@ export const useResourceStore = defineStore("resource", {
},
resources: lastResource.list,
lastUpdateTime: lastResource.lastUpdateTime || "",
selectedResources: [] as Resource[],
shareInfo: {} as ShareInfoResponse,
resourceSelect: [] as ShareInfo[],
loading: false,
lastKeyWord: "",
backupPlan: false,
loadTree: false,
}),
actions: {
setLoadTree(loadTree: boolean) {
this.loadTree = loadTree;
},
// 搜索资源
async searchResources(keyword?: string, isLoadMore = false, channelId?: string): Promise<void> {
this.loading = true;
@@ -153,6 +159,11 @@ export const useResourceStore = defineStore("resource", {
}
},
// 设置选择资源
async setSelectedResource(resourceSelect: ShareInfo[]) {
this.resourceSelect = resourceSelect;
},
// 转存资源
async saveResource(resource: ResourceItem, folderId: string): Promise<void> {
const savePromises: Promise<void>[] = [];
@@ -178,25 +189,12 @@ export const useResourceStore = defineStore("resource", {
const match = link.match(drive.regex);
if (!match) throw new Error("链接解析失败");
const parsedCode = drive.parseShareCode(match);
const shareInfo = {
...this.shareInfo,
list: this.resourceSelect,
};
if (this.is115Drive(drive)) {
let shareInfo = await drive.api.getShareInfo(
parsedCode as { shareCode: string; receiveCode: string }
);
if (shareInfo) {
if (Array.isArray(shareInfo)) {
shareInfo = {
list: shareInfo,
...parsedCode,
};
} else {
shareInfo = {
...shareInfo,
...parsedCode,
};
}
}
const params = drive.getSaveParams(shareInfo, folderId);
const result = await drive.api.saveFile(params);
@@ -206,16 +204,6 @@ export const useResourceStore = defineStore("resource", {
ElMessage.error(result.message);
}
} else {
let shareInfo = this.is115Drive(drive)
? await drive.api.getShareInfo(parsedCode as { shareCode: string; receiveCode: string })
: await drive.api.getShareInfo(parsedCode as { pwdId: string });
if (shareInfo) {
if (Array.isArray(shareInfo)) {
shareInfo = {
list: shareInfo,
};
}
}
const params = drive.getSaveParams(shareInfo, folderId);
const result = await drive.api.saveFile(params);
@@ -283,6 +271,49 @@ export const useResourceStore = defineStore("resource", {
}
},
// 获取资源列表并选择
async getResourceListAndSelect(resource: ResourceItem): Promise<void> {
const { cloudType } = resource;
const drive = CLOUD_DRIVES.find((x) => x.type === cloudType);
if (!drive) {
return;
}
const link = resource.cloudLinks.find((link) => drive.regex.test(link));
if (!link) return;
const match = link.match(drive.regex);
if (!match) throw new Error("链接解析失败");
const parsedCode = drive.parseShareCode(match);
let shareInfo = {} as ShareInfoResponse;
this.setLoadTree(true);
if (this.is115Drive(drive)) {
shareInfo = await drive.api.getShareInfo(
parsedCode as { shareCode: string; receiveCode: string }
);
} else {
shareInfo = this.is115Drive(drive)
? await drive.api.getShareInfo(parsedCode as { shareCode: string; receiveCode: string })
: await drive.api.getShareInfo(parsedCode as { pwdId: string });
}
if (shareInfo) {
if (Array.isArray(shareInfo)) {
shareInfo = {
list: shareInfo,
...parsedCode,
};
} else {
shareInfo = {
...shareInfo,
...parsedCode,
};
}
}
this.shareInfo = shareInfo;
this.setSelectedResource(this.shareInfo.list);
this.setLoadTree(false);
},
// 统一错误处理
handleError(message: string, error: unknown): void {
console.error(message, error);

View File

@@ -26,7 +26,7 @@ export interface Resource {
export interface ShareInfo {
fileId: string;
fileName: string;
fileSize: number;
fileSize?: number;
fileIdToken?: string;
}
@@ -36,6 +36,7 @@ export interface ShareInfoResponse {
stoken?: string;
shareCode?: string;
receiveCode?: string;
fileSize?: number;
}
export interface Folder {

View File

@@ -0,0 +1,9 @@
export const formattedFileSize = (size: number): string => {
if (size < 1024 * 1024) {
return `${(size / 1024).toFixed(2)}KB`;
}
if (size < 1024 * 1024 * 1024) {
return `${(size / 1024 / 1024).toFixed(2)}MB`;
}
return `${(size / 1024 / 1024 / 1024).toFixed(2)}GB`;
};

View File

@@ -4,7 +4,7 @@ import { RequestResult } from "../types/response";
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL as string,
timeout: 9000,
timeout: 16000,
withCredentials: true,
headers: {
"Content-Type": "application/json",

View File

@@ -32,7 +32,11 @@
@save="handleSave"
></ResourceCard>
<el-empty v-if="resourceStore.resources.length === 0" :image-size="200" />
<el-dialog v-if="currentResource" v-model="folderDialogVisible" title="选择保存目录">
<el-dialog
v-if="currentResource"
v-model="saveDialogVisible"
:title="saveDialogMap[saveDialogStep].title"
>
<template #header="{ titleId }">
<div class="my-header">
<div :id="titleId">
@@ -43,19 +47,34 @@
>
{{ currentResource.cloudType }}
</el-tag>
选择保存目录
{{ saveDialogMap[saveDialogStep].title }}
<span
v-if="resourceStore.shareInfo.fileSize && saveDialogStep === 1"
style="font-weight: bold"
>
({{ formattedFileSize(resourceStore.shareInfo.fileSize || 0) }})
</span>
</div>
</div>
</template>
<folder-select
v-if="folderDialogVisible"
:cloud-type="currentResource.cloudType"
@select="handleFolderSelect"
@close="folderDialogVisible = false"
/>
<div v-loading="resourceStore.loadTree">
<resource-select
v-if="saveDialogVisible && saveDialogStep === 1"
:cloud-type="currentResource.cloudType"
/>
<folder-select
v-if="saveDialogVisible && saveDialogStep === 2"
:cloud-type="currentResource.cloudType"
@select="handleFolderSelect"
@close="saveDialogVisible = false"
/>
</div>
<div class="dialog-footer">
<el-button @click="folderDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveBtnClick">保存</el-button>
<el-button @click="saveDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfirmClick">{{
saveDialogMap[saveDialogStep].buttonText
}}</el-button>
</div>
</el-dialog>
</div>
@@ -66,25 +85,43 @@ import { ref } from "vue";
import { useResourceStore } from "@/stores/resource";
import { useUserSettingStore } from "@/stores/userSetting";
import FolderSelect from "@/components/Home/FolderSelect.vue";
import ResourceSelect from "@/components/Home/ResourceSelect.vue";
import ResourceTable from "@/components/Home/ResourceTable.vue";
import { formattedFileSize } from "@/utils/index";
import type { ResourceItem, TagColor } from "@/types";
import ResourceCard from "@/components/Home/ResourceCard.vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
const router = useRouter();
const resourceStore = useResourceStore();
const userStore = useUserSettingStore();
const folderDialogVisible = ref(false);
const saveDialogVisible = ref(false);
const currentResource = ref<ResourceItem | null>(null);
const currentFolderId = ref<string | null>(null);
const saveDialogStep = ref<1 | 2>(1);
const refreshResources = async () => {
resourceStore.searchResources("", false);
};
const saveDialogMap = {
1: {
title: "选择资源",
buttonText: "下一步",
},
2: {
title: "选择保存目录",
buttonText: "保存",
},
};
const handleSave = (resource: ResourceItem) => {
currentResource.value = resource;
folderDialogVisible.value = true;
saveDialogVisible.value = true;
saveDialogStep.value = 1;
resourceStore.getResourceListAndSelect(currentResource.value);
};
const handleFolderSelect = async (folderId: string) => {
@@ -92,9 +129,21 @@ const handleFolderSelect = async (folderId: string) => {
currentFolderId.value = folderId;
};
const handleConfirmClick = async () => {
if (saveDialogStep.value === 1) {
if (resourceStore.resourceSelect.length === 0) {
ElMessage.warning("请选择要保存的资源");
return;
}
saveDialogStep.value = 2;
} else {
handleSaveBtnClick();
}
};
const handleSaveBtnClick = async () => {
if (!currentResource.value || !currentFolderId.value) return;
folderDialogVisible.value = false;
saveDialogVisible.value = false;
await resourceStore.saveResource(currentResource.value, currentFolderId.value);
};
const setDisplayStyle = (style: string) => {

View File

@@ -1,6 +1,6 @@
{
"name": "cloud-saver",
"version": "0.1.0",
"version": "0.1.1",
"private": true,
"workspaces": [
"frontend",