+
${this.staticIconMap.download}
-
+
${this.staticIconMap.more}
@@ -1271,30 +787,9 @@
`;
},
- closeActionMenu: function () {
- this.actionMenuState.open = false;
- this.actionMenuState.fileId = null;
- this.actionMenuState.anchorEl = null;
- this.actionMenuState.items = [];
-
- this.renderActionMenu();
- },
- openActionMenu: function ($el, file) {
- var items = (this.options.getActionMenu && this.options.getActionMenu(file)) || [];
-
- this.actionMenuState = {
- open: true,
- fileId: file.id,
- anchorEl: $el[0],
- items: items
- };
-
- this.renderActionMenu();
- },
-
- // =====================
- // 渲染文件列表
- // =====================
+ /**
+ * 渲染文件列表
+ */
renderFileList: function () {
if (this.files.length === 0) {
this.$fileList.hide();
@@ -1305,7 +800,7 @@
this.$fileList.show();
this.$emptyTip.hide();
- var html = '';
+ var html = "";
var self = this;
this.files.forEach(function (file) {
html += self.createFileItem(file);
@@ -1315,130 +810,1248 @@
this.bindFileItemEvents();
},
- // =====================
- // 更新UI
- // =====================
+ /**
+ * 更新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'));
+ 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 canBatchDownload =
+ this.hasPermission(null, "download") && hasSelectedFiles;
+ this.$btnDownload?.prop("disabled", !canBatchDownload);
// 批量删除按钮:需要全局权限且有选中文件
- var canBatchDelete = this.hasPermission(null, 'delete') && hasSelectedFiles;
- this.$btnDelete?.prop('disabled', !canBatchDelete);
+ 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 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;
+ var pendingCount = this.files.filter(
+ (file) => file.status === "pending",
+ ).length;
this.$pendingCount?.text(pendingCount);
// 更新文件项状态
- this.$fileList.find('.g3-file-item').each(function () {
+ 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);
+ 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);
+ $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');
+ var canDownload = self.hasPermission(file, "download");
+ var canView = self.hasPermission(file, "view");
+ var canDelete = self.hasPermission(file, "delete");
- $item.find('.g3-file-action.download')
- .toggleClass('is-disabled', !canDownload);
-
- $item.find('.g3-file-action.preview')
- .toggleClass('is-disabled', !canView);
-
- $item.find('.g3-file-delete-btn')
- .toggleClass('is-disabled', !canDelete);
+ $item
+ .find(".g3-file-action.download")
+ .toggleClass("is-disabled", !canDownload);
+ $item
+ .find(".g3-file-action.preview")
+ .toggleClass("is-disabled", !canView);
+ $item
+ .find(".g3-file-delete-btn")
+ .toggleClass("is-disabled", !canDelete);
});
},
- // =====================
- // 获取选中文件
- // =====================
- getSelectedFiles: function () {
- return this.files.filter(file => this.selectedFiles.has(file.id));
- },
+ // ============================================================================
+ // 事件绑定相关方法
+ // ============================================================================
- // =====================
- // 设置文件
- // =====================
- setFiles: function (files) {
+ /**
+ * 绑定事件
+ */
+ bindEvents: function () {
var self = this;
- this.files = files.map(function (file) {
- return {
- id: file.id || self.generateUniqueId(),
- name: file.name,
- size: file.size,
- uploadDate: file.uploadDate || new Date().toISOString(),
- url: file.url || '',
- file: file.file || null,
- status: file.status || 'uploaded'
- };
+ // 选择全部
+ 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.selectedFiles.clear();
- this.renderFileList();
- this.updateUI();
+ // 批量下载
+ this.$btnDownload.off("click").on("click", function () {
+ var selectedFiles = self.getSelectedFiles();
+ if (selectedFiles.length === 0) return;
+ self.handleDownload(selectedFiles, { batch: true });
+ });
+ // 批量删除
+ this.$btnDelete.off("click").on("click", function () {
+ var selectedFiles = self.getSelectedFiles();
+ if (selectedFiles.length === 0) return;
- return this;
+ self.handleDelete(selectedFiles);
+ });
+
+ // 选择文件
+ this.$btnSelect.off("click").on("click", function () {
+ self.$fileInput.click();
+ });
+
+ // 上传
+ this.$btnUpload.off("click").on("click", function () {
+ 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("click.g3file_actionmenu")
+ .on("click.g3file_actionmenu", function (e) {
+ if (
+ !$(e.target).closest(".g3-file-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();
+ $container.addClass("drag-over");
+ })
+ .on("dragover", function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ $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");
+
+ var files = e.originalEvent.dataTransfer.files;
+ if (files && files.length > 0) {
+ self.handleFileSelect(files);
+ }
+ });
+
+ // 粘贴文件
+ $(document)
+ .off("paste.g3file")
+ .on("paste.g3file", function (e) {
+ var clipboardData = e.originalEvent.clipboardData;
+ var files = clipboardData.files;
+ if (files && files.length > 0) {
+ e.preventDefault();
+ self.handleFileSelect(files);
+ }
+ });
},
- // =====================
- // 更新状态
- // =====================
- updateState: function (partialState) {
- if (partialState.files) {
- this.setFiles(partialState.files);
- } else {
- // 其他状态更新时清理任务
- this.clearTasks();
+ /**
+ * 绑定文件项事件
+ */
+ bindFileItemEvents: function () {
+ var self = this;
+
+ // 预览
+ this.$fileList
+ .off("click", ".preview")
+ .on("click", ".preview", function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ var id = $(this).closest(".g3-file-item").data("id");
+ var file = self.files.find((f) => f.id === id);
+
+ if (!file) return;
+
+ if (typeof self.options.onPreview === "function") {
+ self.options.onPreview([file]);
+ }
+ });
+
+ // 下载
+ this.$fileList
+ .off("click", ".download")
+ .on("click", ".download", function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ var id = $(this).closest(".g3-file-item").data("id");
+ var file = self.files.find((f) => f.id === id);
+
+ if (!file) return;
+
+ self.handleDownload([file], { batch: false });
+ });
+
+ // 复选框
+ this.$fileList
+ .off("change", ".g3-file-checkbox")
+ .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();
+ });
+
+ // 删除
+ this.$fileList
+ .off("click", ".g3-file-delete-btn")
+ .on("click", ".g3-file-delete-btn", function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ var id = $(this).closest(".g3-file-item").data("id");
+ var file = self.files.find((f) => f.id === id);
+
+ if (!file) return;
+
+ self.handleDelete([file]);
+ });
+
+ // 文件项点击
+ this.$fileList
+ .off("click", ".g3-file-item")
+ .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.files.find((f) => f.id === id);
+
+ if (!file) return;
+
+ if (typeof self.options.onPreview === "function") {
+ self.options.onPreview([file]);
+ }
+ });
+
+ // 更多按钮
+ this.$fileList.off("click", ".more").on("click", ".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);
+ });
+ },
+
+ /**
+ * 绑定任务面板事件
+ */
+ bindTaskDockEvents: function () {
+ const self = this;
+
+ // 绑定关闭按钮事件
+ this.$taskDock
+ .find('.dock-actions button[title="close"]')
+ .off("click")
+ .on("click", function () {
+ self.clearTasks();
+ });
+ },
+
+ // ============================================================================
+ // 文件处理相关方法
+ // ============================================================================
+
+ /**
+ * 处理文件选择
+ * @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 (partialState.selectedFiles) {
- this.selectedFiles = new Set(partialState.selectedFiles);
+ 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 (self.isImageFile(file.name)) {
+ if (typeof self.options.compressImage === "function") {
+ try {
+ compressedFile = await self.options.compressImage(file);
+ } catch (e) {
+ console.warn("外部压缩算法执行失败,使用内部压缩:", e);
+ compressedFile = await self.compressImage(file);
+ }
+ } else {
+ compressedFile = await self.compressImage(file);
+ }
+
+ url = URL.createObjectURL(compressedFile);
+ self.objectUrls.set(self.generateUniqueId(), url);
+ }
+
+ tempFiles.push({
+ id: self.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 (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.showNotification({
+ title: this.language.error || "错误",
+ message:
+ this.language.downloadError || "下载失败,请检查文件是否有效",
+ type: "error",
+ duration: 5000,
+ });
+ 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.showNotification({
+ title: this.language.error || "错误",
+ message:
+ this.language.downloadError || "下载失败,请检查文件是否有效",
+ type: "error",
+ duration: 5000,
+ });
+ return;
+ }
+
+ saveAs(blob, file.name || "未命名文件");
+ } catch (err) {
+ console.error("文件下载失败:", err);
+ this.showNotification({
+ title: this.language.error || "错误",
+ message:
+ this.language.downloadError || "下载失败,请检查文件是否有效",
+ type: "error",
+ duration: 5000,
+ });
+ }
+ },
+
+ /**
+ * 内部批量下载
+ * @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 failedCount = 0;
+
+ const concurrency = 3;
+
+ // 为每个文件创建任务
+ const taskIds = [];
+ files.forEach((file) => {
+ const taskId = this.addTask({
+ name: file.name,
+ type: "download",
+ progress: 0,
+ status: "running",
+ });
+ taskIds.push(taskId);
+ });
+
+ const downloadOne = async (item, index) => {
+ const taskId = taskIds[index];
+ try {
+ // 更新任务进度
+ this.updateTask(taskId, { progress: 0, status: "running" });
+
+ 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++;
+ this.finishTask(taskId, "fail");
+ return;
+ }
+
+ const uniqueFileName = this.getUniqueFileName(item.name, addedFiles);
+
+ zip.file(uniqueFileName, content);
+ addedFiles.add(uniqueFileName);
+
+ // 标记任务完成
+ this.finishTask(taskId, "success");
+ } catch (err) {
+ console.warn("跳过失败文件:", item.name, err);
+ failedCount++;
+ this.finishTask(taskId, "fail");
+ }
+ };
+
+ try {
+ // 使用副本,不修改原始数据
+ const filesCopy = [...files];
+ const runner = async () => {
+ while (filesCopy.length) {
+ const index = total - filesCopy.length;
+ const item = filesCopy.shift();
+ await downloadOne(item, index);
+ }
+ };
+
+ await Promise.all(Array.from({ length: concurrency }, runner));
+
+ 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");
+
+ const successCount = total - failedCount;
+ console.log(`下载完成:${successCount}/${total},失败:${failedCount}`);
+ } catch (err) {
+ console.error("批量下载失败:", err);
+ }
+ },
+
+ /**
+ * 内部删除
+ * @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.renderFileList();
this.updateUI();
}
+ },
- if (partialState.globalPermission || partialState.filePermissionResolver) {
- this.updatePermissions({
- globalPermission: partialState.globalPermission,
- filePermissionResolver: partialState.filePermissionResolver
+ /**
+ * 内部上传方法,支持阿里云上传
+ * @param {Object} file - 文件对象
+ * @param {Function} callback - 上传完成回调
+ */
+ internalUpload: function (file, callback) {
+ const self = this;
+ const aliyunConfig = this.options.uploadConfig.aliyun;
+
+ if (!aliyunConfig || !aliyunConfig.getUploadToken) {
+ callback(new Error("阿里云上传配置不完整,缺少 getUploadToken 函数"));
+ return;
+ }
+
+ // 获取上传凭证
+ aliyunConfig
+ .getUploadToken(file)
+ .then((tokenData) => {
+ // 这里是模拟上传过程,实际项目中需要使用阿里云 SDK 进行上传
+ // 模拟上传延迟
+ setTimeout(() => {
+ // 模拟上传成功
+ const uploadedFile = {
+ ...file,
+ status: "uploaded",
+ url: tokenData.uploadUrl || `http://example.com/${file.name}`,
+ uploadDate: new Date().toISOString(),
+ };
+
+ // 更新文件状态
+ self.updateFileStatus(file.id, "uploaded", {
+ url: uploadedFile.url,
+ uploadDate: uploadedFile.uploadDate,
+ });
+
+ // 触发上传成功回调
+ if (typeof self.options.onUploadSuccess === "function") {
+ self.options.onUploadSuccess(uploadedFile);
+ }
+
+ callback(null, uploadedFile);
+ }, 1000);
+ })
+ .catch((error) => {
+ // 更新文件状态为失败
+ self.updateFileStatus(file.id, "failed");
+
+ // 触发上传失败回调
+ if (typeof self.options.onUploadError === "function") {
+ self.options.onUploadError(file, error);
+ }
+
+ callback(error);
+ });
+ },
+
+ /**
+ * 处理上传逻辑
+ * @param {Array} files - 文件数组
+ */
+ handleUpload: function (files) {
+ const self = this;
+ const customUpload = this.options.uploadConfig.customUpload;
+ let successCount = 0;
+ let failCount = 0;
+
+ // 过滤出未上传的文件
+ const pendingFiles = files.filter((file) => file.status === "pending");
+
+ if (pendingFiles.length === 0) {
+ // 触发上传完成回调
+ if (typeof self.options.onUploadComplete === "function") {
+ self.options.onUploadComplete(0, 0);
+ }
+ return;
+ }
+
+ // 逐个上传文件
+ pendingFiles.forEach((file) => {
+ // 更新文件状态为上传中
+ self.updateFileStatus(file.id, "uploading");
+
+ if (typeof customUpload === "function") {
+ // 使用自定义上传方法
+ customUpload(file)
+ .then((uploadedFile) => {
+ // 更新文件状态
+ self.updateFileStatus(file.id, "uploaded", uploadedFile);
+
+ // 触发上传成功回调
+ if (typeof self.options.onUploadSuccess === "function") {
+ self.options.onUploadSuccess(uploadedFile);
+ }
+
+ successCount++;
+ checkComplete();
+ })
+ .catch((error) => {
+ // 更新文件状态为失败
+ self.updateFileStatus(file.id, "failed");
+
+ // 触发上传失败回调
+ if (typeof self.options.onUploadError === "function") {
+ self.options.onUploadError(file, error);
+ }
+
+ failCount++;
+ checkComplete();
+ });
+ } else {
+ // 使用阿里云内部上传方法
+ self.internalUpload(file, (error, uploadedFile) => {
+ if (error) {
+ failCount++;
+ } else {
+ successCount++;
+ }
+ checkComplete();
+ });
+ }
+ });
+
+ // 检查是否所有文件都上传完成
+ function checkComplete() {
+ if (successCount + failCount === pendingFiles.length) {
+ // 触发上传完成回调
+ if (typeof self.options.onUploadComplete === "function") {
+ self.options.onUploadComplete(successCount, failCount);
+ }
+ }
+ }
+ },
+
+ // ============================================================================
+ // 任务管理相关方法
+ // ============================================================================
+
+ /**
+ * 添加任务
+ * @param {Object} task - 任务对象
+ * @returns {string} 任务ID
+ */
+ addTask: function (task) {
+ const newTask = {
+ id: task.id || this.generateUniqueId(),
+ name: task.name,
+ type: task.type || "custom",
+ progress: task.progress || 0,
+ status: task.status || "running",
+ };
+
+ this.tasks.push(newTask);
+ this.updateTaskStats();
+ this.showTaskDock();
+
+ // 只在批量下载时锁定UI
+ if (task.type === "download" || task.type === "upload") {
+ this.lockUI();
+ }
+
+ return newTask.id;
+ },
+
+ /**
+ * 更新任务
+ * @param {string} taskId - 任务ID
+ * @param {Object} updates - 更新内容
+ */
+ updateTask: function (taskId, updates) {
+ const task = this.tasks.find((t) => t.id === taskId);
+ if (task) {
+ Object.assign(task, updates);
+ this.updateTaskDock();
+ }
+ },
+
+ /**
+ * 完成任务
+ * @param {string} taskId - 任务ID
+ * @param {string} status - 任务状态
+ */
+ finishTask: function (taskId, status) {
+ const task = this.tasks.find((t) => t.id === taskId);
+ if (task) {
+ task.status = status || "success";
+ task.progress = 100;
+ this.updateTaskStats();
+ this.updateTaskDock();
+
+ const allCompleted = this.tasks.every(
+ (t) => t.status === "success" || t.status === "fail",
+ );
+ if (allCompleted) {
+ this.unlockUI();
+ }
+ }
+ },
+
+ /**
+ * 更新任务统计
+ */
+ updateTaskStats: function () {
+ this.taskStats.total = this.tasks.length;
+ this.taskStats.success = this.tasks.filter(
+ (t) => t.status === "success",
+ ).length;
+ this.taskStats.fail = this.tasks.filter(
+ (t) => t.status === "fail",
+ ).length;
+ },
+
+ /**
+ * 清空任务
+ */
+ clearTasks: function () {
+ this.tasks = [];
+ this.taskStats = {
+ total: 0,
+ success: 0,
+ fail: 0,
+ };
+ this.hideTaskDock();
+ this.unlockUI();
+ },
+
+ // ============================================================================
+ // 通知相关方法
+ // ============================================================================
+
+ /**
+ * 显示通知
+ * @param {Object} options - 通知配置
+ * @returns {string} 通知ID
+ */
+ showNotification: function (options) {
+ const defaultOptions = {
+ title: "",
+ message: "",
+ type: "info", // success, warning, error, info
+ duration: 3000,
+ showClose: true,
+ customClass: "",
+ offset: 20,
+ position: "top-right", // top-right, top-left, bottom-right, bottom-left
+ };
+
+ const config = $.extend({}, defaultOptions, options);
+ const notificationId = this.generateUniqueId();
+
+ // 创建通知元素
+ const notificationEl = $(
+ `
+
+ ${config.title ? `
${config.title}
` : ""}
+
${config.message}
+
+ ${config.showClose ? '
' : ""}
+
`,
+ );
+
+ // 添加到容器
+ this.$notificationContainer.append(notificationEl);
+
+ // 计算位置
+ this._positionNotification(notificationEl, config);
+
+ // 绑定关闭事件
+ if (config.showClose) {
+ notificationEl.find(".notification-close").on("click", () => {
+ this.closeNotification(notificationId);
});
}
- return this;
+ // 自动关闭
+ if (config.duration > 0) {
+ setTimeout(() => {
+ this.closeNotification(notificationId);
+ }, config.duration);
+ }
+
+ // 存储通知
+ this.notifications.push({
+ id: notificationId,
+ element: notificationEl,
+ config: config,
+ });
+
+ return notificationId;
},
- // =====================
- // 权限判断(唯一入口)
- // =====================
+ /**
+ * 关闭通知
+ * @param {string} notificationId - 通知ID
+ */
+ closeNotification: function (notificationId) {
+ const index = this.notifications.findIndex(
+ (n) => n.id === notificationId,
+ );
+ if (index !== -1) {
+ const notification = this.notifications[index];
+ notification.element.addClass("notification-fade-out");
+ setTimeout(() => {
+ notification.element.remove();
+ this.notifications.splice(index, 1);
+ // 重新计算其他通知位置
+ this._repositionNotifications();
+ }, 300);
+ }
+ },
+
+ /**
+ * 关闭所有通知
+ */
+ closeAllNotifications: function () {
+ this.notifications.forEach((notification) => {
+ notification.element.addClass("notification-fade-out");
+ setTimeout(() => {
+ notification.element.remove();
+ }, 300);
+ });
+ this.notifications = [];
+ },
+
+ /**
+ * 定位通知
+ * @param {jQuery} element - 通知元素
+ * @param {Object} config - 配置
+ */
+ _positionNotification: function (element, config) {
+ const position = config.position || "top-right";
+ const offset = config.offset || 20;
+
+ // 计算当前位置
+ let top, left, right, bottom;
+ const containerRect =
+ this.$notificationContainer[0].getBoundingClientRect();
+
+ // 计算同位置的通知数量
+ const samePositionNotifications = this.notifications.filter(
+ (n) => n.config.position === position,
+ );
+
+ const notificationHeight = 80; // 估计通知高度
+ const spacing = 12; // 通知间距
+
+ switch (position) {
+ case "top-right":
+ top =
+ offset +
+ samePositionNotifications.length * (notificationHeight + spacing);
+ right = offset;
+ break;
+ case "top-left":
+ top =
+ offset +
+ samePositionNotifications.length * (notificationHeight + spacing);
+ left = offset;
+ break;
+ case "bottom-right":
+ bottom =
+ offset +
+ samePositionNotifications.length * (notificationHeight + spacing);
+ right = offset;
+ break;
+ case "bottom-left":
+ bottom =
+ offset +
+ samePositionNotifications.length * (notificationHeight + spacing);
+ left = offset;
+ break;
+ }
+
+ element.css({
+ top: top,
+ left: left,
+ right: right,
+ bottom: bottom,
+ });
+ },
+
+ /**
+ * 重新定位通知
+ */
+ _repositionNotifications: function () {
+ // 按位置分组
+ const notificationsByPosition = {};
+ this.notifications.forEach((notification) => {
+ const position = notification.config.position || "top-right";
+ if (!notificationsByPosition[position]) {
+ notificationsByPosition[position] = [];
+ }
+ notificationsByPosition[position].push(notification);
+ });
+
+ // 重新定位每个位置的通知
+ Object.keys(notificationsByPosition).forEach((position) => {
+ const notifications = notificationsByPosition[position];
+ const offset = notifications[0].config.offset || 20;
+ const notificationHeight = 80;
+ const spacing = 12;
+
+ notifications.forEach((notification, index) => {
+ let top, left, right, bottom;
+
+ switch (position) {
+ case "top-right":
+ top = offset + index * (notificationHeight + spacing);
+ right = offset;
+ break;
+ case "top-left":
+ top = offset + index * (notificationHeight + spacing);
+ left = offset;
+ break;
+ case "bottom-right":
+ bottom = offset + index * (notificationHeight + spacing);
+ right = offset;
+ break;
+ case "bottom-left":
+ bottom = offset + index * (notificationHeight + spacing);
+ left = offset;
+ break;
+ }
+
+ notification.element.css({
+ top: top,
+ left: left,
+ right: right,
+ bottom: bottom,
+ });
+ });
+ });
+ },
+
+ /**
+ * 测试通知功能
+ */
+ testNotifications: function () {
+ // 测试不同类型的通知
+ this.showNotification({
+ title: "成功",
+ message: "操作成功完成",
+ type: "success",
+ duration: 5000,
+ });
+
+ this.showNotification({
+ title: "警告",
+ message: "操作可能会影响其他功能",
+ type: "warning",
+ duration: 5000,
+ });
+
+ this.showNotification({
+ title: "错误",
+ message: "操作失败,请重试",
+ type: "error",
+ duration: 5000,
+ });
+
+ this.showNotification({
+ title: "信息",
+ message: "这是一条信息通知",
+ type: "info",
+ duration: 5000,
+ });
+ },
+
+ /**
+ * 显示任务面板
+ */
+ showTaskDock: function () {
+ if (this.$taskDock) {
+ this.$taskDock.show();
+ this.bindTaskDockEvents();
+ }
+ },
+
+ /**
+ * 隐藏任务面板
+ */
+ hideTaskDock: function () {
+ if (this.$taskDock) {
+ this.$taskDock.hide();
+ }
+ },
+
+ /**
+ * 更新任务面板
+ */
+ updateTaskDock: function () {
+ if (!this.$taskDock || this.tasks.length === 0) return;
+
+ // 获取当前正在运行的任务,或者最后一个任务
+ const currentTask =
+ this.tasks.find((t) => t.status === "running") ||
+ this.tasks[this.tasks.length - 1];
+
+ if (currentTask) {
+ const totalProgress =
+ this.tasks.reduce((sum, task) => sum + task.progress, 0) /
+ this.tasks.length;
+
+ const taskTitle =
+ currentTask.type === "upload"
+ ? this.language.batchUpload
+ : this.language.batchDownload;
+ const taskTotal = this.language.taskTotal;
+ const taskSuccess = this.language.taskSuccess;
+ const taskFail = this.language.taskFail;
+
+ this.$taskDock.find(".dock-title").text(taskTitle);
+ this.$taskDock
+ .find(".progress-line .count")
+ .text(
+ `${this.taskStats.success + this.taskStats.fail} / ${this.taskStats.total}`,
+ );
+ this.$taskDock
+ .find(".progress-line .percent")
+ .text(`${Math.round(totalProgress)}%`);
+ this.$taskDock.find(".task-name").text(currentTask.name);
+ this.$taskDock
+ .find(".bar .bar-inner")
+ .css("width", `${currentTask.progress}%`);
+ this.$taskDock
+ .find(".task-result")
+ .text(
+ `${taskTotal}: ${this.taskStats.total} | ${taskSuccess}: ${this.taskStats.success} | ${taskFail}: ${this.taskStats.fail}`,
+ );
+ }
+ },
+
+ // ============================================================================
+ // UI Lock 功能
+ // ============================================================================
+
+ /**
+ * 锁定UI
+ */
+ lockUI: function () {
+ if (this.$uiLock) {
+ this.$uiLock.show();
+ }
+ },
+
+ /**
+ * 解锁UI
+ */
+ unlockUI: function () {
+ if (this.$uiLock) {
+ this.$uiLock.hide();
+ }
+ },
+
+ /**
+ * 检查UI是否被锁定
+ * @returns {boolean} 是否被锁定
+ */
+ isUILocked: function () {
+ return this.$uiLock && this.$uiLock.is(":visible");
+ },
+
+ // ============================================================================
+ // 操作菜单相关方法
+ // ============================================================================
+
+ /**
+ * 关闭操作菜单
+ */
+ 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.getActionMenu && this.options.getActionMenu(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) {
var globalPermission = this.options.globalPermission;
var filePermissionResolver = this.options.filePermissionResolver;
// 1. 优先判断单文件权限解析函数
- if (typeof filePermissionResolver === 'function') {
+ if (typeof filePermissionResolver === "function") {
var fileResult = filePermissionResolver({ file: file, action: action });
if (fileResult !== undefined) {
return !!fileResult;
@@ -1446,9 +2059,9 @@
}
// 2. 判断全局权限
- if (typeof globalPermission === 'function') {
+ if (typeof globalPermission === "function") {
return !!globalPermission({ file: file, action: action });
- } else if (typeof globalPermission === 'object') {
+ } else if (typeof globalPermission === "object") {
return !!globalPermission[action];
}
@@ -1456,9 +2069,10 @@
return true;
},
- // =====================
- // 更新权限配置
- // =====================
+ /**
+ * 更新权限配置
+ * @param {Object} config - 权限配置
+ */
updatePermissions: function (config) {
if (!config) return;
@@ -1473,9 +2087,90 @@
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));
+ },
+
+ /**
+ * 设置文件
+ * @param {Array} files - 文件数组
+ * @returns {G3File} 实例
+ */
+ setFiles: function (files) {
+ var self = this;
+
+ this.files = files.map(function (file) {
+ return {
+ id: file.id || self.generateUniqueId(),
+ name: file.name,
+ size: file.size,
+ uploadDate: file.uploadDate || new Date().toISOString(),
+ url: file.url || "",
+ file: file.file || null,
+ status: file.status || "uploaded",
+ };
+ });
+
+ this.selectedFiles.clear();
+ this.renderFileList();
+ this.updateUI();
+
+ return this;
+ },
+
+ /**
+ * 更新状态
+ * @param {Object} partialState - 部分状态
+ * @returns {G3File} 实例
+ */
+ updateState: function (partialState) {
+ if (partialState.files) {
+ this.setFiles(partialState.files);
+ } else {
+ // 其他状态更新时清理任务
+ this.clearTasks();
+ }
+
+ if (partialState.selectedFiles) {
+ this.selectedFiles = new Set(partialState.selectedFiles);
+ this.updateUI();
+ }
+
+ if (
+ partialState.globalPermission ||
+ partialState.filePermissionResolver
+ ) {
+ this.updatePermissions({
+ globalPermission: partialState.globalPermission,
+ filePermissionResolver: partialState.filePermissionResolver,
+ });
+ }
+
+ return this;
+ },
+
+ /**
+ * 销毁
+ */
destroy: function () {
this.$container.off();
@@ -1484,7 +2179,7 @@
try {
URL.revokeObjectURL(url);
} catch (e) {
- console.warn('释放ObjectURL失败:', e);
+ console.warn("释放ObjectURL失败:", e);
}
});
this.objectUrls.clear();
@@ -1494,13 +2189,13 @@
this.selectedFiles.clear();
this.$container.empty();
- console.log('G3File 组件已销毁');
- }
+ console.log("G3File 组件已销毁");
+ },
};
- // ==========================
+ // ============================================================================
// jQuery 插件封装
- // ==========================
+ // ============================================================================
$.fn.G3File = function (options) {
var args = Array.prototype.slice.call(arguments, 1);
var instance;
@@ -1512,8 +2207,8 @@
if (!instance) {
instance = new G3File(this, options);
- $this.data('G3File', instance);
- } else if (typeof options === 'string') {
+ $this.data("G3File", instance);
+ } else if (typeof options === "string") {
var method = options;
if (instance[method]) {
// 保存方法调用结果
diff --git a/code/file-preview/index.html b/code/file-preview/index.html
index cd5416a9..217ab83d 100644
--- a/code/file-preview/index.html
+++ b/code/file-preview/index.html
@@ -64,137 +64,230 @@