diff --git a/tone-page-web/app/console/(with-menu)/storage/components/UploadManager.tsx b/tone-page-web/app/console/(with-menu)/storage/components/UploadManager.tsx index 56a3608..3955f85 100644 --- a/tone-page-web/app/console/(with-menu)/storage/components/UploadManager.tsx +++ b/tone-page-web/app/console/(with-menu)/storage/components/UploadManager.tsx @@ -16,11 +16,11 @@ import React, { useRef, useState } from "react"; import { Progress } from '@/components/ui/progress'; import { cn } from "@/lib/utils"; import { toast } from "sonner"; +import { OssStore } from "@/lib/oss/OssStore"; interface UploadManagerProps { children: React.ReactNode; - store?: OSS; - basePath?: string; + ossStore?: OssStore; handleRefreshFileList?: () => void; } @@ -31,7 +31,7 @@ interface UploadFileItem { progress: number;// 0 ~ 100 } -export function UploadManager({ children, store, basePath, handleRefreshFileList }: UploadManagerProps) { +export function UploadManager({ children, ossStore, handleRefreshFileList }: UploadManagerProps) { const [filesList, setFileList] = useState([]); const handleFileSelect = (fileList: FileList) => { @@ -105,10 +105,10 @@ export function UploadManager({ children, store, basePath, handleRefreshFileList // 开始上传文件 const startUploadFile = async (fileItem: UploadFileItem) => { - if (!store || !basePath) return; + if (!ossStore) return; let checkpoint: any; - await store.multipartUpload(`${basePath}/${fileItem.file.name}`, fileItem.file, { + await ossStore.storeMeta.store?.multipartUpload(`${ossStore.getWorkDir()}/${fileItem.file.name}`, fileItem.file, { checkpoint: checkpoint, progress: (p, cpt, res) => { setFileList(currentFileList => { diff --git a/tone-page-web/app/console/(with-menu)/storage/page.tsx b/tone-page-web/app/console/(with-menu)/storage/page.tsx index ccff0eb..392a7ab 100644 --- a/tone-page-web/app/console/(with-menu)/storage/page.tsx +++ b/tone-page-web/app/console/(with-menu)/storage/page.tsx @@ -15,6 +15,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { UploadManager } from './components/UploadManager'; +import { OssStore } from '@/lib/oss/OssStore'; const formatSizeNumber = (n: number) => { @@ -30,114 +31,48 @@ const formatSizeNumber = (n: number) => { } } -interface FileItem { - id: string; - name: string; - size: number;// Byte - lastModified: Date; - isChecked: boolean; -}; + export default function Page() { - const { stsTokenData, isLoading, error, store } = useOssStore(); + const ossStore = new OssStore({ + prefix: 'tone-page', + prefixAddUserId: true, + }); + const objectList = ossStore.useObjectList; - const [fileList, setFileList] = useState(null); - - const handleRefreshFileList = async () => { - if (!store || !stsTokenData) return toast.error('初始化失败,请刷新界面重试'); - setFileList(null); - const res = await store.listV2({ prefix: `tone-page/${stsTokenData.userId}` }, {}).catch(e => { - toast.error('文件列表加载失败'); - }); - - if (res && res.objects) { - setFileList(res.objects.map(v => ({ - id: v.name, - name: v.name.replace(`tone-page/${stsTokenData.userId}/`, ''), - size: v.size, - lastModified: new Date(v.lastModified), - isChecked: false, - }))); - } - } - - const handleCheckboxChange = (id: string, checked: boolean) => { - setFileList((prevFileList) => { - if (!prevFileList) return null; - - return prevFileList.map(item => { - if (item.id === id) { - return { ...item, isChecked: checked }; - } - return item; - }) - }) - } - - useEffect(() => { - store && stsTokenData && handleRefreshFileList(); - }, [stsTokenData]); + const handleRefreshFileList = async () => ossStore.loadObjectList().catch(e => toast.error(e.message)); + const handleCheckboxChange = ossStore.handleObjectCheckedStateChanged.bind(ossStore); const checkedFileIds = useMemo(() => { - if (!fileList) return null; - return fileList.filter(i => i.isChecked).map(i => i.id); - }, [fileList]) + return (objectList || []).filter(i => i.isChecked).map(i => i.id); + }, [objectList]) + const handleDeleteObject = async (id: string) => { + await ossStore.deleteObject(id) + .then(() => ossStore.loadObjectList()) + .catch(e => toast.error(`${e.message}`)) + } const handleDeleteCheckedFiles = async () => { - if (!store || !stsTokenData) return toast.error('初始化未完成或失败,请等待或重新加载'); - const deleteFiles = (fileList || []).filter(i => i.isChecked); - if (!deleteFiles) return toast.error('请选择需要删除的文件'); + const res = await ossStore.deleteCheckedObjects(); - let failedCount = 0; - for (let item of deleteFiles) { - await deleteFile(item.name).catch(e => { failedCount++; return e; }); - } - - if (failedCount > 0) { - toast.warning(`删除完成,共有${failedCount}个文件删除失败`) + if (res.failed > 0) { + toast.warning(`删除完成,共有${res.failed}个文件删除失败`) } else { - toast.success(`${deleteFiles.length}个文件删除完成`) + toast.success(`${res.all}个文件删除完成`) } handleRefreshFileList(); } - const handleDeleteFile = async (fileItem: FileItem) => { - await deleteFile(fileItem.name) - .then(() => toast.success('删除完成')) - .catch(() => toast.error('删除失败')); - handleRefreshFileList(); - } - - const deleteFile = async (localFilename: string) => { - if (!store) throw new Error('store未初始化'); - if (!stsTokenData) throw new Error('sts服务未初始化'); - return store.delete(`/tone-page/${stsTokenData.userId}/${localFilename}`); - } - - const downloadFile = (localFilename: string) => { - if (!store) throw new Error('store未初始化'); - if (!stsTokenData) throw new Error('sts服务未初始化'); - const url = store.signatureUrl(`/tone-page/${stsTokenData.userId}/${localFilename}`); - const a = document.createElement('a'); - a.href = url; - a.download = localFilename; - a.target = '_blank'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - } - - const handleDownloadFile = async (fileItem: FileItem) => { - downloadFile(fileItem.name); + const handleDownloadObject = async (id: string) => { + ossStore.downloadObject(id).catch(e => toast.error(`${e.message}`)); } return (
@@ -145,19 +80,19 @@ export default function Page() {
- {fileList &&
共有 {fileList.length} 个文件,目前最大支持100个文件
} + {objectList &&
共有 {objectList.length} 个文件,目前最大支持100个文件
}
- {(isLoading || (fileList == null && !error)) &&
加载中...
} + {/* {(ossStore.isLoading || ( == null && !error)) &&
加载中...
} {error &&
{`${error}`}
} - {fileList && fileList.length === 0 &&
暂无文件
} + {fileList && fileList.length === 0 &&
暂无文件
} */}
@@ -169,7 +104,7 @@ export default function Page() { { - fileList && fileList.map(d => ( + objectList && objectList.map(d => ( handleCheckboxChange(d.id, Boolean(v))} /> @@ -180,9 +115,9 @@ export default function Page() { {d.name} - handleDownloadFile(d)}>下载 + handleDownloadObject(d.id)}>下载 {/* 编辑 */} - handleDeleteFile(d)}>删除 + handleDeleteObject(d.id)}>删除 diff --git a/tone-page-web/hooks/admin/web/blog/use-oss-store.ts b/tone-page-web/hooks/admin/web/blog/use-oss-store.ts index 7a9004a..3ef6cb2 100644 --- a/tone-page-web/hooks/admin/web/blog/use-oss-store.ts +++ b/tone-page-web/hooks/admin/web/blog/use-oss-store.ts @@ -1,16 +1,22 @@ import { useOssSts } from "@/hooks/oss/use-oss-sts"; +import { StsToken } from "@/lib/api/oss"; import OSS from "ali-oss"; +import { useEffect } from "react"; -export function useOssStore() { +export function useOssStore(options: { region: string; bucket: string; onStsTokenDataChanged?: (data: StsToken | undefined) => void; }) { const { stsTokenData, isLoading, error } = useOssSts(); + useEffect(() => { + options.onStsTokenDataChanged?.(stsTokenData); + }, [stsTokenData]) + return { stsTokenData, isLoading, error, store: stsTokenData ? new OSS({ - region: 'oss-cn-chengdu', - bucket: 'tone-personal', + region: options.region, + bucket: options.bucket, accessKeyId: stsTokenData.AccessKeyId, accessKeySecret: stsTokenData.AccessKeySecret, stsToken: stsTokenData.SecurityToken, diff --git a/tone-page-web/lib/api/oss/index.ts b/tone-page-web/lib/api/oss/index.ts index c3e42f3..4bb3c15 100644 --- a/tone-page-web/lib/api/oss/index.ts +++ b/tone-page-web/lib/api/oss/index.ts @@ -1,11 +1,13 @@ import fetcher from "../fetcher"; +export interface StsToken { + AccessKeyId: string; + AccessKeySecret: string; + Expiration: string;// ISO 8601 格式 + SecurityToken: string; + userId: string; +} + export async function getStsToken() { - return fetcher<{ - AccessKeyId: string; - AccessKeySecret: string; - Expiration: string;// ISO 8601 格式 - SecurityToken: string; - userId: string; - }>('/api/oss/sts', { method: 'GET' }); + return fetcher('/api/oss/sts', { method: 'GET' }); } \ No newline at end of file diff --git a/tone-page-web/lib/oss/OssStore.ts b/tone-page-web/lib/oss/OssStore.ts new file mode 100644 index 0000000..c8c1978 --- /dev/null +++ b/tone-page-web/lib/oss/OssStore.ts @@ -0,0 +1,150 @@ +import { useOssStore } from "@/hooks/admin/web/blog/use-oss-store"; +import { Dispatch, SetStateAction, useEffect, useState } from "react"; + +interface OssObjectItem { + id: string; + name: string; + size: number;// Byte + lastModified: Date; + isChecked: boolean; +}; + +type OssObjectList = null | OssObjectItem[]; + +export class OssStore { + + private objectList: OssObjectList = null; + private setObjectList: Dispatch>; + + public storeMeta: ReturnType; + + constructor(private options: { + prefix?: string; + prefixAddUserId?: boolean; + } = {}) { + const [objectList, setObjectList] = useState(null); + this.objectList = objectList; + this.setObjectList = setObjectList; + + this.storeMeta = useOssStore({ + region: 'oss-cn-chengdu', + bucket: 'tone-personal', + onStsTokenDataChanged: (data) => { + if (!data) return; + this.loadObjectList(); + } + }); + } + + get useObjectList() { + return this.objectList; + } + + public async loadObjectList() { + const store = await this.getStore(); + this.setObjectList(null); + + const workDir = this.getWorkDir(); + const res = await store.listV2({ ...workDir ? { prefix: workDir } : {} }, {}); + if (!res || !res.objects) throw new Error('文件列表加载失败'); + + this.setObjectList(res.objects.map(v => ({ + id: v.name, + name: v.name.replace(`${workDir}/`, ''), + size: v.size, + lastModified: new Date(v.lastModified), + isChecked: false, + }))) + } + + public handleObjectCheckedStateChanged(id: string, value: boolean) { + this.setObjectList(current => current ? current.map(objectItem => { + if (objectItem.id === id) { + return { ...objectItem, isChecked: value } + } + return objectItem; + }) : null) + } + + public async deleteObject(id: string) { + if (!this.storeMeta.store || !this.storeMeta.stsTokenData) { + throw new Error('初始化失败,请刷新界面重试'); + } + const fileItem = (this.objectList || []).find(i => i.id === id); + if (!fileItem) throw new Error('文件不存在'); + + const objectName = this.getObjectName(fileItem.name); + const delRes = await this.storeMeta.store.delete(objectName).catch(() => null); + if (!delRes) throw new Error('删除失败'); + } + + public async deleteCheckedObjects() { + if (!this.storeMeta.store || !this.storeMeta.stsTokenData) { + throw new Error('初始化失败,请刷新界面重试'); + } + + const objects = (this.objectList || []).filter(i => i.isChecked); + if (objects.length === 0) throw new Error('请选择需要删除的文件'); + + let failedCount = 0; + for (const objectItem of objects) { + await this.deleteObject(objectItem.name).catch(e => failedCount++); + } + + return { all: objects.length, failed: failedCount }; + } + + public async downloadObject(id: string) { + if (!this.storeMeta.store || !this.storeMeta.stsTokenData) { + throw new Error('初始化失败,请刷新界面重试'); + } + + const objectItem = this.getObjectItemById(id); + const url = this.storeMeta.store.signatureUrl(this.getObjectName(objectItem.name)); + const a = document.createElement('a'); + a.href = url; + a.download = objectItem.name; + a.target = '_blank'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } + + private async getStore() { + if (!this.storeMeta.store || !this.storeMeta.stsTokenData) { + throw new Error('初始化失败,请刷新界面重试'); + } + return this.storeMeta.store; + } + + private getObjectName(localName: string) { + return `${this.getWorkDir()}/${localName}`; + } + + private getObjectNameById(id: string) { + const objectItem = this.getObjectItemById(id); + return this.getObjectName(objectItem.name); + } + + private getObjectItemById(id: string) { + const objectItem = (this.objectList || []).find(i => i.id === id); + if (!objectItem) throw new Error('文件不存在'); + return objectItem; + } + + public getWorkDir() { + const { stsTokenData } = this.storeMeta; + if (!stsTokenData) return; + + if (this.options.prefixAddUserId) { + return `${this.options.prefix ? `${this.options.prefix}/` : ''}${stsTokenData.userId}`; + } else { + return this.options.prefix; + } + } + + get isLoading() { + return this.storeMeta.isLoading; + } + +} \ No newline at end of file