2232 lines
68 KiB
JavaScript
2232 lines
68 KiB
JavaScript
(function ($) {
|
|
"use strict";
|
|
|
|
// ============================================================================
|
|
// G3File 构造函数
|
|
// ============================================================================
|
|
function G3File(container, options) {
|
|
// 实例隔离
|
|
this.$container = $(container);
|
|
this.options = $.extend({}, this.defaults, options);
|
|
this.instanceId = G3FileUtils.generateUniqueId();
|
|
this.documentNamespace = ".g3file_" + this.instanceId;
|
|
this.files = [];
|
|
this.selectedFiles = new Set();
|
|
this.objectUrls = new Map();
|
|
this.language =
|
|
G3FileLanguages[this.options.language] || G3FileLanguages["zh-CN"];
|
|
|
|
// 操作菜单状态
|
|
this.actionMenuState = {
|
|
open: false,
|
|
fileId: null,
|
|
anchorEl: null,
|
|
items: [],
|
|
};
|
|
|
|
// 当前任务(单个任务实例)
|
|
this.tasks = new Map();
|
|
|
|
// UI锁定状态
|
|
this.uiLocked = false;
|
|
|
|
// 阿里云 OSS 凭证缓存
|
|
this.aliyunKeyCache = null;
|
|
|
|
// 预览相关状态(基于 fileId 定位,避免删除/新增导致错位)
|
|
this.currentPreviewFileId = null;
|
|
|
|
this.init();
|
|
}
|
|
|
|
// ============================================================================
|
|
// 原型方法
|
|
// ============================================================================
|
|
G3File.prototype = {
|
|
// ============================================================================
|
|
// 配置和常量
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 默认配置
|
|
*/
|
|
defaults: {
|
|
/**
|
|
* 初始化时的文件数据
|
|
* @typedef {Object} FileItem
|
|
* @property {string} _id - 文件唯一标识符
|
|
* @property {string} name - 文件名
|
|
* @property {number} size - 文件大小(字节)
|
|
* @property {string} uploadDate - 上传日期(ISO格式)
|
|
* @property {string} url - 文件访问URL
|
|
* @property {File|null} file - 本地File对象(仅待上传文件有)
|
|
* @property {string} status - 文件状态:pending/uploading/uploaded/failed
|
|
* @type {FileItem[]}
|
|
*/
|
|
data: [],
|
|
mode: "aliyun",
|
|
onDeleteBefore: null,
|
|
onDelete: null,
|
|
onDownload: null,
|
|
onSelect: null,
|
|
onUpload: null,
|
|
onUploadBefore: null,
|
|
onDblClick: null,
|
|
onUploadSuccess: null,
|
|
onUploadError: null,
|
|
onUploadComplete: null,
|
|
customActionMenu: null,
|
|
language: "zh-CN",
|
|
maxFileSize: 10 * 1024 * 1024,
|
|
// 上传配置
|
|
uploadConfig: {
|
|
aliyun: {
|
|
// 阿里云配置是可选的
|
|
// getAliyunKey 是必须的,用于动态获取阿里云 OSS 上传凭证
|
|
getAliyunKey: null, // 异步函数,返回 { accessid, callback, dir, expire, host, policy, signature }
|
|
},
|
|
customUpload: null, // 自定义上传方法
|
|
},
|
|
// 全局权限(支持对象或函数)
|
|
globalPermission: {
|
|
upload: true,
|
|
download: true,
|
|
delete: true,
|
|
view: true,
|
|
},
|
|
},
|
|
|
|
// ============================================================================
|
|
// 初始化和核心方法
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 初始化
|
|
* @returns {G3File} 实例
|
|
*/
|
|
init: function () {
|
|
var self = this;
|
|
|
|
self.render();
|
|
self.bindEvents();
|
|
self.updateUI();
|
|
|
|
if (self.options.data && self.options.data.length) {
|
|
self.setFiles(self.options.data);
|
|
}
|
|
|
|
self.preloadAliyunKey();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* 预加载阿里云 OSS 凭证(初始化时调用)
|
|
*/
|
|
preloadAliyunKey: async function () {
|
|
if (this.options.mode !== "aliyun") {
|
|
return;
|
|
}
|
|
const aliyunConfig = this.options.uploadConfig.aliyun;
|
|
if (!aliyunConfig || typeof aliyunConfig.getAliyunKey !== "function") {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const keyData = await aliyunConfig.getAliyunKey();
|
|
if (keyData && keyData.accessid && keyData.host) {
|
|
this.aliyunKeyCache = {
|
|
data: keyData,
|
|
expireTime: keyData.expire
|
|
? keyData.expire * 1000
|
|
: Date.now() + 3600000,
|
|
};
|
|
console.log("[G3File] 阿里云 OSS 凭证预加载成功");
|
|
}
|
|
} catch (error) {
|
|
console.warn("[G3File] 阿里云 OSS 凭证预加载失败:", error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 检查并获取有效的阿里云 OSS 凭证
|
|
* @returns {Object|null} 阿里云凭证数据
|
|
*/
|
|
getValidAliyunKey: async function () {
|
|
const now = Date.now();
|
|
|
|
if (this.aliyunKeyCache) {
|
|
if (now < this.aliyunKeyCache.expireTime - 60000) {
|
|
return this.aliyunKeyCache.data;
|
|
}
|
|
console.log("[G3File] 阿里云 OSS 凭证即将过期,自动刷新");
|
|
}
|
|
|
|
const aliyunConfig = this.options.uploadConfig.aliyun;
|
|
if (!aliyunConfig || typeof aliyunConfig.getAliyunKey !== "function") {
|
|
throw new Error("阿里云上传配置不完整,缺少 getAliyunKey 函数");
|
|
}
|
|
|
|
try {
|
|
const keyData = await aliyunConfig.getAliyunKey();
|
|
if (!keyData || !keyData.accessid || !keyData.host) {
|
|
throw new Error("阿里云凭证获取失败");
|
|
}
|
|
|
|
this.aliyunKeyCache = {
|
|
data: keyData,
|
|
expireTime: keyData.expire
|
|
? keyData.expire * 1000
|
|
: Date.now() + 3600000,
|
|
};
|
|
|
|
console.log("[G3File] 阿里云 OSS 凭证获取成功");
|
|
return keyData;
|
|
} catch (error) {
|
|
console.error("[G3File] 获取阿里云 OSS 凭证失败:", error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 生成文件 key(UUID + 原扩展名)
|
|
* @param {Object} keyData - 阿里云凭证数据
|
|
* @param {string} filename - 原始文件名
|
|
* @returns {string} 生成的 key
|
|
*/
|
|
generateFileKey: function (keyData, filename) {
|
|
const dir = keyData.dir || "";
|
|
const ext = filename.slice(filename.lastIndexOf(".")) || "";
|
|
const uuid = G3FileUtils.generateUniqueId().replace("file_", "");
|
|
return dir + uuid + ext;
|
|
},
|
|
|
|
// ============================================================================
|
|
// 工具方法
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 获取文件图标
|
|
* @param {string} filename - 文件名
|
|
* @returns {string} 图标HTML
|
|
*/
|
|
getFileIcon: function (filename) {
|
|
return G3FileUtils.getFileIcon(filename);
|
|
},
|
|
|
|
/**
|
|
* 获取文件Blob
|
|
* @param {string} url - 文件URL
|
|
* @returns {Promise<ArrayBuffer>} 文件二进制数据
|
|
*/
|
|
getFileBlob: async function (url) {
|
|
if (!url) {
|
|
throw new Error("无效的文件URL");
|
|
}
|
|
|
|
let fetchUrl = url;
|
|
if (this.options.mode === "aliyun" && this.aliyunKeyCache) {
|
|
const { accessid, policy, signature } = this.aliyunKeyCache.data;
|
|
if (accessid && policy && signature) {
|
|
const separator = url.includes("?") ? "&" : "?";
|
|
fetchUrl = `${url}${separator}OSSAccessKeyId=${accessid}&policy=${policy}&Signature=${signature}`;
|
|
}
|
|
}
|
|
|
|
const res = await fetch(fetchUrl);
|
|
|
|
if (!res.ok) {
|
|
throw new Error("网络请求失败,状态码:" + res.status);
|
|
}
|
|
|
|
const buffer = await res.arrayBuffer();
|
|
return buffer;
|
|
},
|
|
|
|
// ============================================================================
|
|
// UI 渲染相关方法
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 渲染
|
|
* @returns {G3File} 实例
|
|
*/
|
|
render: function () {
|
|
const html = `
|
|
<div class="g3-file-container">
|
|
<div class="g3-left-panel">
|
|
<div class="g3-header">
|
|
<div class="g3-check-all">
|
|
<input type="checkbox" class="g3-check-all-checkbox" style="cursor: pointer;" />
|
|
<span>${this.language.selectAll}</span>
|
|
</div>
|
|
<button class="g3-btn g3-btn-download-all" disabled>
|
|
${G3FileIcons.action.download}
|
|
${this.language.downloadBatch}
|
|
</button>
|
|
<button class="g3-btn g3-btn-delete-all" disabled>
|
|
${G3FileIcons.action.delete}
|
|
${this.language.deleteBatch}
|
|
</button>
|
|
</div>
|
|
<div class="g3-main" style="display: none;">main</div>
|
|
<div class="g3-empty-tip">
|
|
<span>${this.language.emptyTip1}</span>
|
|
<span>${this.language.emptyTip2}</span>
|
|
<span>${this.language.emptyTip3}</span>
|
|
</div>
|
|
<div class="g3-footer">
|
|
<div class="g3-pending-count">
|
|
<span>${this.language.pending}</span>
|
|
<span class="g3-pending-number">0</span>
|
|
</div>
|
|
|
|
<div>
|
|
<button class="g3-btn g3-btn-select" disabled>
|
|
${G3FileIcons.action.select}
|
|
${this.language.select}
|
|
</button>
|
|
<button class="g3-btn g3-btn-upload" disabled>
|
|
${G3FileIcons.action.upload}
|
|
${this.language.upload}
|
|
</button>
|
|
</div>
|
|
<input type="file" class="g3-file-input" style="display: none;" multiple />
|
|
</div>
|
|
</div>
|
|
<div class="g3-preview-panel">
|
|
<div class="g3-preview-toolbar">
|
|
<div class="g3-preview-nav">
|
|
<button class="g3-preview-btn g3-preview-prev" disabled>
|
|
${G3FileIcons.preview.prev}
|
|
</button>
|
|
<span class="g3-preview-counter">0/0</span>
|
|
<button class="g3-preview-btn g3-preview-next" disabled>
|
|
${G3FileIcons.preview.next}
|
|
</button>
|
|
</div>
|
|
<span class="g3-preview-name">${this.language.selectFiles}</span>
|
|
</div>
|
|
<div class="g3-preview-content">
|
|
<div class="g3-preview-placeholder">
|
|
<h3>${this.language.filePreview}</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="g3-global-menu"></div>
|
|
</div>
|
|
`;
|
|
|
|
this.$container.html(html);
|
|
|
|
this.$fileList = this.$container.find(".g3-main");
|
|
this.$emptyTip = this.$container.find(".g3-empty-tip");
|
|
this.$checkAll = this.$container.find(".g3-check-all-checkbox");
|
|
this.$btnDownload = this.$container.find(".g3-btn-download-all");
|
|
this.$btnDelete = this.$container.find(".g3-btn-delete-all");
|
|
this.$btnUpload = this.$container.find(".g3-btn-upload");
|
|
this.$btnSelect = this.$container.find(".g3-btn-select");
|
|
this.$fileInput = this.$container.find(".g3-file-input");
|
|
this.$pendingCount = this.$container.find(".g3-pending-number");
|
|
|
|
this.$previewPanel = this.$container.find(".g3-preview-panel");
|
|
this.$previewToolbar = this.$container.find(".g3-preview-toolbar");
|
|
this.$previewContent = this.$container.find(".g3-preview-content");
|
|
this.$btnPrev = this.$container.find(".g3-preview-prev");
|
|
this.$btnNext = this.$container.find(".g3-preview-next");
|
|
this.$previewName = this.$container.find(".g3-preview-name");
|
|
this.$previewCounter = this.$container.find(".g3-preview-counter");
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* 渲染操作菜单
|
|
*/
|
|
renderActionMenu: function () {
|
|
var actionMenu = this.$container.find(".g3-global-menu");
|
|
|
|
if (!actionMenu.length) {
|
|
this.$container.append('<div class="g3-global-menu"></div>');
|
|
actionMenu = this.$container.find(".g3-global-menu");
|
|
}
|
|
|
|
var actionMenuState = this.actionMenuState;
|
|
|
|
if (!actionMenuState.open) {
|
|
actionMenu.removeClass("is-open");
|
|
return;
|
|
}
|
|
|
|
var html = "";
|
|
|
|
actionMenuState.items.forEach(function (item) {
|
|
var isDisabled = false;
|
|
if (typeof item.disabled === "function") {
|
|
isDisabled = item.disabled(self.getFileById(actionMenuState.fileId));
|
|
} else if (item.disabled !== undefined) {
|
|
isDisabled = item.disabled;
|
|
}
|
|
var disabledClass = isDisabled ? "is-disabled" : "";
|
|
html += `<div class="g3-action-menu-item ${disabledClass}">${item.title}</div>`;
|
|
});
|
|
|
|
actionMenu.html(html);
|
|
|
|
actionMenu
|
|
.find(".g3-action-menu-item")
|
|
.off("click")
|
|
.on("click", function (e) {
|
|
e.stopPropagation();
|
|
|
|
var idx = $(this).index();
|
|
var item = actionMenuState.items[idx];
|
|
|
|
var isDisabled = false;
|
|
if (typeof item.disabled === "function") {
|
|
isDisabled = item.disabled(
|
|
self.getFileById(actionMenuState.fileId),
|
|
);
|
|
} else if (item.disabled !== undefined) {
|
|
isDisabled = item.disabled;
|
|
}
|
|
|
|
if (isDisabled) return;
|
|
|
|
item.onClick &&
|
|
item.onClick(self.getFileById(actionMenuState.fileId));
|
|
|
|
self.closeActionMenu();
|
|
});
|
|
|
|
var $anchor = $(actionMenuState.anchorEl).closest(".g3-action-more");
|
|
|
|
if (!$anchor.length) {
|
|
return;
|
|
}
|
|
|
|
var rect = $anchor[0].getBoundingClientRect();
|
|
var $mainContainer = this.$container.find(".main");
|
|
var mainRect =
|
|
$mainContainer.length > 0
|
|
? $mainContainer[0].getBoundingClientRect()
|
|
: null;
|
|
|
|
var menuWidth = 160;
|
|
var itemHeight = 36;
|
|
var menuHeight = actionMenuState.items.length * itemHeight;
|
|
|
|
var top = rect.bottom;
|
|
var left = rect.right - menuWidth;
|
|
|
|
if (mainRect) {
|
|
var spaceBottom = mainRect.bottom - rect.bottom; // main 容器底部到锚点的距离
|
|
var spaceTop = rect.top - mainRect.top; // 锚点到 main 容器顶部的距离
|
|
|
|
var openUp = spaceBottom < menuHeight && spaceTop > menuHeight;
|
|
|
|
if (openUp) {
|
|
top = rect.top - menuHeight; // 在锚点上方显示
|
|
}
|
|
|
|
// 确保菜单不超出 main 容器左右边界
|
|
if (left < mainRect.left) {
|
|
left = mainRect.left;
|
|
}
|
|
if (left + menuWidth > mainRect.right) {
|
|
left = mainRect.right - menuWidth;
|
|
}
|
|
|
|
// 确保菜单不超出 main 容器上下边界
|
|
if (top < mainRect.top) {
|
|
top = mainRect.top;
|
|
}
|
|
if (top + menuHeight > mainRect.bottom) {
|
|
top = mainRect.bottom - menuHeight;
|
|
}
|
|
} else {
|
|
// 如果没有找到 main 容器,使用视口边界
|
|
var viewportWidth = window.innerWidth;
|
|
var viewportHeight = window.innerHeight;
|
|
|
|
// 确保菜单不超出视口左右边界
|
|
if (left < 0) {
|
|
left = 0;
|
|
}
|
|
if (left + menuWidth > viewportWidth) {
|
|
left = viewportWidth - menuWidth;
|
|
}
|
|
|
|
// 确保菜单不超出视口上下边界
|
|
if (top < 0) {
|
|
top = 0;
|
|
}
|
|
if (top + menuHeight > viewportHeight) {
|
|
top = viewportHeight - menuHeight;
|
|
}
|
|
}
|
|
|
|
actionMenu.css({
|
|
top: top,
|
|
left: left,
|
|
});
|
|
|
|
requestAnimationFrame(function () {
|
|
actionMenu.addClass("is-open");
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 创建文件项
|
|
* @param {Object} file - 文件对象
|
|
* @returns {string} 文件项HTML
|
|
*/
|
|
createFileItem: function (file) {
|
|
const downloadDisabled = !this.hasPermission(file, "download");
|
|
const previewDisabled = !this.hasPermission(file, "view");
|
|
const deleteDisabled = !this.hasPermission(file, "delete");
|
|
|
|
let content = "";
|
|
|
|
if (G3FileUtils.isImageFile(file.name)) {
|
|
content = `
|
|
<div class="g3-img-container">
|
|
<img src="${file.url}" alt="${file.name}" class="g3-file-img" onError="this.parentElement.classList.add('is-error'); this.style.display='none';" />
|
|
<div class="g3-img-error">
|
|
${G3FileIcons.preview.imgLoadError}
|
|
</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
content = this.getFileIcon(file.name);
|
|
}
|
|
|
|
let statusBadge = "";
|
|
switch (file.status) {
|
|
case "pending":
|
|
statusBadge = `
|
|
<div class="g3-status-badge g3-status-pending">
|
|
${this.language.pending}
|
|
</div>
|
|
`;
|
|
break;
|
|
case "uploading":
|
|
statusBadge = `
|
|
<div class="g3-status-badge g3-status-uploading">
|
|
${this.language.uploading}
|
|
</div>
|
|
`;
|
|
break;
|
|
case "failed":
|
|
statusBadge = `
|
|
<div class="g3-status-badge g3-status-failed">
|
|
${this.language.uploadFailed}
|
|
</div>
|
|
`;
|
|
break;
|
|
}
|
|
|
|
// <span>${G3FileUtils.formatDate(file.uploadDate)}</span>
|
|
|
|
var actionMenuItems =
|
|
(this.options.customActionMenu &&
|
|
this.options.customActionMenu(file)) ||
|
|
[];
|
|
var moreActionHtml =
|
|
actionMenuItems && actionMenuItems.length > 0
|
|
? `
|
|
<div class="g3-file-action g3-action-more" title="${this.language.more}">
|
|
${G3FileIcons.action.more}
|
|
</div>
|
|
`
|
|
: "";
|
|
|
|
return `
|
|
<div class="g3-file-item" data-id="${file._id}">
|
|
<input type="checkbox" class="g3-file-checkbox" />
|
|
<label class="g3-checkbox-label"></label>
|
|
<div class="g3-file-preview">${content}</div>
|
|
${statusBadge}
|
|
${deleteDisabled ? "" : `<div class="g3-file-delete-btn">${G3FileIcons.action.delete}</div>`}
|
|
|
|
<div class="g3-file-info">
|
|
<span class="g3-file-name">${file.name}</span>
|
|
<span>${G3FileUtils.formatFileSize(file.size)}</span>
|
|
<span>${G3FileUtils.formatDate(file.uploadDate)}</span>
|
|
</div>
|
|
|
|
<div class="g3-file-actions">
|
|
<div class="g3-file-action g3-action-preview ${previewDisabled ? "is-disabled" : ""}" title="${this.language.preview}">
|
|
${G3FileIcons.action.preview}
|
|
</div>
|
|
|
|
<div class="g3-file-action g3-action-download ${downloadDisabled ? "is-disabled" : ""}" title="${this.language.download}">
|
|
${G3FileIcons.action.download}
|
|
</div>
|
|
|
|
${moreActionHtml}
|
|
</div>
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
/**
|
|
* 渲染文件列表
|
|
*/
|
|
renderFileList: function () {
|
|
if (this.files.length === 0) {
|
|
this.$fileList.hide();
|
|
this.$emptyTip.show();
|
|
return;
|
|
}
|
|
|
|
this.$fileList.show();
|
|
this.$emptyTip.hide();
|
|
|
|
var html = "";
|
|
var self = this;
|
|
this.files.forEach(function (file) {
|
|
html += self.createFileItem(file);
|
|
});
|
|
|
|
this.$fileList.html(html);
|
|
},
|
|
|
|
/**
|
|
* 更新UI
|
|
*/
|
|
updateUI: function () {
|
|
var self = this;
|
|
var hasSelectedFiles = this.selectedFiles.size > 0;
|
|
|
|
// 更新全局按钮状态
|
|
this.$btnSelect?.prop("disabled", !this.hasPermission(null, "select"));
|
|
this.$btnUpload?.prop("disabled", !this.hasPermission(null, "upload"));
|
|
|
|
// 批量下载按钮:需要全局权限且有选中文件
|
|
var canBatchDownload =
|
|
this.hasPermission(null, "download") && hasSelectedFiles;
|
|
this.$btnDownload?.prop("disabled", !canBatchDownload);
|
|
|
|
// 批量删除按钮:需要全局权限且有选中文件
|
|
var canBatchDelete =
|
|
this.hasPermission(null, "delete") && hasSelectedFiles;
|
|
this.$btnDelete?.prop("disabled", !canBatchDelete);
|
|
|
|
// 更新全选checkbox状态
|
|
var allSelected =
|
|
this.files.length > 0 && this.selectedFiles.size === this.files.length;
|
|
this.$checkAll?.prop("checked", allSelected);
|
|
|
|
// 更新待上传数量
|
|
var pendingCount = this.files.filter(
|
|
(file) => file.status === "pending",
|
|
).length;
|
|
this.$pendingCount?.text(pendingCount);
|
|
|
|
// 更新文件项状态
|
|
this.$fileList.find(".g3-file-item").each(function () {
|
|
var $item = $(this);
|
|
var fileId = $item.data("id");
|
|
var file = self.files.find((f) => f._id === fileId);
|
|
|
|
if (!file) return;
|
|
|
|
// 更新checkbox选中状态
|
|
var isSelected = self.selectedFiles.has(fileId);
|
|
$item.find(".g3-file-checkbox").prop("checked", isSelected);
|
|
|
|
// 更新按钮权限状态
|
|
var canDownload = self.hasPermission(file, "download");
|
|
var canView = self.hasPermission(file, "view");
|
|
var canDelete = self.hasPermission(file, "delete");
|
|
|
|
$item
|
|
.find(".g3-action-download")
|
|
.toggleClass("is-disabled", !canDownload);
|
|
$item.find(".g3-action-preview").toggleClass("is-disabled", !canView);
|
|
});
|
|
},
|
|
|
|
updateFileItem: function (fileId) {
|
|
var file = this.getFileById(fileId);
|
|
var $item = this.$fileList.find(`.g3-file-item[data-id="${fileId}"]`);
|
|
if (!file || !$item.length) return;
|
|
|
|
$item
|
|
.find(".g3-file-checkbox")
|
|
.prop("checked", this.selectedFiles.has(fileId));
|
|
|
|
var $badge = $item.find(".g3-status-badge");
|
|
if (file.status === "pending") {
|
|
$badge
|
|
.attr("class", "g3-status-badge g3-status-pending")
|
|
.text(this.language.pending)
|
|
.show();
|
|
} else if (file.status === "uploading") {
|
|
$badge
|
|
.attr("class", "g3-status-badge g3-status-uploading")
|
|
.text(this.language.uploading)
|
|
.show();
|
|
} else if (file.status === "failed") {
|
|
$badge
|
|
.attr("class", "g3-status-badge g3-status-failed")
|
|
.text(this.language.uploadFailed)
|
|
.show();
|
|
} else if (file.status === "uploaded") {
|
|
$badge.attr("class", "g3-status-badge g3-status-uploaded").hide();
|
|
}
|
|
|
|
var canDownload = this.hasPermission(file, "download");
|
|
var canView = this.hasPermission(file, "view");
|
|
$item
|
|
.find(".g3-action-download")
|
|
.toggleClass("is-disabled", !canDownload);
|
|
$item.find(".g3-action-preview").toggleClass("is-disabled", !canView);
|
|
},
|
|
|
|
/**
|
|
* 获取已上传的文件列表(仅status为uploaded的文件)
|
|
*/
|
|
getUploadedFiles: function () {
|
|
return this.files.filter(function (file) {
|
|
return file.status === "uploaded";
|
|
});
|
|
},
|
|
|
|
isPreviewableFile: function (fileName) {
|
|
var nonPreviewableExts = [
|
|
".rar",
|
|
".jar",
|
|
".exe",
|
|
".zip",
|
|
".7z",
|
|
".tar",
|
|
".gz",
|
|
".bz2",
|
|
".iso",
|
|
".dll",
|
|
".bin",
|
|
".msi",
|
|
".cab",
|
|
".rpm",
|
|
".deb",
|
|
".apk",
|
|
".ipa",
|
|
".dat",
|
|
".bak",
|
|
".tmp",
|
|
".log",
|
|
".cache",
|
|
".db",
|
|
".sqlite",
|
|
];
|
|
var ext = fileName.toLowerCase().substring(fileName.lastIndexOf("."));
|
|
return !nonPreviewableExts.includes(ext);
|
|
},
|
|
|
|
getPreviewFileIndex: function (fileId) {
|
|
var uploadedFiles = this.getUploadedFiles();
|
|
return uploadedFiles.findIndex(function (f) {
|
|
return f._id === fileId;
|
|
});
|
|
},
|
|
|
|
previewPrev: function () {
|
|
var uploadedFiles = this.getUploadedFiles();
|
|
if (uploadedFiles.length === 0) return;
|
|
|
|
var currentIndex = this.getPreviewFileIndex(this.currentPreviewFileId);
|
|
var newIndex =
|
|
currentIndex > 0 ? currentIndex - 1 : uploadedFiles.length - 1;
|
|
this.currentPreviewFileId = uploadedFiles[newIndex]._id;
|
|
this.updatePreview();
|
|
},
|
|
|
|
previewNext: function () {
|
|
var uploadedFiles = this.getUploadedFiles();
|
|
if (uploadedFiles.length === 0) return;
|
|
|
|
var currentIndex = this.getPreviewFileIndex(this.currentPreviewFileId);
|
|
var newIndex =
|
|
currentIndex < uploadedFiles.length - 1 ? currentIndex + 1 : 0;
|
|
this.currentPreviewFileId = uploadedFiles[newIndex]._id;
|
|
this.updatePreview();
|
|
},
|
|
/**
|
|
* 更新预览区域显示
|
|
*/
|
|
updatePreview: function () {
|
|
var allFiles = this.getUploadedFiles();
|
|
var total = allFiles.length;
|
|
|
|
if (total === 0) {
|
|
this.$previewContent.html(`
|
|
<div class="g3-preview-placeholder">
|
|
<p>${this.language.selectFiles}</p>
|
|
</div>
|
|
`);
|
|
this.$previewName.text(this.language.selectFiles);
|
|
this.$previewCounter.text("0/0");
|
|
this.$btnPrev.prop("disabled", true);
|
|
this.$btnNext.prop("disabled", true);
|
|
this.currentPreviewFileId = null;
|
|
this.previewCache = null;
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!this.currentPreviewFileId ||
|
|
!this.getFileById(this.currentPreviewFileId)
|
|
) {
|
|
this.currentPreviewFileId = allFiles[0]._id;
|
|
}
|
|
|
|
var currentFile = this.getFileById(this.currentPreviewFileId);
|
|
if (!currentFile) {
|
|
this.currentPreviewFileId = allFiles[0]._id;
|
|
currentFile = allFiles[0];
|
|
}
|
|
|
|
var currentIndex = this.getPreviewFileIndex(this.currentPreviewFileId);
|
|
var isUploaded = currentFile.status === "uploaded";
|
|
var isPreviewable = this.isPreviewableFile(currentFile.name);
|
|
var fileUrl =
|
|
currentFile.url && currentFile.url !== "" ? currentFile.url : null;
|
|
|
|
this.$btnPrev.prop("disabled", currentIndex === 0);
|
|
this.$btnNext.prop("disabled", currentIndex === total - 1);
|
|
|
|
this.$previewName.text(currentFile.name);
|
|
var counterText = this.language.previewCount
|
|
.replace("{current}", currentIndex + 1)
|
|
.replace("{total}", total);
|
|
this.$previewCounter.text(counterText);
|
|
|
|
var self = this;
|
|
|
|
if (isUploaded && fileUrl && isPreviewable) {
|
|
var previewUrl = this.options.previewUrl || "";
|
|
var fullPreviewUrl = previewUrl
|
|
? previewUrl + G3FileUtils.base64Encode(fileUrl)
|
|
: fileUrl;
|
|
|
|
if (!this.previewCache) {
|
|
this.previewCache = { currentUrl: null, nextUrl: null };
|
|
}
|
|
|
|
if (this.previewCache.currentUrl === fullPreviewUrl) {
|
|
return;
|
|
}
|
|
|
|
var $content = this.$previewContent;
|
|
var $currentIframe = $content.find(".g3-preview-iframe.current");
|
|
var $nextIframe = $content.find(".g3-preview-iframe.next");
|
|
|
|
if (
|
|
$nextIframe.length &&
|
|
this.previewCache.nextUrl === fullPreviewUrl
|
|
) {
|
|
$currentIframe.removeClass("current").addClass("next").hide();
|
|
$nextIframe.removeClass("next").addClass("current").show();
|
|
this.previewCache.currentUrl = fullPreviewUrl;
|
|
this.previewCache.nextUrl = null;
|
|
return;
|
|
}
|
|
|
|
if ($nextIframe.length) {
|
|
$nextIframe.remove();
|
|
}
|
|
|
|
var newIframe = $(`
|
|
<iframe
|
|
src="${fullPreviewUrl}"
|
|
class="g3-preview-iframe next"
|
|
frameborder="0"
|
|
allowfullscreen
|
|
></iframe>
|
|
`);
|
|
|
|
newIframe.on("load", function () {
|
|
if ($currentIframe.length) {
|
|
$currentIframe.css("opacity", "0");
|
|
setTimeout(function () {
|
|
$currentIframe.remove();
|
|
}, 300);
|
|
}
|
|
newIframe.removeClass("next").addClass("current");
|
|
self.previewCache.currentUrl = fullPreviewUrl;
|
|
self.previewCache.nextUrl = null;
|
|
});
|
|
|
|
newIframe.on("error", function () {
|
|
newIframe.remove();
|
|
self.showPlaceholder(
|
|
$content,
|
|
`<p class="g3-url-error">${self.language.urlError}</p>`,
|
|
);
|
|
});
|
|
|
|
$content.append(newIframe);
|
|
this.previewCache.nextUrl = fullPreviewUrl;
|
|
} else {
|
|
this.previewCache = null;
|
|
var tipsHtml = "";
|
|
if (!isUploaded) {
|
|
tipsHtml = `<p class="g3-upload-tip">${this.language.uploadToPreview}</p>`;
|
|
} else if (!isPreviewable) {
|
|
tipsHtml = `<p class="g3-cannot-preview">${this.language.downloadToPreview}</p>`;
|
|
} else if (!fileUrl) {
|
|
tipsHtml = `<p class="g3-url-error">${this.language.urlError}</p>`;
|
|
}
|
|
|
|
this.showPlaceholder(this.$previewContent, tipsHtml);
|
|
}
|
|
},
|
|
|
|
showPlaceholder: function ($container, tipsHtml) {
|
|
$container.find(".g3-preview-iframe").remove();
|
|
$container.html(`
|
|
<div class="g3-preview-placeholder">
|
|
${tipsHtml}
|
|
</div>
|
|
`);
|
|
},
|
|
|
|
setPreviewFile: function (fileId) {
|
|
var file = this.getFileById(fileId);
|
|
if (file) {
|
|
this.currentPreviewFileId = fileId;
|
|
this.updatePreview();
|
|
}
|
|
},
|
|
|
|
// ============================================================================
|
|
// 事件绑定相关方法
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 绑定事件
|
|
*/
|
|
bindEvents: function () {
|
|
var self = this;
|
|
var documentActionMenuEvent =
|
|
"click.g3file_actionmenu_" + this.instanceId;
|
|
var documentPasteEvent = "paste" + this.documentNamespace;
|
|
|
|
// 选择全部
|
|
this.$checkAll.off("change").on("change", function () {
|
|
var checked = $(this).is(":checked");
|
|
|
|
self.selectedFiles.clear();
|
|
|
|
if (checked) {
|
|
self.files.forEach(function (file) {
|
|
self.selectedFiles.add(file._id);
|
|
});
|
|
}
|
|
|
|
self.updateUI();
|
|
});
|
|
|
|
// 批量下载
|
|
this.$btnDownload.off("click").on("click", function () {
|
|
if (!self.hasPermission(null, "download")) return;
|
|
var selectedFiles = self.getSelectedFiles();
|
|
if (selectedFiles.length === 0) return;
|
|
|
|
self.handleDownload(selectedFiles, { batch: true });
|
|
});
|
|
|
|
// 批量删除
|
|
this.$btnDelete.off("click").on("click", function () {
|
|
if (!self.hasPermission(null, "delete")) return;
|
|
var selectedFiles = self.getSelectedFiles();
|
|
if (selectedFiles.length === 0) return;
|
|
|
|
self.handleDelete(selectedFiles);
|
|
});
|
|
|
|
// 选择文件
|
|
this.$btnSelect.off("click").on("click", function () {
|
|
if (!self.hasPermission(null, "select")) return;
|
|
self.$fileInput.click();
|
|
});
|
|
|
|
// 上传
|
|
this.$btnUpload.off("click").on("click", function () {
|
|
if (!self.hasPermission(null, "upload")) return;
|
|
var pendingFiles = self.files.filter(
|
|
(file) => file.status === "pending",
|
|
);
|
|
if (typeof self.options.onUpload === "function") {
|
|
self.options.onUpload(pendingFiles);
|
|
} else {
|
|
// 使用新的上传逻辑
|
|
self.handleUpload(self.files);
|
|
}
|
|
});
|
|
|
|
// 文件选择
|
|
this.$fileInput.off("change").on("change", function (e) {
|
|
var files = e.target.files;
|
|
if (!files || files.length === 0) return;
|
|
|
|
self.handleFileSelect(files);
|
|
|
|
// 重置文件输入框
|
|
self.$fileInput.val("");
|
|
});
|
|
|
|
// 点击其他地方关闭菜单
|
|
$(document)
|
|
.off(documentActionMenuEvent)
|
|
.on(documentActionMenuEvent, function (e) {
|
|
if (
|
|
!$(e.target).closest(".g3-action-more").length &&
|
|
!$(e.target).closest(".g3-global-menu").length
|
|
) {
|
|
self.closeActionMenu();
|
|
}
|
|
});
|
|
|
|
// 拖拽文件
|
|
const $container = this.$container;
|
|
|
|
$container
|
|
.off("dragenter dragleave dragover drop")
|
|
.on("dragenter", function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (self.hasPermission(null, "upload")) {
|
|
$container.addClass("drag-over");
|
|
}
|
|
})
|
|
.on("dragover", function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (self.hasPermission(null, "upload")) {
|
|
$container.addClass("drag-over");
|
|
}
|
|
})
|
|
.on("dragleave", function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (e.target === $container[0]) {
|
|
$container.removeClass("drag-over");
|
|
}
|
|
})
|
|
.on("drop", function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
$container.removeClass("drag-over");
|
|
|
|
if (!self.hasPermission(null, "upload")) return;
|
|
|
|
var files = e.originalEvent.dataTransfer.files;
|
|
if (files && files.length > 0) {
|
|
self.handleFileSelect(files);
|
|
}
|
|
});
|
|
|
|
// 粘贴文件
|
|
$(document)
|
|
.off(documentPasteEvent)
|
|
.on(documentPasteEvent, function (e) {
|
|
var clipboardData = e.originalEvent.clipboardData;
|
|
var files = clipboardData.files;
|
|
if (files && files.length > 0) {
|
|
if (!self.hasPermission(null, "upload")) return;
|
|
e.preventDefault();
|
|
self.handleFileSelect(files);
|
|
}
|
|
});
|
|
|
|
// 预览上一个
|
|
this.$btnPrev.off("click").on("click", function () {
|
|
self.previewPrev();
|
|
});
|
|
|
|
// 预览下一个
|
|
this.$btnNext.off("click").on("click", function () {
|
|
self.previewNext();
|
|
});
|
|
|
|
// 文件列表事件委托(只在 init 时绑定一次)
|
|
this.$fileList
|
|
// 预览按钮
|
|
.on("click", ".g3-action-preview", function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var id = $(this).closest(".g3-file-item").data("id");
|
|
var file = self.getFileById(id);
|
|
if (!file) return;
|
|
if (!self.hasPermission(file, "view")) return;
|
|
self.setPreviewFile(id);
|
|
})
|
|
// 下载按钮
|
|
.on("click", ".g3-action-download", function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var id = $(this).closest(".g3-file-item").data("id");
|
|
var file = self.getFileById(id);
|
|
if (!file) return;
|
|
if (!self.hasPermission(file, "download")) return;
|
|
self.handleDownload([file], { batch: false });
|
|
})
|
|
// 复选框
|
|
.on("change", ".g3-file-checkbox", function () {
|
|
var id = $(this).closest(".g3-file-item").data("id");
|
|
if ($(this).is(":checked")) {
|
|
self.selectedFiles.add(id);
|
|
} else {
|
|
self.selectedFiles.delete(id);
|
|
}
|
|
self.updateUI();
|
|
})
|
|
// 删除按钮
|
|
.on("click", ".g3-file-delete-btn", function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var id = $(this).closest(".g3-file-item").data("id");
|
|
var file = self.getFileById(id);
|
|
if (!file) return;
|
|
if (!self.hasPermission(file, "delete")) return;
|
|
self.handleDelete([file]);
|
|
})
|
|
// 文件项单击预览
|
|
.on("click", ".g3-file-item", function (e) {
|
|
if (
|
|
$(e.target).closest(
|
|
".g3-file-action, .g3-file-checkbox, .g3-file-delete-btn",
|
|
).length
|
|
) {
|
|
return;
|
|
}
|
|
var id = $(this).data("id");
|
|
var file = self.getFileById(id);
|
|
if (!file) return;
|
|
if (!self.hasPermission(file, "view")) return;
|
|
self.setPreviewFile(id);
|
|
})
|
|
// 文件项双击
|
|
.on("dblclick", ".g3-file-item", function (e) {
|
|
if (
|
|
$(e.target).closest(
|
|
".g3-file-action, .g3-file-checkbox, .g3-file-delete-btn",
|
|
).length
|
|
) {
|
|
return;
|
|
}
|
|
var id = $(this).data("id");
|
|
var file = self.getFileById(id);
|
|
if (!file) return;
|
|
if (!self.hasPermission(file, "view")) return;
|
|
|
|
if (typeof self.options.onDblClick === "function") {
|
|
self.options.onDblClick(file);
|
|
return;
|
|
}
|
|
|
|
var isUploaded = file.status === "uploaded";
|
|
var isPreviewable = self.isPreviewableFile(file.name);
|
|
var fileUrl = file.url && file.url !== "" ? file.url : null;
|
|
|
|
if (isUploaded && fileUrl && isPreviewable) {
|
|
var previewUrl = self.options.previewUrl || "";
|
|
var fullPreviewUrl = previewUrl
|
|
? previewUrl + G3FileUtils.base64Encode(fileUrl)
|
|
: fileUrl;
|
|
window.open(fullPreviewUrl, "_blank");
|
|
}
|
|
})
|
|
|
|
// 更多按钮
|
|
.on("click", ".g3-action-more", function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var $el = $(this).closest(".g3-file-action");
|
|
var id = $el.closest(".g3-file-item").data("id");
|
|
var file = self.getFileById(id);
|
|
if (!file) return;
|
|
if (
|
|
self.actionMenuState.open &&
|
|
self.actionMenuState.fileId === file._id
|
|
) {
|
|
self.closeActionMenu();
|
|
return;
|
|
}
|
|
self.openActionMenu($el, file);
|
|
});
|
|
|
|
// 任务关闭按钮事件委托
|
|
this.$container.on("click", ".g3-task-close", function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var taskId = $(this).closest(".g3-task-center").data("task-id");
|
|
self.closeTask(taskId);
|
|
});
|
|
},
|
|
|
|
// ============================================================================
|
|
// 文件处理相关方法
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 处理文件选择
|
|
* @param {FileList} files - 文件列表
|
|
*/
|
|
handleFileSelect: async function (files) {
|
|
var self = this;
|
|
var maxSize = this.options.maxFileSize || 10 * 1024 * 1024;
|
|
var validFiles = [];
|
|
var invalidFiles = [];
|
|
|
|
for (var i = 0; i < files.length; i++) {
|
|
var file = files[i];
|
|
if (file.size > maxSize) {
|
|
invalidFiles.push(file);
|
|
} else {
|
|
validFiles.push(file);
|
|
}
|
|
}
|
|
|
|
if (invalidFiles.length > 0) {
|
|
var errorMsg = this.language.fileSizeError || "文件大小超过限制";
|
|
console.warn(
|
|
errorMsg + ": " + invalidFiles.map((f) => f.name).join(", "),
|
|
);
|
|
}
|
|
|
|
if (validFiles.length > 0) {
|
|
var tempFiles = [];
|
|
|
|
for (var j = 0; j < validFiles.length; j++) {
|
|
var file = validFiles[j];
|
|
let url = "";
|
|
let compressedFile = file;
|
|
|
|
if (G3FileUtils.isImageFile(file.name)) {
|
|
if (typeof self.options.compressImage === "function") {
|
|
try {
|
|
compressedFile = await self.options.compressImage(file);
|
|
} catch (e) {
|
|
console.warn("外部压缩算法执行失败,使用内部压缩:", e);
|
|
compressedFile = await G3FileUtils.compressImage(file);
|
|
}
|
|
} else {
|
|
compressedFile = await G3FileUtils.compressImage(file);
|
|
}
|
|
|
|
url = URL.createObjectURL(compressedFile);
|
|
self.objectUrls.set(G3FileUtils.generateUniqueId(), url);
|
|
}
|
|
|
|
tempFiles.push({
|
|
_id: G3FileUtils.generateUniqueId(),
|
|
name: file.name,
|
|
size: compressedFile.size,
|
|
uploadDate: new Date().toISOString(),
|
|
url: url,
|
|
file: compressedFile,
|
|
status: "pending",
|
|
});
|
|
}
|
|
|
|
self.setFiles(self.files.concat(tempFiles));
|
|
|
|
if (typeof self.options.onSelect === "function") {
|
|
self.options.onSelect(validFiles);
|
|
}
|
|
|
|
self.autoScrollToBottom();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 自动滚动到底部
|
|
*/
|
|
autoScrollToBottom: function () {
|
|
this.$fileList.scrollTop(this.$fileList[0].scrollHeight);
|
|
},
|
|
|
|
/**
|
|
* 处理下载
|
|
* @param {Array} files - 文件数组
|
|
* @param {Object} options - 下载选项
|
|
*/
|
|
handleDownload: function (files, options) {
|
|
if (typeof this.options.onDownload === "function") {
|
|
this.options.onDownload(files, options);
|
|
return;
|
|
}
|
|
|
|
if (options.batch) {
|
|
this.internalDownloadBatch(files);
|
|
} else {
|
|
this.internalDownload(files[0]);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 处理删除
|
|
* @param {Array} files - 文件数组
|
|
*/
|
|
handleDelete: function (files) {
|
|
var self = this;
|
|
|
|
if (this.uiLocked) {
|
|
console.warn("UI is locked, please wait...");
|
|
return;
|
|
}
|
|
|
|
if (typeof this.options.onDeleteBefore === "function") {
|
|
var result = this.options.onDeleteBefore(files);
|
|
if (result === false) return;
|
|
if (typeof result.then === "function") {
|
|
result.then(function (confirmed) {
|
|
if (confirmed) self.internalDelete(files);
|
|
});
|
|
return;
|
|
}
|
|
if (typeof result === "function") {
|
|
result(function (confirmed) {
|
|
if (confirmed) self.internalDelete(files);
|
|
});
|
|
return;
|
|
}
|
|
} else if (typeof this.options.onDelete === "function") {
|
|
this.options.onDelete(files, function (success) {
|
|
if (success) {
|
|
self.internalDelete(files);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.internalDelete(files);
|
|
},
|
|
|
|
/**
|
|
* 内部下载
|
|
* @param {Object} file - 文件对象
|
|
*/
|
|
internalDownload: async function (file) {
|
|
if (!file) {
|
|
console.warn("下载失败:文件对象无效", file);
|
|
this.showToast({
|
|
title: this.language.error || "错误",
|
|
message:
|
|
this.language.downloadError || "下载失败,请检查文件是否有效",
|
|
type: "error",
|
|
duration: 3000,
|
|
});
|
|
return;
|
|
}
|
|
|
|
console.log("开始下载文件:", file.name);
|
|
|
|
try {
|
|
let blob;
|
|
if (file.status === "uploaded" && file.url) {
|
|
const buffer = await this.getFileBlob(file.url);
|
|
blob = new Blob([buffer]);
|
|
} else if (file.file) {
|
|
blob = file.file;
|
|
} else {
|
|
console.warn("下载失败:文件没有URL或本地文件对象", file);
|
|
this.showToast({
|
|
title: this.language.error || "错误",
|
|
message:
|
|
this.language.downloadError || "下载失败,请检查文件是否有效",
|
|
type: "error",
|
|
duration: 3000,
|
|
});
|
|
return;
|
|
}
|
|
|
|
saveAs(blob, file.name || "未命名文件");
|
|
} catch (err) {
|
|
console.error("文件下载失败:", err);
|
|
this.showToast({
|
|
title: this.language.error || "错误",
|
|
message:
|
|
this.language.downloadError || "下载失败,请检查文件是否有效",
|
|
type: "error",
|
|
duration: 3000,
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 内部批量下载
|
|
* @param {Array} files - 文件数组
|
|
*/
|
|
internalDownloadBatch: async function (files) {
|
|
if (!files || files.length === 0) return;
|
|
|
|
const zip = new JSZip();
|
|
const addedFiles = new Set();
|
|
|
|
const total = files.length;
|
|
let successCount = 0;
|
|
let failedCount = 0;
|
|
|
|
const concurrency = 3;
|
|
const taskId = G3FileUtils.generateUniqueId();
|
|
|
|
this.startTask({
|
|
id: taskId,
|
|
title: this.language.batchDownload || "批量下载",
|
|
total: total,
|
|
current: 0,
|
|
currentItem: "",
|
|
});
|
|
|
|
const downloadOne = async (item) => {
|
|
try {
|
|
this.updateTask({
|
|
id: taskId,
|
|
current: successCount + failedCount,
|
|
currentItem: item.name,
|
|
});
|
|
|
|
let content;
|
|
if (item.status === "uploaded" && item.url) {
|
|
content = await this.getFileBlob(item.url);
|
|
} else if (item.file) {
|
|
content = await item.file.arrayBuffer();
|
|
} else {
|
|
console.warn("跳过失败文件:", item.name, "没有URL或本地文件对象");
|
|
failedCount++;
|
|
return;
|
|
}
|
|
|
|
const uniqueFileName = G3FileUtils.getUniqueFileName(
|
|
item.name,
|
|
addedFiles,
|
|
);
|
|
|
|
zip.file(uniqueFileName, content);
|
|
addedFiles.add(uniqueFileName);
|
|
|
|
successCount++;
|
|
} catch (err) {
|
|
console.warn("跳过失败文件:", item.name, err);
|
|
failedCount++;
|
|
}
|
|
};
|
|
|
|
try {
|
|
const filesCopy = [...files];
|
|
const runner = async () => {
|
|
while (filesCopy.length) {
|
|
const item = filesCopy.shift();
|
|
await downloadOne(item);
|
|
}
|
|
};
|
|
|
|
await Promise.all(Array.from({ length: concurrency }, runner));
|
|
|
|
this.updateTask({
|
|
id: taskId,
|
|
current: total,
|
|
});
|
|
|
|
const blob = await zip.generateAsync({ type: "blob" });
|
|
|
|
const now = new Date();
|
|
const pad = (n) => (n < 10 ? "0" + n : n);
|
|
|
|
const fileName =
|
|
now.getFullYear() +
|
|
pad(now.getMonth() + 1) +
|
|
pad(now.getDate()) +
|
|
pad(now.getHours()) +
|
|
pad(now.getMinutes());
|
|
|
|
saveAs(blob, fileName + ".zip");
|
|
|
|
this.finishTask({
|
|
id: taskId,
|
|
total: total,
|
|
success: successCount,
|
|
failed: failedCount,
|
|
});
|
|
} catch (err) {
|
|
console.error("批量下载失败:", err);
|
|
failedCount = total - successCount;
|
|
|
|
this.finishTask({
|
|
id: taskId,
|
|
total: total,
|
|
success: successCount,
|
|
failed: failedCount,
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 内部删除
|
|
* @param {Array} files - 文件数组
|
|
*/
|
|
internalDelete: function (files) {
|
|
var self = this;
|
|
|
|
files.forEach(function (file) {
|
|
// 清理本地临时连接
|
|
if (file.url && file.status === "pending") {
|
|
self.objectUrls.forEach((url, key) => {
|
|
if (url === file.url) {
|
|
try {
|
|
URL.revokeObjectURL(url);
|
|
self.objectUrls.delete(key);
|
|
} catch (e) {
|
|
console.warn("释放ObjectURL失败:", e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
self.selectedFiles.delete(file._id);
|
|
});
|
|
|
|
this.files = this.files.filter(
|
|
(f) => !files.find((file) => file._id === f._id),
|
|
);
|
|
this.renderFileList();
|
|
this.updateUI();
|
|
},
|
|
|
|
/**
|
|
* 更新文件状态
|
|
* @param {string} fileId - 文件ID
|
|
* @param {string} status - 新状态
|
|
* @param {Object} data - 附加数据
|
|
*/
|
|
updateFileStatus: function (fileId, status, data) {
|
|
const file = this.files.find((f) => f._id === fileId);
|
|
if (file) {
|
|
file.status = status;
|
|
if (data) {
|
|
Object.assign(file, data);
|
|
}
|
|
this.updateFileItem(fileId);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 内部上传方法,支持阿里云 OSS 直传
|
|
* @param {Object} file - 文件对象
|
|
* @param {Function} callback - 上传完成回调
|
|
*/
|
|
internalUpload: async function (file, callback) {
|
|
try {
|
|
const keyData = await this.getValidAliyunKey();
|
|
|
|
const key = this.generateFileKey(keyData, file.name);
|
|
|
|
const formData = new FormData();
|
|
formData.append("OSSAccessKeyId", keyData.accessid);
|
|
formData.append("policy", keyData.policy);
|
|
formData.append("signature", keyData.signature);
|
|
formData.append("key", key);
|
|
formData.append("callback", keyData.callback);
|
|
formData.append("success_action_status", "200");
|
|
formData.append("file", file.file || file);
|
|
|
|
const response = await fetch(keyData.host, {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`上传失败,HTTP 状态码: ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
const uploadedFile = {
|
|
...file,
|
|
status: "uploaded",
|
|
url: result.url || `${keyData.host}/${key}`,
|
|
uploadDate: new Date().toISOString(),
|
|
};
|
|
|
|
callback(null, uploadedFile);
|
|
} catch (error) {
|
|
console.error("[G3File] 上传失败:", error);
|
|
|
|
callback(error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 处理上传逻辑
|
|
* @param {Array} files - 文件数组
|
|
*/
|
|
handleUpload: function (files) {
|
|
var self = this;
|
|
|
|
if (this.uiLocked) {
|
|
console.warn("UI is locked, please wait...");
|
|
return;
|
|
}
|
|
|
|
var customUpload = this.options.uploadConfig.customUpload;
|
|
var concurrency = 3;
|
|
var pendingFiles = files.filter(function (file) {
|
|
return file.status === "pending";
|
|
});
|
|
|
|
if (pendingFiles.length === 0) {
|
|
if (typeof self.options.onUploadComplete === "function") {
|
|
self.options.onUploadComplete(0, 0);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (typeof this.options.onUploadBefore === "function") {
|
|
var result = this.options.onUploadBefore(pendingFiles);
|
|
if (result === false) return;
|
|
if (typeof result.then === "function") {
|
|
result.then(function (confirmed) {
|
|
if (confirmed)
|
|
self.internalHandleUpload(
|
|
pendingFiles,
|
|
customUpload,
|
|
concurrency,
|
|
);
|
|
});
|
|
return;
|
|
}
|
|
if (typeof result === "function") {
|
|
result(function (confirmed) {
|
|
if (confirmed)
|
|
self.internalHandleUpload(
|
|
pendingFiles,
|
|
customUpload,
|
|
concurrency,
|
|
);
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
self.internalHandleUpload(pendingFiles, customUpload, concurrency);
|
|
},
|
|
|
|
internalHandleUpload: function (pendingFiles, customUpload, concurrency) {
|
|
var self = this;
|
|
|
|
var total = pendingFiles.length;
|
|
var taskId = this.startTask({
|
|
id: G3FileUtils.generateUniqueId(),
|
|
title: this.language.batchUpload || "批量上传",
|
|
total: total,
|
|
current: 0,
|
|
currentItem: "",
|
|
});
|
|
|
|
var successCount = 0;
|
|
var failCount = 0;
|
|
var completedCount = 0;
|
|
var index = 0;
|
|
|
|
function uploadNext() {
|
|
if (index >= pendingFiles.length) return Promise.resolve();
|
|
|
|
var file = pendingFiles[index++];
|
|
self.updateFileStatus(file._id, "uploading");
|
|
|
|
return new Promise(function (resolve) {
|
|
async function handleResult(error, uploadedFile) {
|
|
completedCount++;
|
|
|
|
if (error) {
|
|
failCount++;
|
|
self.updateFileStatus(file._id, "failed");
|
|
if (typeof self.options.onUploadError === "function") {
|
|
self.options.onUploadError(file, error);
|
|
}
|
|
} else {
|
|
successCount++;
|
|
self.updateFileStatus(file._id, "uploaded", uploadedFile);
|
|
if (typeof self.options.onUploadSuccess === "function") {
|
|
var result = self.options.onUploadSuccess(uploadedFile);
|
|
if (result && typeof result.then === "function") {
|
|
await result;
|
|
}
|
|
}
|
|
}
|
|
|
|
self.updateTask({
|
|
id: taskId,
|
|
current: completedCount,
|
|
currentItem: file.name,
|
|
});
|
|
|
|
if (completedCount === total) {
|
|
self.finishTask({
|
|
id: taskId,
|
|
total: total,
|
|
success: successCount,
|
|
failed: failCount,
|
|
});
|
|
if (typeof self.options.onUploadComplete === "function") {
|
|
self.options.onUploadComplete(successCount, failCount);
|
|
}
|
|
}
|
|
|
|
resolve();
|
|
}
|
|
|
|
if (typeof customUpload === "function") {
|
|
customUpload(file)
|
|
.then(function (uploadedFile) {
|
|
handleResult(null, uploadedFile);
|
|
})
|
|
.catch(function (error) {
|
|
handleResult(error, null);
|
|
});
|
|
} else if (self.options.mode === "aliyun") {
|
|
self.internalUpload(file, function (error, uploadedFile) {
|
|
handleResult(error, uploadedFile);
|
|
});
|
|
} else {
|
|
handleResult(new Error("No upload method configured"), null);
|
|
}
|
|
}).then(function () {
|
|
return uploadNext();
|
|
});
|
|
}
|
|
|
|
var runners = [];
|
|
for (var i = 0; i < concurrency && i < pendingFiles.length; i++) {
|
|
runners.push(uploadNext());
|
|
}
|
|
|
|
Promise.all(runners);
|
|
},
|
|
|
|
// ============================================================================
|
|
// Toast 提示系统
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Toast 图标映射(从外部 G3FileIcons 导入)
|
|
*/
|
|
toastIcons: {
|
|
success: G3FileIcons.toast.success,
|
|
error: G3FileIcons.toast.error,
|
|
info: G3FileIcons.toast.info,
|
|
warning: G3FileIcons.toast.warning,
|
|
},
|
|
|
|
/**
|
|
* 显示 Toast 提示
|
|
* @param {Object} options - Toast 配置
|
|
* @param {string} options.title - 标题
|
|
* @param {string} options.message - 消息内容
|
|
* @param {string} options.type - 类型:success/error/info/warning
|
|
* @param {number} options.duration - 显示时长(毫秒),默认 3000
|
|
*/
|
|
showToast: function (options) {
|
|
const { title, message, type = "info", duration = 3000 } = options;
|
|
const self = this;
|
|
|
|
const toastContainer = this.$container.find(".g3-toast-container");
|
|
if (!toastContainer.length) {
|
|
this.$container.append('<div class="g3-toast-container"></div>');
|
|
this.$toastContainer = this.$container.find(".g3-toast-container");
|
|
}
|
|
|
|
const toastId = "toast_" + G3FileUtils.generateUniqueId();
|
|
const toastType = this.language.toast[type] || type;
|
|
const icon = this.toastIcons[type] || this.toastIcons.info;
|
|
|
|
const toastHtml = `
|
|
<div class="g3-toast g3-toast-${type}" id="${toastId}">
|
|
<div class="g3-toast-icon">${icon}</div>
|
|
<div class="g3-toast-content">
|
|
<div class="g3-toast-title">${toastType}</div>
|
|
${message ? `<div class="g3-toast-message">${message}</div>` : ""}
|
|
</div>
|
|
<button class="g3-toast-close">×</button>
|
|
</div>
|
|
`;
|
|
|
|
this.$toastContainer.append(toastHtml);
|
|
const $toast = this.$container.find(`#${toastId}`);
|
|
|
|
$toast
|
|
.find(".g3-toast-close")
|
|
.off("click")
|
|
.on("click", function () {
|
|
self.removeToast(toastId);
|
|
});
|
|
|
|
setTimeout(function () {
|
|
self.removeToast(toastId);
|
|
}, duration);
|
|
|
|
const maxToasts = 5;
|
|
const toasts = this.$toastContainer.find(".g3-toast");
|
|
if (toasts.length > maxToasts) {
|
|
toasts.first().remove();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 移除 Toast
|
|
* @param {string} toastId - Toast ID
|
|
*/
|
|
removeToast: function (toastId) {
|
|
const $toast = this.$container.find(`#${toastId}`);
|
|
if ($toast.length) {
|
|
$toast.addClass("closing");
|
|
setTimeout(function () {
|
|
$toast.remove();
|
|
}, 300);
|
|
}
|
|
},
|
|
|
|
// ============================================================================
|
|
// Task Center 任务中心系统
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 创建并启动任务
|
|
* @param {Object} options - 任务配置
|
|
* @param {string} options.id - 任务ID
|
|
* @param {string} options.title - 任务标题(国际化key或文本)
|
|
* @param {number} options.total - 总数量
|
|
* @param {number} options.current - 当前进度
|
|
* @param {string} options.currentItem - 当前处理项
|
|
*/
|
|
startTask: function (options) {
|
|
var task = {
|
|
id: options.id || G3FileUtils.generateUniqueId(),
|
|
title: options.title,
|
|
total: options.total || 0,
|
|
current: options.current || 0,
|
|
currentItem: options.currentItem || "",
|
|
success: 0,
|
|
failed: 0,
|
|
status: "running",
|
|
};
|
|
this.tasks.set(task.id, task);
|
|
this.lockUI();
|
|
this.renderTaskCenter(task.id);
|
|
return task.id;
|
|
},
|
|
|
|
/**
|
|
* 更新任务进度
|
|
* @param {Object} options - 更新配置
|
|
* @param {string} options.id - 任务ID
|
|
* @param {number} options.current - 当前进度
|
|
* @param {string} options.currentItem - 当前处理项
|
|
*/
|
|
updateTask: function (options) {
|
|
var task = this.tasks.get(options.id);
|
|
if (!task) return;
|
|
if (options.current !== undefined) task.current = options.current;
|
|
if (options.currentItem !== undefined)
|
|
task.currentItem = options.currentItem;
|
|
this.updateTaskCenter(options.id);
|
|
},
|
|
|
|
/**
|
|
* 完成任务
|
|
* @param {Object} options - 完成配置
|
|
* @param {string} options.id - 任务ID
|
|
* @param {number} options.total - 总数量
|
|
* @param {number} options.success - 成功数量
|
|
* @param {number} options.failed - 失败数量
|
|
*/
|
|
finishTask: function (options) {
|
|
var task = this.tasks.get(options.id);
|
|
if (!task) return;
|
|
task.status = "finished";
|
|
task.total = options.total !== undefined ? options.total : task.total;
|
|
task.success = options.success || 0;
|
|
task.failed = options.failed || 0;
|
|
task.current = task.total;
|
|
this.updateTaskCenter(options.id);
|
|
this.tasks.delete(options.id);
|
|
if (this.tasks.size === 0) {
|
|
this.unlockUI();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 手动关闭任务 UI
|
|
* @param {string} id - 任务ID
|
|
*/
|
|
closeTask: function (id) {
|
|
var task = this.tasks.get(id);
|
|
if (task) {
|
|
task.status = "closed";
|
|
this.tasks.delete(id);
|
|
}
|
|
|
|
var taskCenter = this.$container.find(
|
|
`.g3-task-center[data-task-id="${id}"]`,
|
|
);
|
|
if (taskCenter.length) {
|
|
taskCenter.remove();
|
|
}
|
|
this.$taskCenter = null;
|
|
|
|
if (this.tasks.size === 0) {
|
|
this.unlockUI();
|
|
}
|
|
},
|
|
|
|
renderTaskCenter: function (taskId) {
|
|
var task = this.tasks.get(taskId);
|
|
if (!task) return;
|
|
|
|
let taskCenter = this.$container.find(".g3-task-center");
|
|
if (!taskCenter.length) {
|
|
this.$container.append(`
|
|
<div class="g3-task-center" data-task-id="${taskId}">
|
|
<div class="g3-task-header">
|
|
<span class="g3-task-title">${task.title}</span>
|
|
<button class="g3-task-close">×</button>
|
|
</div>
|
|
<div class="g3-task-body">
|
|
<div class="g3-current-item"></div>
|
|
<div class="g3-progress-bar">
|
|
<div class="g3-progress-inner"></div>
|
|
</div>
|
|
<div class="g3-progress-text"></div>
|
|
<div class="g3-result-section" style="display: none;">
|
|
<div class="g3-result-item">
|
|
<div class="g3-result-value"></div>
|
|
<div class="g3-result-label">${this.language.task.total}</div>
|
|
</div>
|
|
<div class="g3-result-item g3-result-success">
|
|
<div class="g3-result-value"></div>
|
|
<div class="g3-result-label">${this.language.task.success}</div>
|
|
</div>
|
|
<div class="g3-result-item g3-result-failed">
|
|
<div class="g3-result-value"></div>
|
|
<div class="g3-result-label">${this.language.task.failed}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`);
|
|
taskCenter = this.$container.find(".g3-task-center");
|
|
}
|
|
|
|
this.$taskCenter = taskCenter;
|
|
this.$taskCenter.addClass("is-visible");
|
|
this.updateTaskCenter(taskId);
|
|
},
|
|
|
|
updateTaskCenter: function (taskId) {
|
|
var task = this.tasks.get(taskId);
|
|
if (!task || !this.$taskCenter) return;
|
|
|
|
const progress =
|
|
task.total > 0 ? ((task.current / task.total) * 100).toFixed(0) : 0;
|
|
|
|
this.$taskCenter.find(".g3-task-title").text(task.title);
|
|
this.$taskCenter
|
|
.find(".g3-current-item")
|
|
.text(
|
|
task.currentItem
|
|
? `${this.language.task.currentItem}: ${task.currentItem}`
|
|
: "",
|
|
);
|
|
this.$taskCenter.find(".g3-progress-inner").css("width", `${progress}%`);
|
|
this.$taskCenter
|
|
.find(".g3-progress-text")
|
|
.text(`${task.current} / ${task.total}`);
|
|
|
|
if (task.status === "finished") {
|
|
this.$taskCenter.find(".g3-result-section").show();
|
|
this.$taskCenter
|
|
.find(".g3-result-item:first .g3-result-value")
|
|
.text(task.total);
|
|
this.$taskCenter
|
|
.find(".g3-result-success .g3-result-value")
|
|
.text(task.success);
|
|
this.$taskCenter
|
|
.find(".g3-result-failed .g3-result-value")
|
|
.text(task.failed);
|
|
} else {
|
|
this.$taskCenter.find(".g3-result-section").hide();
|
|
}
|
|
},
|
|
|
|
// ============================================================================
|
|
// UI Lock 机制
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 锁定 UI
|
|
*/
|
|
lockUI: function () {
|
|
this.uiLocked = true;
|
|
|
|
let uiLock = this.$container.find(".g3-ui-lock");
|
|
if (!uiLock.length) {
|
|
this.$container.append('<div class="g3-ui-lock"></div>');
|
|
uiLock = this.$container.find(".g3-ui-lock");
|
|
}
|
|
|
|
this.$uiLock = uiLock;
|
|
this.$uiLock.addClass("is-locked");
|
|
},
|
|
|
|
/**
|
|
* 解锁 UI
|
|
*/
|
|
unlockUI: function () {
|
|
this.uiLocked = false;
|
|
this.$uiLock?.removeClass("is-locked");
|
|
},
|
|
|
|
// ============================================================================
|
|
// 操作菜单相关方法
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 关闭操作菜单
|
|
*/
|
|
closeActionMenu: function () {
|
|
this.actionMenuState.open = false;
|
|
this.actionMenuState.fileId = null;
|
|
this.actionMenuState.anchorEl = null;
|
|
this.actionMenuState.items = [];
|
|
|
|
this.renderActionMenu();
|
|
},
|
|
|
|
/**
|
|
* 打开操作菜单
|
|
* @param {jQuery} $el - 触发元素
|
|
* @param {Object} file - 文件对象
|
|
*/
|
|
openActionMenu: function ($el, file) {
|
|
var items =
|
|
(this.options.customActionMenu &&
|
|
this.options.customActionMenu(file)) ||
|
|
[];
|
|
|
|
this.actionMenuState = {
|
|
open: true,
|
|
fileId: file._id,
|
|
anchorEl: $el[0],
|
|
items: items,
|
|
};
|
|
|
|
this.renderActionMenu();
|
|
},
|
|
|
|
// ============================================================================
|
|
// 权限相关方法
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 权限判断(唯一入口)
|
|
* @param {Object} file - 文件对象
|
|
* @param {string} action - 操作类型
|
|
* @returns {boolean} 是否有权限
|
|
*/
|
|
hasPermission: function (file, action) {
|
|
return this.resolvePermission(file, action);
|
|
},
|
|
|
|
resolvePermission: function (file, action) {
|
|
var globalPermission = this.options.globalPermission;
|
|
|
|
if (typeof globalPermission === "function") {
|
|
return !!globalPermission({ file: file, action: action });
|
|
} else if (typeof globalPermission === "object") {
|
|
var permissionKey = action === "select" ? "upload" : action;
|
|
return !!globalPermission[permissionKey];
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* 更新权限配置
|
|
* @param {Object} permissions - 权限对象
|
|
*/
|
|
updatePermissions: function (permissions) {
|
|
if (!permissions) return;
|
|
|
|
this.options.globalPermission = permissions;
|
|
this.updateUI();
|
|
},
|
|
|
|
// ============================================================================
|
|
// 其他方法
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 根据ID获取文件
|
|
* @param {string} id - 文件ID
|
|
* @returns {Object} 文件对象
|
|
*/
|
|
getFileById: function (id) {
|
|
return this.files.find(function (f) {
|
|
return f._id === id;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 获取选中文件
|
|
* @returns {Array} 选中文件数组
|
|
*/
|
|
getSelectedFiles: function () {
|
|
return this.files.filter((file) => this.selectedFiles.has(file._id));
|
|
},
|
|
|
|
/**
|
|
* 获取所有文件
|
|
* @returns {Array} 文件数组
|
|
*/
|
|
getFiles: function () {
|
|
return this.files;
|
|
},
|
|
|
|
/**
|
|
* 设置文件
|
|
* @param {Array} files - 文件数组
|
|
* @returns {G3File} 实例
|
|
*/
|
|
setFiles: function (files) {
|
|
this.files = files.map(function (file) {
|
|
return {
|
|
_id: file._id || G3FileUtils.generateUniqueId(),
|
|
name: file.name,
|
|
size: file.size,
|
|
uploadDate: file.uploadDate || new Date().toISOString(),
|
|
url: file.url || "",
|
|
file: file.file || null,
|
|
status: file.status || "uploaded",
|
|
...file,
|
|
};
|
|
});
|
|
|
|
this.selectedFiles.clear();
|
|
this.currentPreviewFileId = null;
|
|
this.previewCache = null;
|
|
this.renderFileList();
|
|
this.updateUI();
|
|
this.updatePreview();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* 更新状态
|
|
* @param {Object} partialState - 部分状态
|
|
* @returns {G3File} 实例
|
|
*/
|
|
updateState: function (partialState) {
|
|
if (partialState.files) {
|
|
this.setFiles(partialState.files);
|
|
} else {
|
|
// 其他状态更新时关闭所有运行中的任务
|
|
var self = this;
|
|
this.tasks.forEach(function (task, taskId) {
|
|
self.closeTask(taskId);
|
|
});
|
|
}
|
|
|
|
if (partialState.selectedFiles) {
|
|
this.selectedFiles = new Set(partialState.selectedFiles);
|
|
this.updateUI();
|
|
}
|
|
|
|
if (
|
|
Object.prototype.hasOwnProperty.call(partialState, "globalPermission")
|
|
) {
|
|
this.updatePermissions(partialState.globalPermission);
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* 销毁
|
|
*/
|
|
destroy: function () {
|
|
// 清理 document 事件
|
|
$(document).off(this.documentNamespace);
|
|
$(document).off(".g3file_actionmenu_" + this.instanceId);
|
|
|
|
// 清理 ObjectURL
|
|
var self = this;
|
|
this.objectUrls.forEach(function (url) {
|
|
try {
|
|
URL.revokeObjectURL(url);
|
|
} catch (e) {
|
|
console.warn("释放ObjectURL失败:", e);
|
|
}
|
|
});
|
|
this.objectUrls.clear();
|
|
|
|
// 清理任务
|
|
this.tasks.forEach(function (task, taskId) {
|
|
self.closeTask(taskId);
|
|
});
|
|
|
|
// 清理 DOM
|
|
this.$container.find(".g3-global-menu").remove();
|
|
this.$container.find(".g3-toast-container").remove();
|
|
this.$container.find(".g3-task-center").remove();
|
|
this.$container.find(".g3-ui-lock").remove();
|
|
this.$container.empty();
|
|
|
|
// 重置状态
|
|
this.files = [];
|
|
this.selectedFiles.clear();
|
|
this.tasks.clear();
|
|
this.currentPreviewFileId = null;
|
|
this.uiLocked = false;
|
|
|
|
console.log("G3File 组件已销毁");
|
|
},
|
|
};
|
|
|
|
// ============================================================================
|
|
// jQuery 插件封装
|
|
// ============================================================================
|
|
$.fn.G3File = function (options) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
var instance;
|
|
var methodResult; // 存储方法调用结果
|
|
|
|
this.each(function () {
|
|
var $this = $(this);
|
|
instance = $this.data("G3File");
|
|
|
|
if (!instance) {
|
|
instance = new G3File(this, options);
|
|
$this.data("G3File", instance);
|
|
} else if (typeof options === "string") {
|
|
var method = options;
|
|
if (instance[method]) {
|
|
// 保存方法调用结果
|
|
methodResult = instance[method].apply(instance, args);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (typeof options === "string") {
|
|
return methodResult !== undefined ? methodResult : this;
|
|
}
|
|
|
|
return instance || this;
|
|
};
|
|
})(jQuery);
|