mirror of
https://github.com/jiangrui1994/CloudSaver.git
synced 2026-01-10 15:18:46 +08:00
feat:增加转存资源列表展示与选择
This commit is contained in:
2
frontend/auto-imports.d.ts
vendored
2
frontend/auto-imports.d.ts
vendored
@@ -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']
|
||||
}
|
||||
|
||||
2
frontend/components.d.ts
vendored
2
frontend/components.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -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([]);
|
||||
}
|
||||
};
|
||||
|
||||
82
frontend/src/components/Home/ResourceSelect.vue
Normal file
82
frontend/src/components/Home/ResourceSelect.vue
Normal 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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
9
frontend/src/utils/index.ts
Normal file
9
frontend/src/utils/index.ts
Normal 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`;
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloud-saver",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"frontend",
|
||||
|
||||
Reference in New Issue
Block a user