// ==UserScript== // @name 123FastLink // @namespace http://tampermonkey.net/ // @version 2025.09.13.1 // @description Creat and save 123pan instant links. // @author Baoqing // @author Chaofan // @author lipkiat // @match *://*.123pan.com/* // @match *://*.123pan.cn/* // @icon https://www.google.com/s2/favicons?sz=64&domain=123pan.com // @grant none // @downloadURL https://update.greasyfork.icu/scripts/525210/123FastLink.user.js // @updateURL https://update.greasyfork.icu/scripts/525210/123FastLink.meta.js // ==/UserScript== (function () { 'use strict'; const GlobalConfig = { scriptVersion: "3.0.3", usesBase62EtagsInExport: true, getFileListPageDelay: 500, getFileInfoBatchSize: 100, getFileInfoDelay: 200, getFolderInfoDelay: 300, saveLinkDelay: 100, mkdirDelay: 100, scriptName: "123FASTLINKV3", COMMON_PATH_LINK_PREFIX_V2: "123FLCPV2$" }; // 1. 123云盘API通信类 class PanApiClient { constructor() { this.host = 'https://' + window.location.host; this.authToken = localStorage['authorToken']; this.loginUuid = localStorage['LoginUuid']; this.appVersion = '3'; this.referer = document.location.href; this.getFileListPageDelay = GlobalConfig.getFileListPageDelay; this.progress = 0; this.progressDesc = ""; } buildURL(path, queryParams) { const queryString = new URLSearchParams(queryParams || {}).toString(); return `${this.host}${path}?${queryString}`; } async sendRequest(method, path, queryParams, body) { const headers = { 'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Bearer ' + this.authToken, 'platform': 'web', 'App-Version': this.appVersion, 'LoginUuid': this.loginUuid, 'Origin': this.host, 'Referer': this.referer, }; try { const response = await fetch(this.buildURL(path, queryParams), { method, headers, body, credentials: 'include' }); const data = await response.json(); if (data.code !== 0) { throw new Error(data.message); } return data; } catch (e) { console.error('[123FASTLINK] [PanApiClient]', 'API请求失败:', e); throw e; } } async getOnePageFileList(parentFileId, page) { const urlParams = { //'2015049069': '1756010983-3879364-4059457292', driveId: '0', limit: '100', next: '0', orderBy: 'file_name', orderDirection: 'asc', parentFileId: parentFileId.toString(), trashed: 'false', SearchData: '', Page: page.toString(), OnlyLookAbnormalFile: '0', event: 'homeListFile', operateType: '1', inDirectSpace: 'false' }; const data = await this.sendRequest("GET", "/b/api/file/list/new", urlParams); //console.log("[123FASTLINK] [PanApiClient]", "获取文件列表:", data.data.InfoList); console.log("[123FASTLINK] [PanApiClient]", "获取文件列表 ID:", parentFileId, "Page:", page); return { data: { InfoList: data.data.InfoList }, total: data.data.Total }; //return { data: { fileList: data.data.fileList } }; } async getFileList(parentFileId) { let InfoList = []; this.progress = 0; this.progressDesc = `获取文件列表 文件夹ID:${parentFileId}`; // 默认一页100 // 先获取一次,得到Total console.log("[123FASTLINK] [PanApiClient]", "开始获取文件列表,ID:", parentFileId); const info = await this.getOnePageFileList(parentFileId, 1); InfoList.push(...info.data.InfoList); const total = info.total; if (total > 100) { const times = Math.ceil(total / 100); for (let i = 2; i < times + 1; i++) { this.progress = Math.ceil((i / times) * 100); // this.progressDesc = `获取文件列表: ${this.progress}%`; const pageInfo = await this.getOnePageFileList(parentFileId, i); InfoList.push(...pageInfo.data.InfoList); // 延时 await new Promise(resolve => setTimeout(resolve, this.getFileListPageDelay)); } } this.progress = 100; return { data: { InfoList }, total: total }; } async getFileInfo(idList) { const fileIdList = idList.map(fileId => ({ fileId })); const data = await this.sendRequest("POST", "/b/api/file/info", {}, JSON.stringify({ fileIdList })); return { data: { InfoList: data.data.infoList } }; } async uploadRequest(fileInfo) { try { const response = await this.sendRequest('POST', '/b/api/file/upload_request', {}, JSON.stringify({ ...fileInfo, RequestSource: null })); const reuse = response['data']['Reuse']; console.log('[123FASTLINK] [PanApiClient]', 'reuse:', reuse); if (response['code'] !== 0) { return [false, response['message']]; } if (!reuse) { console.error('[123FASTLINK] [PanApiClient]', '保存文件失败:', fileInfo.fileName, 'response:', response); return [false, "未能实现秒传"]; } else { return [true, null]; } } catch (error) { console.error('[123FASTLINK] [PanApiClient]', '上传请求失败:', error); return [false, '请求失败']; } } // 从sessionStorage中获取父级文件ID async getParentFileId() { const homeFilePath = JSON.parse(sessionStorage['filePath'])['homeFilePath']; const parentFileId = (homeFilePath[homeFilePath.length - 1] || 0); console.log('[123FASTLINK] [PanApiClient] parentFileId:', parentFileId); return parentFileId.toString(); } // 获取文件 async getFile(fileInfo, parentFileId) { if (!parentFileId) { parentFileId = await this.getParentFileId(); } return await this.uploadRequest({ driveId: 0, etag: fileInfo.etag, fileName: fileInfo.fileName, parentFileId, size: fileInfo.size, type: 0, duplicate: 1 }); } async mkdirInNowFolder(folderName = "New Folder") { const parentFileId = await this.getParentFileId(); return this.mkdir(parentFileId, folderName); } async mkdir(parentFileId, folderName = "New Folder") { let folderFileId = null; try { const response = await this.sendRequest('POST', '/b/api/file/upload_request', {}, JSON.stringify({ driveId: 0, etag: "", fileName: folderName, parentFileId, size: 0, type: 1, duplicate: 1, NotReuse: true, event: "newCreateFolder", operateType: 1, RequestSource: null })); folderFileId = response['data']['Info']['FileId']; } catch (error) { console.error('[123FASTLINK] [PanApiClient]', '创建文件夹失败:', error); return { 'folderFileId': null, 'folderName': folderName, 'success': false }; } console.log('[123FASTLINK] [PanApiClient]', '创建文件夹 ID:', folderFileId); return { 'folderFileId': folderFileId, 'folderName': folderName, 'success': true }; } } // 2. 选中文件管理类 class TableRowSelector { constructor() { this.selectedRowKeys = []; this.unselectedRowKeys = []; this.isSelectAll = false; this._inited = false; } init() { if (this._inited) return; this._inited = true; const originalCreateElement = document.createElement; const self = this; document.createElement = function (tagName, options) { const element = originalCreateElement.call(document, tagName, options); const observer = new MutationObserver(() => { if (element.classList.contains('ant-table-row') && element.classList.contains('ant-table-row-level-0') && element.classList.contains('editable-row')) { const input = element.querySelector('input'); if (input) { input.addEventListener('click', function () { const rowKey = element.getAttribute('data-row-key'); if (self.isSelectAll) { if (!this.checked) { if (!self.unselectedRowKeys.includes(rowKey)) { self.unselectedRowKeys.push(rowKey); } } else { const idx = self.unselectedRowKeys.indexOf(rowKey); if (idx > -1) { self.unselectedRowKeys.splice(idx, 1); } } } else { if (this.checked) { if (!self.selectedRowKeys.includes(rowKey)) { self.selectedRowKeys.push(rowKey); } } else { const idx = self.selectedRowKeys.indexOf(rowKey); if (idx > -1) { self.selectedRowKeys.splice(idx, 1); } } } self._outputSelection(); }); } observer.disconnect(); } else if (// 检查是否为全选框并绑定事件 element.classList.contains('ant-checkbox-input') && element.getAttribute('aria-label') === 'Select all') { // 新建全选框时 如果父元素没有ant-checkbox-indeterminate或ant-checkbox-checked的class值 // 则是切换页面而非点击刷新按钮,或者没有选择,此时所有清除选择缓存。 if (!(element.parentElement.classList.contains('ant-checkbox-indeterminate') || element.parentElement.classList.contains('ant-checkbox-checked'))) { self.unselectedRowKeys = []; self.selectedRowKeys = []; self.isSelectAll = false; } self._bindSelectAllEvent(element); console.log('[123FASTLINK] [Selector] 已为全选框绑定事件'); } else if (// 取消选择按钮 element.classList.contains('ant-btn') && element.classList.contains('ant-btn-link') && element.classList.contains('ant-btn-color-link') && element.classList.contains('ant-btn-variant-link') && element.classList.contains('mfy-button')) { element.addEventListener('click', function () { self.selectedRowKeys = []; self.unselectedRowKeys = []; self.isSelectAll = false; self._outputSelection && self._outputSelection(); }); } }); observer.observe(element, { attributes: true, attributeFilter: ['class', 'aria-label'] }); return element; }; console.log('[123FASTLINK] [Selector] CreatElement监听已激活'); } _bindSelectAllEvent(checkbox) { if (checkbox.dataset.selectAllBound) return; checkbox.dataset.selectAllBound = 'true'; checkbox.addEventListener('click', () => { if (checkbox.checked) { this.isSelectAll = true; this.unselectedRowKeys = []; this.selectedRowKeys = []; } else { this.isSelectAll = false; this.selectedRowKeys = []; this.unselectedRowKeys = []; } this._outputSelection(); }); } _outputSelection() { if (this.isSelectAll) { if (this.unselectedRowKeys.length === 0) { console.log('[123FASTLINK] [Selector]', '全选'); } else { console.log('[123FASTLINK] [Selector]', '全选,反选这些:', this.unselectedRowKeys); } } else { console.log('[123FASTLINK] [Selector]', '当前选中:', this.selectedRowKeys); } } getSelection() { return { isSelectAll: this.isSelectAll, selectedRowKeys: [...this.selectedRowKeys], unselectedRowKeys: [...this.unselectedRowKeys] }; } } // 3. 秒传链接生成/转存类 class ShareLinkManager { constructor(apiClient) { this.apiClient = apiClient; // this.selector = selector; this.progress = 0; this.progressDesc = ""; this.taskCancel = false; // 取消当前任务的请求标志 this.getFileInfoBatchSize = GlobalConfig.getFileInfoBatchSize; this.getFileInfoDelay = GlobalConfig.getFileInfoDelay; this.getFolderInfoDelay = GlobalConfig.getFolderInfoDelay; this.saveLinkDelay = GlobalConfig.saveLinkDelay; this.mkdirDelay = GlobalConfig.mkdirDelay; this.fileInfoList = []; // this.scriptName = GlobalConfig.scriptName, this.commonPath = ""; this.COMMON_PATH_LINK_PREFIX_V2 = GlobalConfig.COMMON_PATH_LINK_PREFIX_V2; this.usesBase62EtagsInExport = GlobalConfig.usesBase62EtagsInExport; this.scriptVersion = GlobalConfig.scriptVersion; } /** * 递归获取指定文件夹ID下的所有文件信息 * @param {*} parentFileId * @param folderName * @param {*} total 仅用来计算进度 */ async _getAllFileInfoByFolderId(parentFileId, folderName = '', total) { //console.log("[123FASTLINK] [ShareLinkManager]", await this.apiClient.getFileList(parentFileId)); this.progressDesc = `正在扫描文件夹:${folderName}`; let progress = this.progress; const progressUpdater = setInterval(() => { //this.showProgressModal("生成秒传链接", , this.progressDesc); this.progress = progress + this.apiClient.progress / total; this.progressDesc = this.apiClient.progressDesc; // 不主动停止 if (this.progress > 100) { clearInterval(progressUpdater); //setTimeout(() => this.hideProgressModal(), 500); } }, 500); const allFileInfoList = (await this.apiClient.getFileList(parentFileId)).data.InfoList.map(file => ({ fileName: file.FileName, etag: file.Etag, size: file.Size, type: file.Type, fileId: file.FileId })); clearInterval(progressUpdater); // 分开文件和文件夹 // 文件添加所在文件夹名称 const fileInfo = allFileInfoList.filter(file => file.type !== 1); fileInfo.forEach(file => { file.path = folderName + file.fileName; }); this.fileInfoList.push(...fileInfo); console.log("[123FASTLINK] [ShareLinkManager]", "获取文件列表,ID:", parentFileId); const directoryFileInfo = allFileInfoList.filter(file => file.type === 1); for (const folder of directoryFileInfo) { // 延时 await new Promise(resolve => setTimeout(resolve, this.getFolderInfoDelay)); // 任务取消,停止深入文件夹 if (this.taskCancel) { this.progressDesc = "任务已取消"; return; } await this._getAllFileInfoByFolderId(folder.fileId, folderName + folder.fileName + "/", total * directoryFileInfo.length); } this.progress = progress + 100 / total; } /** * 分批获取文件信息 * @param {*} idList - 文件ID列表 * @returns - 来自服务器的文件全面数据 */ async _getFileInfoBatch(idList) { const total = idList.length; let completed = 0; let allFileInfo = []; for (let i = 0; i < total; i += this.getFileInfoBatchSize) { const batch = idList.slice(i, i + this.getFileInfoBatchSize); try { const response = await this.apiClient.getFileInfo(batch); allFileInfo = allFileInfo.concat(response.data.InfoList || []); } catch (e) { console.error('[123FASTLINK] [ShareLinkManager]', '获取文件信息失败:', e); } completed += batch.length; // 不能走到100,否则会自动消失,下面获取文件夹还用使用 this.progress = Math.round((completed / total) * 100 - 1); this.progressDesc = `正在获取文件信息... (${completed} / ${total})`; await new Promise(resolve => setTimeout(resolve, this.getFileInfoDelay)); } return allFileInfo.map(file => ({ fileName: file.FileName, etag: file.Etag, size: file.Size, type: file.Type, fileId: file.FileId })); } /** * 获取this.fileInfoList的公共路径 * @returns this.commonPath / commonPath */ async _getCommonPath() { if (!this.fileInfoList || this.fileInfoList.length === 0) return ''; // 提取所有路径并转换为目录组件数组 const pathArrays = this.fileInfoList.map(file => { const path = file.path || ''; // 移除路径末尾的文件名(如果有) const lastSlashIndex = path.lastIndexOf('/'); return lastSlashIndex === -1 ? [] : path.substring(0, lastSlashIndex).split('/'); }); // 找出最长的公共前缀 let commonPrefix = []; const firstPath = pathArrays[0]; for (let i = 0; i < firstPath.length; i++) { const currentComponent = firstPath[i]; const allMatch = pathArrays.every(pathArray => pathArray.length > i && pathArray[i] === currentComponent); if (allMatch) { commonPrefix.push(currentComponent); } else { break; } } // 将公共前缀组件组合为路径字符串 const commonPath = commonPrefix.length > 0 ? commonPrefix.join('/') + '/' : ''; this.commonPath = commonPath; return commonPath; } /** * 获取所有选择的文件,进入文件夹 * @param {*} fileSelectionDetails - 来自selector.getSelection() * @returns - 文件信息在this.fileInfoList里 * @returns - this.commonPath-公共路径 * @returns - boolean - 是否成功获取到文件 */ async _getSelectedFilesInfo(fileSelectionDetails) { this.fileInfoList = []; if (!fileSelectionDetails.isSelectAll && fileSelectionDetails.selectedRowKeys.length === 0) { return false; } let fileSelectFolderInfoList = []; if (fileSelectionDetails.isSelectAll) { this.progress = 10; this.progressDesc = "正在递归获取选择的文件..." let allFileInfo = (await this.apiClient.getFileList(await this.apiClient.getParentFileId())).data.InfoList.map(file => ({ fileName: file.FileName, etag: file.Etag, size: file.Size, type: file.Type, fileId: file.FileId })); // 分开处理文件和文件夹 let fileInfo = allFileInfo.filter(file => file.type !== 1); // 剔除反选的文件,并添加文件夹名称 fileInfo.filter(file => !fileSelectionDetails.unselectedRowKeys.includes(file.fileId.toString())).forEach(file => { file.path = file.fileName; }); // 方便后面继续添加 this.fileInfoList.push(...fileInfo); fileSelectFolderInfoList = allFileInfo.filter(file => file.type === 1).filter(file => !fileSelectionDetails.unselectedRowKeys.includes(file.fileId.toString())); } else { // 未全选 let fileSelectIdList = fileSelectionDetails.selectedRowKeys; if (!fileSelectIdList.length) { this.progress = 100; this.progressDesc = "未选择文件"; return false; } // 获取文件信息 const allFileInfo = await this._getFileInfoBatch(fileSelectIdList); const fileInfo = allFileInfo.filter(info => info.type !== 1); fileInfo.forEach(file => { file.path = file.fileName; }); this.fileInfoList.push(...fileInfo); fileSelectFolderInfoList = allFileInfo.filter(info => info.type === 1); } // 处理文件夹,递归获取全部文件 // this.progressDesc = "正在递归获取选择的文件,如果文件夹过多则可能耗时较长"; for (let i = 0; i < fileSelectFolderInfoList.length; i++) { const folderInfo = fileSelectFolderInfoList[i]; this.progress = Math.round((i / fileSelectFolderInfoList.length) * 100); await new Promise(resolve => setTimeout(resolve, this.getFolderInfoDelay)); // 任务取消 if (this.taskCancel) { this.progressDesc = "任务已取消"; return true; // 已经获取的文件保留 } await this._getAllFileInfoByFolderId(folderInfo.fileId, folderInfo.fileName + "/", fileSelectFolderInfoList.length); } // 处理文件夹路径 // 检查commonPath const commonPath = await this._getCommonPath(); // 去除文件夹路径中的公共路径 if (commonPath) { this.fileInfoList.forEach(info => { // 切片 info.path = info.path.slice(commonPath.length); }); } return true; } /** * 从选择文件生成分享链接 * @param {*} fileSelectionDetails - 来自selector.getSelection() * @returns {Promise} - 分享链接,如果未选择文件则返回空字符串 */ async generateShareLink(fileSelectionDetails) { this.progress = 0; this.progressDesc = "准备获取文件信息..."; // 获取选中的文件(文件夹)的详细信息 // this.fileInfoList, this.commonPath const result = await this._getSelectedFilesInfo(fileSelectionDetails); if (!result) return ''; //// if (hasFolder) alert("文件夹暂时无法秒传,将被忽略"); this.progressDesc = "秒传链接生成完成"; return this.buildShareLink(this.fileInfoList, this.commonPath); } /** * 拼接链接 * @param {*} fileInfoList - {etag: string, size: number, path: string, fileName: string} */ buildShareLink(fileInfoList, commonPath) { const shareLinkFileInfo = fileInfoList.map(info => { //if (info.type === 0) { return [this.usesBase62EtagsInExport ? this._hexToBase62(info.etag) : info.etag, info.size, info.path.replace(/[%#$]/g, '')].join('#'); //} }).filter(Boolean).join('$'); const shareLink = `${this.COMMON_PATH_LINK_PREFIX_V2}${commonPath}%${shareLinkFileInfo}`; return shareLink; } /** * 解析秒传链接 * @param {*} shareLink 秒传链接 * @param {*} InputUsesBase62 输入是否使用Base62 * @param {*} outputUsesBase62 输出是否使用Base62 * @returns {Array} - {etag: string, size: number, path: string, fileName: string} */ _parseShareLink(shareLink, InputUsesBase62 = true, outputUsesBase62 = false) { // Why use Base62 ??? // 本脚本采用hex传递 // 兼容旧版本,检查是否有链接头 let commonPath = ''; let shareFileInfo = ''; if (shareLink.slice(0, 4) === "123F") { const commonPathLinkPrefix = shareLink.split('$')[0]; shareLink = shareLink.replace(`${commonPathLinkPrefix}$`, ''); if (commonPathLinkPrefix + "$" === this.COMMON_PATH_LINK_PREFIX_V2) { commonPath = shareLink.split('%')[0]; shareFileInfo = shareLink.replace(`${commonPath}%`, ''); } else { console.error('[123FASTLINK] [ShareLinkManager]', '不支持的公共路径格式', commonPathLinkPrefix); return "[123FASTLINK] [ShareLinkManager] 不支持的公共路径格式:" + commonPathLinkPrefix; } } else { shareFileInfo = shareLink; InputUsesBase62 = false; } const shareLinkList = Array.from(shareFileInfo.replace(/\r?\n/g, '$').split('$')); this.commonPath = commonPath; return shareLinkList.map(singleShareLink => { const singleFileInfoList = singleShareLink.split('#'); if (singleFileInfoList.length < 3) return null; return { etag: InputUsesBase62 ? (outputUsesBase62 ? singleFileInfoList[0] : this._base62ToHex(singleFileInfoList[0])) : (outputUsesBase62 ? this._hexToBase62(singleFileInfoList[0]) : singleFileInfoList[0]), size: singleFileInfoList[1], path: singleFileInfoList[2], fileName: singleFileInfoList[2].split('/').pop() }; }).filter(Boolean); } /** * 先创建文件夹,给shareFileList添加上parentFolderId,便于保存文件 * @param {*} fileList - {etag: string, size: number, path: string, fileName: string} * @returns shareFileList - {etag: string, size: number, path: string, fileName: string, parentFolderId: number} */ async _makeDirForFiles(shareFileList) { const total = shareFileList.length; // 文件夹创建,并为shareFileList添加parentFolderId------------------------------------ // 记录文件夹(path) this.progressDesc = `正在创建文件夹...`; let folder = {}; // 如果存在commonPath,先创建文件夹 const rootFolderId = await this.apiClient.getParentFileId(); if (this.commonPath) { const commonPathParts = this.commonPath.split('/').filter(part => part !== ''); let currentParentId = rootFolderId; for (let i = 0; i < commonPathParts.length; i++) { const currentPath = commonPathParts.slice(0, i + 1).join('/'); const folderName = commonPathParts[i]; if (!folder[currentPath]) { const newFolder = await this.apiClient.mkdir(currentParentId, folderName); await new Promise(resolve => setTimeout(resolve, this.mkdirDelay)); folder[currentPath] = newFolder.folderFileId; } currentParentId = folder[currentPath]; } } else { folder[''] = rootFolderId; } for (let i = 0; i < shareFileList.length; i++) { const item = shareFileList[i]; const itemPath = item.path.split('/').slice(0, -1); // 记得去掉commonPath末尾的斜杠 let nowParentFolderId = folder[this.commonPath.slice(0, -1)] || rootFolderId; for (let i = 0; i < itemPath.length; i++) { const path = itemPath.slice(0, i + 1).join('/'); if (!folder[path]) { const newFolderID = await this.apiClient.mkdir(nowParentFolderId, itemPath[i]); await new Promise(resolve => setTimeout(resolve, this.mkdirDelay)); folder[path] = newFolderID.folderFileId; nowParentFolderId = newFolderID.folderFileId; } else { nowParentFolderId = folder[path]; } // 任务取消 if (this.taskCancel) { this.progressDesc = "任务已取消"; return shareFileList; } } shareFileList[i].parentFolderId = nowParentFolderId; this.progress = Math.round((i / total) * 100); this.progressDesc = `正在创建文件夹... (${i + 1} / ${total})`; } return shareFileList; } /** * 保存文件列表 * @param {Array} shareFileList - 带parentFolderId的 - _makeDirForFiles - {etag: string, size: number, path: string, fileName: string, parentFolderId: number} * @returns {Object} - {success: [], failed: [fileInfo]} */ async _saveFileList(shareFileList) { let completed = 0; let success = 0; let failed = 0; let successList = []; let failedList = []; const total = shareFileList.length; // 获取文件 ----------------------------- for (let i = 0; i < shareFileList.length; i++) { // 任务取消 if (this.taskCancel) { this.progressDesc = "任务已取消"; break; } const fileInfo = shareFileList[i]; if (i > 0) { await new Promise(resolve => setTimeout(resolve, this.saveLinkDelay)); } const reuse = await this.apiClient.getFile({ etag: fileInfo.etag, size: fileInfo.size, fileName: fileInfo.fileName }, fileInfo.parentFolderId); if (reuse[0]) { success++; successList.push(fileInfo); } else { failed++; console.error('[123FASTLINK] [ShareLinkManager]', '保存文件失败:', fileInfo.fileName); fileInfo.error = reuse[1]; failedList.push(fileInfo); } completed++; console.log('[123FASTLINK] [ShareLinkManager]', '已保存:', fileInfo.fileName); this.progress = Math.round((completed / total) * 100); this.progressDesc = `正在保存第 ${completed} / ${total} 个文件...`; } // this.progress = 100; // this.progressDesc = "保存完成"; return { success: successList, failed: failedList, commonPath: this.commonPath }; } /** * 保存秒传链接 */ async saveTextShareLink(shareLink) { const shareFileList = this._parseShareLink(shareLink); return this._saveFileList(await this._makeDirForFiles(shareFileList)); } /** * 保存JSON格式的秒传链接 * @param {string} jsonContent * @returns {Promise} - 保存结果 */ async saveJsonShareLink(jsonContent) { const shareFileList = this._parseJsonShareLink(jsonContent); return this._saveFileList(await this._makeDirForFiles(shareFileList)); } async saveShareLink(content) { let saveResult = { success: [], failed: [] }; try { // 尝试作为JSON解析 const jsonData = this.safeParse(content); if (jsonData) { saveResult = await this.saveJsonShareLink(jsonData); } else { // 作为普通秒传链接处理 saveResult = await this.saveTextShareLink(content); console.log('保存结果:', saveResult); } } catch (error) { console.error('保存失败:', error); saveResult = { success: [], failed: [] }; } return saveResult; } /** * 重试保存失败的文件 * @param {*} FileList - 包含parentFolderId - {etag: string, size: number, path: string, fileName: string, parentFolderId: number} * 失败的文件列表 - this.saveShareLink().failed * @returns */ async retrySaveFailed(FileList) { return this._saveFileList(FileList); } // -------------------JSON相关----------------------- safeParse(str) { try { return JSON.parse(str); } catch { return null; } } _base62chars() { return '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; } _hexToBase62(hex) { if (!hex) return ''; let num = BigInt('0x' + hex); if (num === 0n) return '0'; let chars = []; const base62 = this._base62chars(); while (num > 0n) { chars.push(base62[Number(num % 62n)]); num = num / 62n; } return chars.reverse().join(''); } _base62ToHex(base62) { if (!base62) return ''; const chars = this._base62chars(); let num = 0n; for (let i = 0; i < base62.length; i++) { num = num * 62n + BigInt(chars.indexOf(base62[i])); } let hex = num.toString(16); if (hex.length % 2) hex = '0' + hex; while (hex.length < 32) hex = '0' + hex; return hex; } /** * 解析JSON格式的秒传链接 * @param {object} jsonData * @returns {Array} - {etag: string, size: number, path: string, fileName: string} */ _parseJsonShareLink(jsonData) { this.commonPath = jsonData['commonPath'] || ''; const shareFileList = jsonData['files']; if (jsonData['usesBase62EtagsInExport']) { shareFileList.forEach(file => { file.etag = this._base62ToHex(file.etag); }); } shareFileList.forEach(file => { file.fileName = file.path.split('/').pop(); }); return shareFileList; } // 格式化文件大小 _formatSize(size) { if (size < 1024) return size + ' B'; 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'; } validateJson(json) { return (json && Array.isArray(json.files) && json.files.every(f => f.etag && f.size && f.path)); } /** * 将秒传链接转换为JSON格式 * @param {*} shareLink * @returns */ shareLinkToJson(shareLink) { const fileInfo = this._parseShareLink(shareLink); if (fileInfo.length === 0) { console.error('[123FASTLINK] [ShareLinkManager]', '解析秒传链接失败:', shareLink); return { error: '解析秒传链接失败' }; } if (this.usesBase62EtagsInExport) { fileInfo.forEach(f => { f.etag = this._hexToBase62(f.etag); }); } const totalSize = fileInfo.reduce((sum, f) => sum + Number(f.size), 0); return { scriptVersion: this.scriptVersion, exportVersion: "1.0", usesBase62EtagsInExport: this.usesBase62EtagsInExport, commonPath: this.commonPath, totalFilesCount: fileInfo.length, totalSize, formattedTotalSize: this._formatSize(totalSize), files: fileInfo.map(f => ({ // 去掉fileName ...f, fileName: undefined })) }; } } // 4. UI管理类 class UiManager { constructor(shareLinkManager, selector) { this.shareLinkManager = shareLinkManager; this.selector = selector; this.isProgressMinimized = false; this.minimizeWidgetId = 'progress-minimize-widget'; // this.currentShareLink = ''; // 存储当前秒传链接 // taskList = [{id: string, type: 'generate'|'save', params: {}}] this.taskList = []; // 任务列表 this.isTaskRunning = false; // 任务是否在运行 this.taskIdCounter = 0; // 任务ID计数器 this.currentTask = null; // 当前正在执行的任务 // this.taskCancel = false; // 取消当前任务的请求标志 } /** * 初始化UI管理器,插入样式表,设置按钮事件 */ init() { const triggerUrlChange = () => { setTimeout(() => this.addButton(), 10); }; window.addEventListener('load', () => { this.insertStyle(); this.addButton(); }); const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function () { originalPushState.apply(this, arguments); triggerUrlChange(); // 直接调用已绑定的函数 }; history.replaceState = function () { originalReplaceState.apply(this, arguments); triggerUrlChange(); }; window.addEventListener('popstate', triggerUrlChange); } /** * 插入样式表 */ insertStyle() { if (!document.getElementById("modal-style")) { let style = document.createElement("style"); style.id = "modal-style"; style.innerHTML = ` .modal-overlay { display: flex; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(4px); justify-content: center; align-items: center; z-index: 10000; animation: fadeIn 0.3s ease-out; } .modal { background: #fff; padding: 32px; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15), 0 8px 16px rgba(0, 0, 0, 0.1); text-align: center; width: 480px; max-width: 90vw; max-height: 90vh; overflow: hidden; position: relative; border: 1px solid rgba(255, 255, 255, 0.2); animation: modalSlideIn 0.3s ease-out; } .close-btn { position: absolute; top: 16px; right: 16px; background: transparent; border: none; font-size: 24px; color: #999; cursor: pointer; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; } .close-btn:hover { background: rgba(244, 67, 54, 0.1); color: #f44336; transform: scale(1.1); } .modal textarea { width: 100%; padding: 16px; margin: 0 0 24px 0; border: 2px solid #e1e5e9; border-radius: 12px; resize: vertical; min-height: 120px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 14px; line-height: 1.5; background: #fafbfc; transition: all 0.3s ease; box-sizing: border-box; outline: none; } .modal textarea:focus { border-color: #4CAF50; background: #ffffff; box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1); transform: translateY(-2px); } .modal textarea.drag-over { border-color: #4CAF50; background: #f0f8f0; } .copy-btn { background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); color: white; border: none; padding: 14px 32px; cursor: pointer; border-radius: 8px; font-size: 16px; font-weight: 500; min-width: 120px; position: relative; overflow: hidden; transition: all 0.3s ease; box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3); } .copy-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(76, 175, 80, 0.4); } .copy-btn:active { transform: translateY(0); box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3); } .button-group { display: flex; gap: 12px; align-items: center; justify-content: center; position: relative; } .copy-dropdown { position: relative; display: inline-block; } .copy-dropdown-menu {position: absolute; top: 100%; left: 0; background: #fff; border: 1px solid #e1e5e9; border-radius: 10px; box-shadow: 0 4px 16px rgba(0,0,0,0.12); min-width: 120px; z-index: 10001; margin-top: 6px; padding: 0; display: none;} .copy-dropdown:hover .copy-dropdown-menu, .copy-dropdown-menu:hover { display: block !important; } .copy-dropdown-menu { bottom: 100% !important; top: auto !important; margin-bottom: 6px !important; margin-top: 0 !important; } .copy-dropdown-menu::before { content: ''; position: absolute; bottom: -6px; left: 0; width: 100%; height: 6px; background: transparent; } .copy-dropdown-item { padding: 10px 18px; cursor: pointer; font-size: 14px; border-bottom: 1px solid #f0f0f0; background: #fff; transition: background 0.2s;} .copy-dropdown-item:last-child { border-bottom: none; } .copy-dropdown-item:hover { background: #e8f5e9; color: #388e3c;} .copy-dropdown-item:first-child { border-radius: 10px 10px 0 0; } .copy-dropdown-item:last-child { border-radius: 0 0 10px 10px; } .export-btn { background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%); color: white; border: none; padding: 14px 24px; cursor: pointer; border-radius: 8px; font-size: 16px; font-weight: 500; min-width: 100px; transition: all 0.3s ease; box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3); } .export-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(33, 150, 243, 0.4); } .file-input-btn { background: linear-gradient(135deg, #FF9800 0%, #F57C00 100%); color: white; border: none; padding: 14px 24px; cursor: pointer; border-radius: 8px; font-size: 16px; font-weight: 500; min-width: 100px; transition: all 0.3s ease; box-shadow: 0 4px 12px rgba(255, 152, 0, 0.3); } .file-input-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(255, 152, 0, 0.4); } .file-input { display: none; } .toast { position: fixed; top: 20px; right: 20px; background: #fff; color: #333; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 10002; font-size: 14px; max-width: 300px; animation: toastSlideIn 0.3s ease-out; } .toast.success { border-left: 4px solid #4CAF50; } .toast.error { border-left: 4px solid #f44336; } .toast.warning { border-left: 4px solid #ff9800; } .toast.info { border-left: 4px solid #2196F3; } .progress-minimize-btn{position:absolute;left:-10px;top:-10px;width:30px;height:30px;border-radius:50%;background:#ffc504;color:#000000ff;border:none;display:flex;align-items:center;justify-content:center;font-weight:700;cursor:pointer;box-shadow:0 2px 6px rgba(0,0,0,0.15);z-index:10003}.progress-minimize-btn:hover{transform:scale(1.05)} .minimized-widget{position:fixed;right:20px;bottom:20px;width:220px;background:#fff;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,0.18);padding:10px 12px;z-index:10005;display:flex;align-items:center;gap:10px;cursor:pointer} .minimized-widget .mini-bar{flex:1} .minimized-widget .mini-title{font-size:12px;color:#333;margin-bottom:6px} .minimized-widget .mini-progress{height:8px;background:#eee;border-radius:6px;overflow:hidden} .minimized-widget .mini-progress>i{display:block;height:100%;background:#4CAF50;width:0%;transition:width 0.2s} .minimized-widget .mini-percent{font-size:12px;color:#666;width:36px;text-align:right} .toast-shake {animation: toastShake 0.4s cubic-bezier(.36,.07,.19,.97) both, toastSlideIn 0.3s ease-out;} #progress-title { margin-bottom:16px; font-size:18px; word-wrap: break-word; word-break: break-all; white-space: pre-wrap; } #progress-desc { margin-top:8px; font-size:13px; color:#888; word-wrap: break-word; word-break: break-all; white-space: pre-wrap; line-height: 1.4; } .task-list-container { margin-top: 16px; } .task-list-toggle { background: transparent; border: 1px solid #ddd; color: #666; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 12px; width: 100%; text-align: left; display: flex; align-items: center; justify-content: space-between; transition: all 0.2s; } .task-list-toggle:hover { background: #f5f5f5; border-color: #bbb; } .task-list-toggle.active { background: #f0f8ff; border-color: #4CAF50; } .task-list { max-height: 120px; overflow-y: auto; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 6px 6px; background: #fafafa; display: none; } .task-list.show { display: block; } .task-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid #eee; font-size: 13px; } .task-item:last-child { border-bottom: none; } .task-item .task-name { color: #333; flex: 1; } .task-item .task-remove { background: #ff4757; color: white; border: none; border-radius: 4px; padding: 2px 6px; font-size: 11px; cursor: pointer; transition: background 0.2s; } .task-item .task-remove:hover { background: #ff3742; } @keyframes toastShake { 10%, 90% { transform: translateX(-2px); } 20%, 80% { transform: translateX(4px); } 30%, 50%, 70% { transform: translateX(-8px); } 40%, 60% { transform: translateX(8px); } 100% { transform: translateX(0); } `; document.head.appendChild(style); } } /** * 显示提示消息(右上角) * @param {*} message * @param {*} type * @param {*} duration */ showToast(message, type = 'info', duration = 3000) { // this.insertStyle(); const toast = document.createElement('div'); toast.className = `toast ${type} toast-shake`; // 添加 toast-shake 类 toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'toastSlideOut 0.3s ease-out forwards'; setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 300); }, duration); } /** * 显示复制弹窗 * @param {*} defaultText */ showCopyModal(defaultText = "") { // this.insertStyle(); // this.currentShareLink = defaultText; // let existingModal = document.getElementById('modal'); // if (existingModal) existingModal.remove(); // 获取文件名列表 let fileListHtml = ''; if (Array.isArray(this.shareLinkManager.fileInfoList) && this.shareLinkManager.fileInfoList.length > 0) { fileListHtml = `
文件列表(共${this.shareLinkManager.fileInfoList.length}个):
${this.shareLinkManager.fileInfoList.map(f => `
${f.path}
`).join('')}
`; } let modalOverlay = document.createElement('div'); modalOverlay.className = 'modal-overlay'; modalOverlay.id = 'modal'; modalOverlay.innerHTML = ` `; // 导出JSON按钮事件 modalOverlay.querySelector('#exportJsonButton').addEventListener('click', (e) => { e.stopPropagation(); this.exportJson(); }) // 下拉菜单悬停控制 const dropdown = modalOverlay.querySelector('.copy-dropdown'); const dropdownMenu = modalOverlay.querySelector('.copy-dropdown-menu'); let hideTimer; // 鼠标进入下拉容器时显示菜单 dropdown.addEventListener('mouseenter', () => { clearTimeout(hideTimer); dropdownMenu.style.display = 'block'; }); // 鼠标离开下拉容器时延迟隐藏菜单 dropdown.addEventListener('mouseleave', () => { hideTimer = setTimeout(() => { dropdownMenu.style.display = 'none'; }, 300); // 300ms延迟,给用户足够时间移动鼠标 }); // 鼠标进入菜单时取消隐藏 dropdownMenu.addEventListener('mouseenter', () => { clearTimeout(hideTimer); }); // 鼠标离开菜单时隐藏 dropdownMenu.addEventListener('mouseleave', () => { hideTimer = setTimeout(() => { dropdownMenu.style.display = 'none'; }, 100); // 稍微延迟避免误触 }); // 复制按钮点击直接复制纯文本 modalOverlay.querySelector('#massageboxButton').addEventListener('click', (e) => { e.stopPropagation(); this.copyContent('text'); }); // 点击菜单项复制对应类型 modalOverlay.querySelectorAll('.copy-dropdown-item').forEach(item => { item.addEventListener('click', (e) => { e.stopPropagation(); const type = item.dataset.type; this.copyContent(type); clearTimeout(hideTimer); dropdownMenu.style.display = 'none'; // 点击后隐藏菜单 }); }); modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) modalOverlay.remove(); }); document.body.appendChild(modalOverlay); setTimeout(() => { const textarea = modalOverlay.querySelector('#copyText'); if (textarea && !defaultText) textarea.focus(); }, 100); } /** * 复制内容到剪贴板 * @param {*} type - 复制类型(文本或JSON) * @returns */ copyContent(type) { const inputField = document.querySelector('#copyText'); if (!inputField) return; let contentToCopy = inputField.value; if (type === 'json') { try { const jsonData = this.shareLinkManager.shareLinkToJson(contentToCopy); contentToCopy = JSON.stringify(jsonData, null, 2); } catch (error) { this.showToast('转换JSON失败: ' + error.message, 'error'); return; } } navigator.clipboard.writeText(contentToCopy).then(() => { this.showToast(`已成功复制${type === 'json' ? 'JSON' : '纯文本'}到剪贴板 📋`, 'success'); }).catch(err => { this.showToast(`复制失败: ${err.message || '请手动复制内容'}`, 'error'); }); } /** * 导出JSON * @returns */ exportJson() { const inputField = document.querySelector('#copyText'); if (!inputField) return; const shareLink = inputField.value; if (!shareLink.trim()) { this.showToast('没有内容可导出', 'warning'); return; } try { const jsonData = this.shareLinkManager.shareLinkToJson(shareLink); const jsonContent = JSON.stringify(jsonData, null, 2); const filename = this.getExportFilename(shareLink); this.downloadJsonFile(jsonContent, filename); this.showToast('JSON文件导出成功 📁', 'success'); } catch (error) { this.showToast('导出失败: ' + error.message, 'error'); } } // 下载JSON文件 downloadJsonFile(content, filename) { const blob = new Blob([content], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // 获取文件名用于JSON导出 getExportFilename(shareLink) { if (this.shareLinkManager.commonPath) { const commonPath = this.shareLinkManager.commonPath.replace(/\/$/, ''); // 去除末尾斜杠 return `${commonPath}.json`; } const lines = shareLink.trim().split('\n').filter(Boolean); if (lines.length === 0) return 'export.json'; const firstLine = lines[0]; const parts = firstLine.split('#'); if (parts.length >= 3) { const fileName = parts[2]; const baseName = fileName.split('/').pop().split('.')[0] || 'export'; return `${baseName}.json`; } return 'export.json'; } /** * 显示或更新进度模态框 * @param title - 标题 * @param percent - 进度百分比(0-100) * @param desc - 进度描述 * @param taskCount - 任务队列长度 */ updateProgressModal(title = "正在处理...", percent = 0, desc = "", taskCount = 1) { percent = Math.ceil(percent); // 如果处于最小化状态,则展示/更新右下角浮动卡片并返回 if (this.isProgressMinimized) { this.updateMinimizedWidget(title, percent, desc, taskCount); return; } let modal = document.getElementById('progress-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'progress-modal'; modal.style = 'position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:10001;background:rgba(0,0,0,0.3);display:flex;align-items:center;justify-content:center;'; modal.innerHTML = `
${title + (taskCount > 1 ? ` - 队列 ${taskCount}` : '')}
${percent}%
${desc}
`; document.body.appendChild(modal); // 绑定最小化按钮事件(点击后移除模态并创建右下角浮动卡片) const btn = modal.querySelector('.progress-minimize-btn'); if (!btn.dataset.bound) { btn.dataset.bound = 'true'; btn.addEventListener('click', (e) => { e.stopPropagation(); this.isProgressMinimized = true; // 读取当前进度显示到浮动卡片 const curTitle = modal.querySelector('#progress-title')?.innerText || title + (taskCount > 1 ? ` - 队列 ${taskCount}` : ''); const curPercent = parseInt(modal.querySelector('#progress-percent')?.innerText || percent) || 0; const curDesc = modal.querySelector('#progress-desc')?.innerText || desc; this.removeProgressModalAndKeepState(); this.updateMinimizedWidget(curTitle, curPercent, curDesc, taskCount); }); } } else { const titleElement = modal.querySelector('#progress-title'); const descElement = modal.querySelector('#progress-desc'); titleElement.innerText = title + (taskCount > 1 ? ` - 队列 ${taskCount}` : ''); titleElement.style.cssText = 'margin-bottom:16px;font-size:18px;word-wrap:break-word;word-break:break-all;white-space:pre-wrap;line-height:1.4;'; modal.querySelector('#progress-bar').style.width = percent + '%'; modal.querySelector('#progress-percent').innerText = percent + '%'; descElement.innerText = desc; descElement.style.cssText = 'margin-top:8px;font-size:13px;color:#888;word-wrap:break-word;word-break:break-all;white-space:pre-wrap;line-height:1.4;'; } // 更新任务列表 this.manageTaskList(modal); } /** * 任务列表管理 - 统一处理任务列表的创建、更新和事件绑定 */ manageTaskList(modal) { const existingContainer = modal.querySelector('.task-list-container'); const currentTaskCount = this.taskList.length; // 没有任务时删除任务列表 if (currentTaskCount === 0) { existingContainer?.remove(); return; } // 生成任务列表HTML const generateHtml = () => `
${this.taskList.map(task => { const isCurrentTask = this.currentTask && this.currentTask.id === task.id; const taskStatus = isCurrentTask ? ' (执行中)' : ''; const taskClass = isCurrentTask ? ' style="background: #e8f5e8; border-left: 3px solid #4CAF50;"' : ''; return `
${task.type === 'generate' ? '链接生成' : '链接转存'}${taskStatus}
`; // }).join('')}
`; // 绑定事件 const bindEvents = (container) => { const toggle = container.querySelector('#task-list-toggle'); const taskList = container.querySelector('#task-list'); // 切换展开/收起 toggle?.addEventListener('click', () => { const isShown = taskList.classList.toggle('show'); toggle.classList.toggle('active', isShown); toggle.querySelector('span:last-child').textContent = isShown ? '▲' : '▼'; }); // 删除任务 container.querySelectorAll('.task-remove').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const taskId = btn.dataset.taskId; // 防止删除正在执行的任务 if (this.currentTask && this.currentTask.id.toString() === taskId) { this.showToast('正在中断任务', 'warning'); this.cancelCurrentTask(); return; } this.taskList = this.taskList.filter(task => task.id.toString() !== taskId); this.manageTaskList(modal); this.showToast('任务已取消', 'info'); }); }); }; if (!existingContainer) { // 创建 const progressDesc = modal.querySelector('#progress-desc'); progressDesc.insertAdjacentHTML('afterend', generateHtml()); bindEvents(modal.querySelector('.task-list-container')); } else { // 检查是否需要重建(任务数量变化或当前任务状态变化) const existingTaskItems = existingContainer.querySelectorAll('.task-item'); const hasCurrentTaskChanged = existingContainer.querySelector('.task-item[style*="background: #e8f5e8"]') ? !this.currentTask : !!this.currentTask; if (existingTaskItems.length !== currentTaskCount || hasCurrentTaskChanged) { const wasExpanded = existingContainer.querySelector('.task-list').classList.contains('show'); existingContainer.remove(); const progressDesc = modal.querySelector('#progress-desc'); progressDesc.insertAdjacentHTML('afterend', generateHtml()); const newContainer = modal.querySelector('.task-list-container'); bindEvents(newContainer); // 恢复展开状态 if (wasExpanded) { const taskList = newContainer.querySelector('.task-list'); const toggle = newContainer.querySelector('#task-list-toggle'); taskList.classList.add('show'); toggle.classList.add('active'); toggle.querySelector('span:last-child').textContent = '▲'; } } else { // 只更新计数 const toggleSpan = existingContainer.querySelector('#task-list-toggle span:first-child'); if (toggleSpan) toggleSpan.textContent = `任务队列 (${currentTaskCount})`; } } } // 隐藏进度条并删除浮动卡片 hideProgressModal() { const modal = document.getElementById('progress-modal'); if (modal) modal.remove(); this.removeMinimizedWidget(); this.isProgressMinimized = false; } // 移除模态但保留 isProgressMinimized 标志(供最小化按钮调用) removeProgressModalAndKeepState() { const modal = document.getElementById('progress-modal'); if (modal) modal.remove(); } // 创建或更新右下角最小化浮动进度条卡片 updateMinimizedWidget(title = '正在处理...', percent = 0, desc = '', taskCount = 1) { let widget = document.getElementById(this.minimizeWidgetId); // 红点提示,仅在剩余任务数>=2时显示 let redDotHtml = ''; if (this.taskList.length >= 1) { redDotHtml = ``; } const html = ` ${redDotHtml}
${title + (taskCount > 1 ? ` - 队列 ${taskCount}` : '')}
${percent}%
`; if (!widget) { widget = document.createElement('div'); widget.id = this.minimizeWidgetId; widget.className = 'minimized-widget'; widget.innerHTML = html; // 修复点击不灵敏:用mousedown替换click,并阻止冒泡 widget.addEventListener('mousedown', (e) => { e.stopPropagation(); e.preventDefault(); this.isProgressMinimized = false; this.removeMinimizedWidget(); // 重新显示模态,使用当前进度值 this.updateProgressModal(title, percent, desc, taskCount); }); document.body.appendChild(widget); } else { widget.innerHTML = html; } } // 移除右下角浮动卡片 removeMinimizedWidget() { const w = document.getElementById(this.minimizeWidgetId); if (w) w.remove(); } /** * 任务函数 - 启动生成链接,UI层面的生成入口 * 包括UI进度条显示和轮询 * @param {*} fileSelectInfo - 选中文件信息,来自selector */ async launchProgressModal(fileSelectInfo) { // 轮询进度 const mgr = this.shareLinkManager; // this.showProgressModal("生成秒传链接", 0, "准备中..."); mgr.progress = 0; const poll = setInterval(() => { this.updateProgressModal("生成秒传链接", mgr.progress, mgr.progressDesc, this.taskList.length); if (mgr.progress > 100) { clearInterval(poll); setTimeout(() => this.hideProgressModal(), 500); } }, 500); const shareLink = await mgr.generateShareLink(fileSelectInfo); // 清除任务取消标志 this.shareLinkManager.taskCancel = false; if (!shareLink) { this.showToast("没有选择文件", 'warning'); clearInterval(poll); return; } clearInterval(poll); this.hideProgressModal(); this.showCopyModal(shareLink); } /** * 显示保存结果模态框 * @param result - {success: [], failed: []} * @returns {Promise} */ async showSaveResultsModal(result) { // this.insertStyle(); // let existingModal = document.getElementById('results-modal'); // if (existingModal) existingModal.remove(); const totalCount = result.success.length + result.failed.length; const successCount = result.success.length; const failedCount = result.failed.length; let failedListHtml = ''; if (failedCount > 0) { failedListHtml = `
失败文件列表:
${result.failed.map(fileInfo => `
${fileInfo.fileName} ${fileInfo.error ? `(${fileInfo.error})` : ''}
`).join('')}
`; } let modalOverlay = document.createElement('div'); modalOverlay.className = 'modal-overlay'; modalOverlay.id = 'results-modal'; modalOverlay.innerHTML = ` `; // 添加重试和导出按钮事件 if (failedCount > 0) { const dropdownMenu = modalOverlay.querySelector('.copy-dropdown-menu'); dropdownMenu.querySelectorAll('.copy-dropdown-item').forEach(item => { item.addEventListener('click', async () => { const action = item.dataset.action; if (action === 'retry') { document.getElementById('results-modal').remove(); this.addAndRunTask('retry', { fileList: result.failed }); } else if (action === 'export') { const shareLink = this.shareLinkManager.buildShareLink(result.failed, result.commonPath || ''); this.showCopyModal(shareLink); } }); }); } modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) modalOverlay.remove(); }); document.body.appendChild(modalOverlay); } /** * 任务函数 - 启动从输入的内容解析并保存秒传链接,UI层面的保存入口,retry为是可以重试失败的文件 * @param {*} content - 输入内容(秒传链接/JSON) */ async launchSaveLink(content, retry = false) { this.updateProgressModal("保存秒传链接", 0, "准备中..."); this.shareLinkManager.progress = 0; const poll = setInterval(() => { this.updateProgressModal("保存秒传链接", this.shareLinkManager.progress, this.shareLinkManager.progressDesc, this.taskList.length); // 正常情况下不主动清除 if (this.shareLinkManager.progress > 100) { clearInterval(poll); } }, 100); let saveResult; if (!retry) { saveResult = await this.shareLinkManager.saveShareLink(content); } else { saveResult = await this.shareLinkManager.retrySaveFailed(content); } // 清除任务取消标志 this.shareLinkManager.taskCancel = false; clearInterval(poll); this.hideProgressModal(); this.showSaveResultsModal(saveResult); this.renewWebPageList(); this.showToast(saveResult ? "保存成功" : "保存失败", saveResult ? 'success' : 'error'); } /** * 模拟点击刷新按钮,刷新页面文件列表 */ renewWebPageList() { // 刷新页面文件列表 const renewButton = document.querySelector('.layout-operate-icon.mfy-tooltip svg'); if (renewButton) { const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window }); renewButton.dispatchEvent(clickEvent); } } /** * 显示输入模态框 */ async showInputModal() { // this.insertStyle(); let existingModal = document.getElementById('save-modal'); if (existingModal) existingModal.remove(); let modalOverlay = document.createElement('div'); modalOverlay.className = 'modal-overlay'; modalOverlay.id = 'save-modal'; modalOverlay.innerHTML = ` `; const textarea = modalOverlay.querySelector('#saveText'); const fileInput = modalOverlay.querySelector('#jsonFileInput'); const selectFileBtn = modalOverlay.querySelector('#selectFileButton'); // 设置文件拖拽和选择 this.setupFileDropAndInput(textarea, fileInput); // 选择文件按钮 selectFileBtn.addEventListener('click', () => { fileInput.click(); }); modalOverlay.querySelector('#saveButton').addEventListener('click', async () => { const content = document.getElementById("saveText").value; if (!content.trim()) { this.showToast("请输入秒传链接或导入JSON文件", 'warning'); return; } modalOverlay.remove(); this.addAndRunTask('save', { content }); }); modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) modalOverlay.remove(); }); document.body.appendChild(modalOverlay); setTimeout(() => { const textarea = modalOverlay.querySelector('#saveText'); if (textarea) textarea.focus(); }, 100); } // 处理文件拖拽和读取 setupFileDropAndInput(textarea, fileInput) { // 拖拽事件 textarea.addEventListener('dragover', (e) => { e.preventDefault(); textarea.classList.add('drag-over'); }); textarea.addEventListener('dragleave', (e) => { e.preventDefault(); textarea.classList.remove('drag-over'); }); textarea.addEventListener('drop', (e) => { e.preventDefault(); textarea.classList.remove('drag-over'); const files = e.dataTransfer.files; if (files.length > 0) { this.readJsonFile(files[0], textarea); } }); // 文件选择事件 fileInput.addEventListener('change', (e) => { const files = e.target.files; if (files.length > 0) { this.readJsonFile(files[0], textarea); } }); } /** * 读取JSON文件并将内容填充到文本区域 * @param {*} file - 要读取的文件 * @param {*} textarea - 目标文本区域 * @returns */ readJsonFile(file, textarea) { if (!file.name.toLowerCase().endsWith('.json')) { this.showToast('请选择JSON文件', 'warning'); return; } const reader = new FileReader(); reader.onload = (e) => { try { const jsonContent = e.target.result; const jsonData = JSON.parse(jsonContent); if (this.shareLinkManager.validateJson(jsonData)) { // const shareLink = this.shareLinkManager.jsonToShareLink(jsonData); textarea.value = jsonContent; this.showToast('JSON文件导入成功 ✅', 'success'); } else { this.showToast('无效的JSON格式', 'error'); } } catch (error) { this.showToast('JSON文件解析失败: ' + error.message, 'error'); } }; reader.readAsText(file); } /** * 队列 - 运行下一个任务 * @returns {null|void} */ runNextTask() { if (this.isTaskRunning) return this.showToast("已添加到队列,稍后执行", 'info'); if (this.taskList.length === 0) return null; // 找到第一个未执行的任务 const task = this.taskList.find(t => !this.currentTask || t.id !== this.currentTask.id); if (!task) return null; // 标记当前任务 this.currentTask = task; // 执行任务 setTimeout(async () => { this.isTaskRunning = true; if (task.type === 'generate') { // 生成秒传链接 await this.launchProgressModal(task.params.fileSelectInfo); } else if (task.type === 'save') { // 保存秒传链接 await this.launchSaveLink(task.params.content); } else if (task.type === 'retry') { // 重试任务 await this.launchSaveLink(task.params.fileList, true); } this.isTaskRunning = false; // 任务完成,从列表中移除 this.taskList = this.taskList.filter(t => t.id !== task.id); this.currentTask = null; this.runNextTask(); }, 100); this.showToast(`任务${task.id}开始执行...`, 'info'); } /** * 解析、添加并触发任务 * @param taskType - 任务类型(generate/save/retry) * @param params - 任务参数 */ addAndRunTask(taskType, params = {}) { const taskId = ++this.taskIdCounter; if (taskType === 'generate') { // 获取选中文件 const fileSelectInfo = this.selector.getSelection(); if (!fileSelectInfo || fileSelectInfo.length === 0) { this.showToast("请先选择文件", 'warning'); return; } this.taskList.push({ id: taskId, type: 'generate', params: { fileSelectInfo } }); } else if (taskType === 'save') { this.taskList.push({ id: taskId, type: 'save', params: { content: params.content } }); } else if (taskType === 'retry') { this.taskList.push({ id: taskId, type: 'retry', params: { fileList: params.fileList } }); } this.runNextTask(); } /** 任务取消 * @returns {boolean} */ cancelCurrentTask() { this.shareLinkManager.taskCancel = true; return true; } addButton() { const buttonExist = document.querySelector('.mfy-button-container'); if (buttonExist) return; const isFilePage = window.location.pathname === "/" && (window.location.search === "" || window.location.search.includes("homeFilePath")); if (!isFilePage) return; const container = document.querySelector('.home-operator-button-group'); if (!container) return; const btnContainer = document.createElement('div'); btnContainer.className = 'mfy-button-container'; btnContainer.style.position = 'relative'; btnContainer.style.display = 'inline-block'; const btn = document.createElement('button'); btn.className = 'ant-btn css-dev-only-do-not-override-168k93g ant-btn-default ant-btn-color-default ant-btn-variant-outlined mfy-button create-button'; btn.style.background = "#4CAF50"; btn.style.color = "#fff"; btn.style.border = "none"; btn.innerHTML = `秒传`; const dropdown = document.createElement('div'); dropdown.className = 'mfy-dropdown'; dropdown.style.display = 'none'; dropdown.style.position = 'absolute'; dropdown.style.top = 'calc(100% + 5px)'; dropdown.style.left = '0'; dropdown.style.backgroundColor = '#fff'; dropdown.style.border = '1px solid #d9d9d9'; dropdown.style.borderRadius = '10px'; dropdown.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)'; dropdown.style.zIndex = '1000'; dropdown.style.minWidth = '120px'; dropdown.style.overflow = 'hidden'; dropdown.innerHTML = `
生成秒传链接
保存秒传链接
`; const style = document.createElement('style'); style.textContent = ` .mfy-button-container:hover .mfy-dropdown { display: block !important; } .mfy-dropdown-item { padding: 8px 12px; cursor: pointer; transition: background 0.3s; font-size: 14px; } .mfy-dropdown-item:hover { background-color: #f5f5f5; } .mfy-dropdown::before { content: ''; position: absolute; top: -5px; left: 0; width: 100%; height: 5px; background: transparent; } `; document.head.appendChild(style); btnContainer.appendChild(btn); btnContainer.appendChild(dropdown); container.insertBefore(btnContainer, container.firstChild); dropdown.querySelectorAll('.mfy-dropdown-item').forEach(item => { item.addEventListener('click', async () => { const action = item.dataset.action; if (action === 'generate') { await this.addAndRunTask('generate'); } else if (action === 'save') { await this.showInputModal(); } dropdown.style.display = 'none'; }); }); btnContainer.addEventListener('mouseenter', function () { dropdown.style.display = 'block'; }); btnContainer.addEventListener('mouseleave', function () { let timer; clearTimeout(timer); timer = setTimeout(() => { dropdown.style.display = 'none'; }, 300); }); } } const apiClient = new PanApiClient(); const selector = new TableRowSelector(); const shareLinkManager = new ShareLinkManager(apiClient); const uiManager = new UiManager(shareLinkManager, selector); selector.init(); uiManager.init(); })();