Files
workspace/code/file-preview/custom-file/index.js
T
2026-04-27 22:11:59 +08:00

2223 lines
66 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function ($) {
"use strict";
// ============================================================================
// G3File 构造函数
// ============================================================================
function G3File(container, options) {
// 实例隔离
this.$container = $(container);
this.options = $.extend({}, this.defaults, options);
this.instanceId = this.generateUniqueId();
this.files = [];
this.selectedFiles = new Set();
this.objectUrls = new Map();
this.language =
this.languages[this.options.language] || this.languages["zh-CN"];
// 操作菜单状态
this.actionMenuState = {
open: false,
fileId: null,
anchorEl: null,
items: [],
};
// 任务管理
this.tasks = [];
this.taskStats = {
total: 0,
success: 0,
fail: 0,
};
this.init();
}
// ============================================================================
// 原型方法
// ============================================================================
G3File.prototype = {
// ============================================================================
// 配置和常量
// ============================================================================
/**
* 默认配置
*/
defaults: {
data: [],
onDelete: null,
onDownload: null,
onSelect: null,
onUpload: null,
onUploadSuccess: null,
onUploadError: null,
onUploadComplete: null,
onPreview: null,
getActionMenu: null,
language: "zh-CN",
maxFileSize: 10 * 1024 * 1024,
// 上传配置
uploadConfig: {
aliyun: {
// 阿里云配置是可选的
// getUploadToken 是必须的,用于动态获取上传凭证
getUploadToken: null, // 动态获取上传凭证的函数,返回 Promise
},
customUpload: null, // 自定义上传方法
},
// 全局权限(支持对象或函数)
globalPermission: {
select: true,
upload: true,
download: true,
delete: true,
view: true,
},
// 单文件权限解析函数(仅支持函数)
filePermissionResolver: null,
},
/**
* 多语言配置
*/
languages: {
"zh-CN": {
selectAll: "全选",
download: "下载",
downloadBatch: "批量下载",
delete: "删除",
upload: "上传",
select: "选择",
more: "更多",
confirmDelete: "确定要删除选中的 {count} 个文件吗?",
downloading: "准备下载 {count} 个文件",
deleting: "删除中...",
name: "名称",
size: "大小",
date: "日期",
pending: "未上传",
selectFiles: "选择文件",
emptyTip1: "拖拽文件到这里 …",
emptyTip2: "支持多文件同时上传/下载",
emptyTip3: "支持粘贴图片上传",
fileSizeError: "文件大小超过限制",
preview: "预览",
downloadingStatus: "下载中:",
downloadingFile: "正在下载:",
downloadComplete: "下载完成",
downloadResult: "共{total}文件,{success}成功,{failed}失败",
close: "关闭",
taskTotal: "总计",
taskSuccess: "成功",
taskFail: "失败",
batchDownload: "批量下载",
batchUpload: "批量上传",
error: "错误",
downloadError: "下载失败,请检查文件是否有效",
},
"en-US": {
selectAll: "Select All",
download: "Download",
downloadBatch: "Batch Download",
delete: "Delete",
upload: "Upload",
select: "Select",
more: "More",
confirmDelete:
"Are you sure you want to delete {count} selected file(s)?",
downloading: "Preparing to download {count} file(s)",
deleting: "Deleting...",
name: "Name",
size: "Size",
date: "Date",
pending: "Pending upload",
selectFiles: "Select Files",
emptyTip1: "Drag files here ...",
emptyTip2: "Supports multiple file upload/download",
emptyTip3: "Supports pasting images to upload",
fileSizeError: "File size exceeds limit",
preview: "Preview",
downloadingStatus: "Downloading:",
downloadingFile: "Downloading:",
downloadComplete: "Download complete",
downloadResult:
"Total {total} files, {success} succeeded, {failed} failed",
close: "Close",
taskTotal: "Total",
taskSuccess: "Success",
taskFail: "Fail",
batchDownload: "Batch Download",
batchUpload: "Batch Upload",
error: "Error",
downloadError: "Download failed, please check if the file is valid",
},
},
/**
* 静态图标映射
*/
staticIconMap: {
download: `
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M12 15V3m9 12v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10l5 5l5-5"/></g></svg>
`,
delete: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 6L6 18M6 6l12 12"/></svg>
`,
select: `
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12.586 12.586L19 19M3.688 3.037a.497.497 0 0 0-.651.651l6.5 15.999a.501.501 0 0 0 .947-.062l1.569-6.083a2 2 0 0 1 1.448-1.479l6.124-1.579a.5.5 0 0 0 .063-.947z"/></svg>
`,
upload: `
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v12m5-7l-5-5l-5 5m14 7v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg>
`,
preview: `
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="m21 21l-4.34-4.34"/><circle cx="11" cy="11" r="8"/></g></svg> `,
more: `
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M17 12h.01M12 12h.01M7 12h.01"/></g></svg>
`,
},
// ============================================================================
// 初始化和核心方法
// ============================================================================
/**
* 加载脚本
* @param {Array} items - 脚本配置数组
* @param {Function} callback - 加载完成回调
*/
loadScripts: function (items, callback) {
var remaining = items.length;
function done() {
remaining--;
if (remaining === 0) {
callback && callback();
}
}
items.forEach(function (item) {
// 已加载
if (item.check && item.check()) {
done();
return;
}
// 防重复加载
var key = item.key;
if (window[key + "_loading"]) {
var timer = setInterval(function () {
if (item.check && item.check()) {
clearInterval(timer);
done();
}
}, 50);
return;
}
window[key + "_loading"] = true;
var script = document.createElement("script");
script.src = item.src;
script.onload = function () {
window[key + "_loading"] = false;
done();
};
script.onerror = function () {
window[key + "_loading"] = false;
console.error(key + " load failed");
done();
};
document.head.appendChild(script);
});
},
/**
* 初始化
* @returns {G3File} 实例
*/
init: function () {
var self = this;
this.loadScripts(
[
{
key: "fileSaver",
src: "/custom-file/libs/FileSaver.min.js",
check: function () {
return typeof window.saveAs === "function";
},
},
{
key: "jsZip",
src: "/custom-file/libs/jszip.min.js",
check: function () {
return typeof window.JSZip === "function";
},
},
],
function () {
self.render();
self.bindEvents();
self.updateUI();
if (self.options.data && self.options.data.length) {
self.setFiles(self.options.data);
}
},
);
return this;
},
// ============================================================================
// 工具方法
// ============================================================================
/**
* 生成唯一ID
* @returns {string} 唯一ID
*/
generateUniqueId: function () {
return "file_" + Math.random().toString(36).slice(2) + Date.now();
},
/**
* 获取文件图标
* @param {string} filename - 文件名
* @returns {string} 图标HTML
*/
getFileIcon: function (filename) {
var ext = filename.split(".").pop().toLowerCase();
var iconMap = {
pdf: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="#909090" d="m24.1 2.072l5.564 5.8v22.056H8.879V30h20.856V7.945z"/><path fill="#f4f4f4" d="M24.031 2H8.808v27.928h20.856V7.873z"/><path fill="#7a7b7c" d="M8.655 3.5h-6.39v6.827h20.1V3.5z"/><path fill="#dd2025" d="M22.472 10.211H2.395V3.379h20.077z"/></svg>',
png: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#5D87FF" d="M14 14.252a1.998 1.998 0 0 1-1.628-.872l-.372-.56a2 2 0 0 0-1.629-.872H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-2.37a1.998 1.998 0 0 1 0 .504z"/><path fill="#FF5D5D" d="M19 10V4a2 2 0 0 0-2-2h-3a2 2 0 0 0-2 2v.847a2 2 0 0 1-1.165 1.772l-.416.211a2 2 0 0 0-1.166 1.772V10h10z"/></svg>',
jpg: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#FF9500" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path fill="#FFB74D" d="M12 18c-3.313 0-6-2.687-6-6s2.687-6 6-6 6 2.687 6 6-2.687 6-6 6z"/><path fill="#FFD54F" d="M12 14c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2z"/></svg>',
default:
'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><g fill="none"><path fill="url(#SVG1)" d="M8.004 1L9.5 4.5L13 6l-.008 7.5a1.5 1.5 0 0 1-1.5 1.5H4.5A1.5 1.5 0 0 1 3 13.5v-11A1.5 1.5 0 0 1 4.5 1z"/><defs><linearGradient id="SVG1" x1="9.994" x2="11.264" y1="1" y2="12.781" gradientUnits="userSpaceOnUse"><stop stop-color="#6CE0FF"/><stop offset="1" stop-color="#4894FE"/></linearGradient></defs></g></svg>',
};
if (this.isImageFile(filename)) {
return "";
}
return iconMap[ext] || iconMap.default;
},
/**
* 压缩图片
* @param {File} file - 图片文件
* @param {Object} options - 压缩选项
* @returns {Promise<File|Blob>} 压缩后的文件
*/
compressImage: async function (file, options = {}) {
const {
minSize = 300 * 1024,
maxSize = 1.2 * 1024 * 1024,
maxWidth = 1920,
maxHeight = 1920,
} = options;
if (!file || !file.type.startsWith("image/")) return file;
const originSize = file.size;
if (originSize <= maxSize) {
return file;
}
const img = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = reject;
image.src = reader.result;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
let w = img.width;
let h = img.height;
const scale = Math.min(1, maxWidth / w, maxHeight / h);
w = Math.round(w * scale);
h = Math.round(h * scale);
canvas.width = w;
canvas.height = h;
ctx.drawImage(img, 0, 0, w, h);
const getBlob = (q) =>
new Promise((resolve) => {
canvas.toBlob((b) => resolve(b), "image/jpeg", q);
});
let quality = 0.92;
if (originSize > 4 * 1024 * 1024) {
quality = 0.88;
}
const blob = await getBlob(quality);
if (blob && blob.size < minSize) {
const safe = await getBlob(0.97);
return safe || file;
}
return blob || file;
},
/**
* 判断是否为图片文件
* @param {string} filename - 文件名
* @returns {boolean} 是否为图片文件
*/
isImageFile: function (filename) {
if (!filename || typeof filename !== "string") return false;
const ext = filename.slice(filename.lastIndexOf(".") + 1).toLowerCase();
const imageExts = new Set(["jpg", "jpeg", "png", "gif", "bmp", "webp"]);
return imageExts.has(ext);
},
/**
* 格式化文件大小
* @param {number} bytes - 文件大小(字节)
* @returns {string} 格式化后的大小
*/
formatFileSize: function (bytes) {
if (bytes === null || bytes === undefined || isNaN(bytes) || bytes <= 0) {
return "-";
}
var k = 1024;
var sizes = ["B", "KB", "MB", "GB", "TB"];
var i = Math.floor(Math.log(bytes) / Math.log(k));
i = Math.max(0, Math.min(i, sizes.length - 1));
return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i];
},
/**
* 格式化日期
* @param {string} dateStr - 日期字符串
* @returns {string} 格式化后的日期
*/
formatDate: function (dateStr) {
if (!dateStr) return "-";
var date = new Date(dateStr);
if (isNaN(date.getTime())) {
return "-";
}
var year = date.getFullYear();
var month = ("0" + (date.getMonth() + 1)).slice(-2);
var day = ("0" + date.getDate()).slice(-2);
var hours = ("0" + date.getHours()).slice(-2);
var minutes = ("0" + date.getMinutes()).slice(-2);
var seconds = ("0" + date.getSeconds()).slice(-2);
return (
year +
"-" +
month +
"-" +
day +
" " +
hours +
":" +
minutes +
":" +
seconds
);
},
/**
* 获取唯一文件名
* @param {string} fileName - 原始文件名
* @param {Set} files - 已存在的文件名集合
* @returns {string} 唯一文件名
*/
getUniqueFileName: function (fileName, files) {
const originalFileName = fileName;
let uniqueFileName = fileName;
let counter = 1;
while (files.has(uniqueFileName)) {
const fileNameWithoutExtension = originalFileName.replace(
/\.[^/.]+$/,
"",
);
const extension = originalFileName.slice(
originalFileName.lastIndexOf("."),
);
uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}`;
counter++;
}
return uniqueFileName;
},
/**
* 获取文件Blob
* @param {string} url - 文件URL
* @returns {Promise<ArrayBuffer>} 文件二进制数据
*/
getFileBlob: async function (url) {
if (!url) {
throw new Error("无效的文件URL");
}
const res = await fetch(url);
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="header">
<div class="check-all">
<input type="checkbox" class="check-all-checkbox" style="cursor: pointer;" />
<span>${this.language.selectAll}</span>
</div>
<button class="download-all" disabled>
${this.staticIconMap.download}
${this.language.downloadBatch}
</button>
<button class="delete-all" disabled>
${this.staticIconMap.delete}
${this.language.delete}
</button>
</div>
<div class="main" style="display: none;">main</div>
<div class="empty-tip">
<span>${this.language.emptyTip1}</span>
<span>${this.language.emptyTip2}</span>
<span>${this.language.emptyTip3}</span>
</div>
<div class="footer">
<div class="pending-count">
<span>${this.language.pending}</span>
<span class="pending-number">0</span>
</div>
<div>
<button class="select" disabled>
${this.staticIconMap.select}
${this.language.select}
</button>
<button class="upload" disabled>
${this.staticIconMap.upload}
${this.language.upload}
</button>
</div>
<input type="file" class="file-input" style="display: none;" multiple />
</div>
<div class="g3-global-menu"></div>
</div>
<div class="notification-container"></div>
<div class="task-dock">
<div class="dock-header">
<span class="dock-title">Task Running</span>
<div class="dock-actions">
<button title="close">×</button>
</div>
</div>
<div class="dock-body">
<div class="progress-line">
<span class="count">0 / 0</span>
<span class="percent">0%</span>
</div>
<div class="task-name"></div>
<div class="bar">
<div class="bar-inner"></div>
</div>
<div class="task-result">
Total: 0 | Success: 0 | Fail: 0
</div>
</div>
</div>
<div class="ui-lock"></div>
`;
this.$container.html(html);
this.$fileList = this.$container.find(".main");
this.$emptyTip = this.$container.find(".empty-tip");
this.$checkAll = this.$container.find(".check-all-checkbox");
this.$btnDownload = this.$container.find(".download-all");
this.$btnDelete = this.$container.find(".delete-all");
this.$btnUpload = this.$container.find(".upload");
this.$btnSelect = this.$container.find(".select");
this.$fileInput = this.$container.find(".file-input");
this.$pendingCount = this.$container.find(".pending-number");
this.$taskDock = this.$container.find(".task-dock");
this.$notificationContainer = this.$container.find(
".notification-container",
);
this.$uiLock = this.$container.find(".ui-lock");
// 通知管理
this.notifications = [];
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);
var self = this;
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-file-action.more");
var rect = $anchor[0].getBoundingClientRect();
var $mainContainer = this.$container.find(".main");
var mainRect = $mainContainer[0].getBoundingClientRect();
var menuWidth = 160;
var itemHeight = 36;
var menuHeight = actionMenuState.items.length * itemHeight;
var top = rect.bottom;
var left = rect.right - menuWidth;
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;
}
actionMenu.css({
top: top,
left: left,
});
requestAnimationFrame(function () {
actionMenu.addClass("is-open");
});
},
/**
* 创建文件项
* @param {Object} file - 文件对象
* @returns {string} 文件项HTML
*/
createFileItem: function (file) {
const previewDisabled = !this.hasPermission(file, "view");
const downloadDisabled = !this.hasPermission(file, "download");
const deleteDisabled = !this.hasPermission(file, "delete");
let content = "";
if (this.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">
${this.staticIconMap.error || '<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>'}
</div>
</div>
`;
} else {
content = this.getFileIcon(file.name);
}
let statusBadge = "";
switch (file.status) {
case "pending":
statusBadge = `
<div class="g3-file-pending-badge">
${this.language.pending}
</div>
`;
break;
case "uploading":
statusBadge = `
<div class="g3-file-uploading-badge">
上传中
</div>
`;
break;
case "failed":
statusBadge = `
<div class="g3-file-failed-badge">
上传失败
</div>
`;
break;
}
const deleteButton = `
<div class="g3-file-delete-btn ${deleteDisabled ? "is-disabled" : ""}>
${this.staticIconMap.delete}
</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}
${deleteButton}
<div class="g3-file-info">
<span class="g3-file-name">${file.name}</span>
<span>${this.formatFileSize(file.size)}</span>
<span>${this.formatDate(file.uploadDate)}</span>
</div>
<div class="g3-file-actions">
<div class="g3-file-action preview ${previewDisabled ? "is-disabled" : ""}" title="${this.language.preview}">
${this.staticIconMap.preview}
</div>
<div class="g3-file-action download ${downloadDisabled ? "is-disabled" : ""}" title="${this.language.download}">
${this.staticIconMap.download}
</div>
<div class="g3-file-action more" title="${this.language.more}">
${this.staticIconMap.more}
</div>
</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);
this.bindFileItemEvents();
},
/**
* 更新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-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);
});
},
// ============================================================================
// 事件绑定相关方法
// ============================================================================
/**
* 绑定事件
*/
bindEvents: function () {
var self = this;
// 选择全部
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 () {
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;
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);
}
});
},
/**
* 绑定文件项事件
*/
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 (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();
}
},
/**
* 内部上传方法,支持阿里云上传
* @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 = $(
`<div class="notification notification-${config.type} ${config.customClass}" data-id="${notificationId}" style="position: fixed; z-index: 9999; animation: slideIn 0.3s ease-out;">
<div class="notification-content">
${config.title ? `<div class="notification-title">${config.title}</div>` : ""}
<div class="notification-message">${config.message}</div>
</div>
${config.showClose ? '<button class="notification-close">×</button>' : ""}
</div>`,
);
// 添加到容器
this.$notificationContainer.append(notificationEl);
// 计算位置
this._positionNotification(notificationEl, config);
// 绑定关闭事件
if (config.showClose) {
notificationEl.find(".notification-close").on("click", () => {
this.closeNotification(notificationId);
});
}
// 自动关闭
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") {
var fileResult = filePermissionResolver({ file: file, action: action });
if (fileResult !== undefined) {
return !!fileResult;
}
}
// 2. 判断全局权限
if (typeof globalPermission === "function") {
return !!globalPermission({ file: file, action: action });
} else if (typeof globalPermission === "object") {
return !!globalPermission[action];
}
// 默认返回 true
return true;
},
/**
* 更新权限配置
* @param {Object} config - 权限配置
*/
updatePermissions: function (config) {
if (!config) return;
if (config.globalPermission !== undefined) {
this.options.globalPermission = config.globalPermission;
}
if (config.filePermissionResolver !== undefined) {
this.options.filePermissionResolver = config.filePermissionResolver;
}
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();
// 清理ObjectURL
this.objectUrls.forEach((url) => {
try {
URL.revokeObjectURL(url);
} catch (e) {
console.warn("释放ObjectURL失败:", e);
}
});
this.objectUrls.clear();
// 清理引用
this.files = [];
this.selectedFiles.clear();
this.$container.empty();
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);
}
}
});
return instance || this;
};
})(jQuery);