(function ($) {
"use strict";
var G3File = {
files: [],
selectedFiles: new Set(),
previewFiles: new Set(),
loadScripts: function (items, callback) {
var self = this;
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);
});
},
// =====================
// 初始化
// =====================
init: function (containerId, options) {
var self = this;
this.$container = $("#" + containerId);
this.options = $.extend({}, this.defaults, options);
this.language =
this.languages[this.options.language] || this.languages["zh-CN"];
this.files = this.options.data || [];
this.selectedFiles = new Set(); // 选中文件集合
this.previewFiles = new Set(); // 预览文件集合
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.updatePermissions(self.options.permissions);
if (self.options.data?.length) {
self.loadFiles(self.options.data);
}
},
);
return this;
},
// =====================
// 默认配置
// =====================
defaults: {
data: [],
onDelete: null,
onDownload: null,
onSelect: null,
onUpload: null,
onPreview: null,
language: "zh-CN",
maxFileSize: 10 * 1024 * 1024, // 默认最大10MB
permissions: {
select: true,
upload: true,
download: true,
delete: true,
view: true,
},
},
// =====================
// 多语言
// =====================
languages: {
"zh-CN": {
selectAll: "全选",
download: "批量下载",
delete: "删除",
upload: "上传",
select: "选择",
confirmDelete: "确定要删除选中的 {count} 个文件吗?",
downloading: "准备下载 {count} 个文件",
deleting: "删除中...",
name: "名称",
size: "大小",
date: "日期",
pending: "未上传",
selectFiles: "选择文件",
emptyTip1: "拖拽文件到这里 …",
emptyTip2: "支持多文件同时上传/下载",
emptyTip3: "支持粘贴图片上传",
fileSizeError: "文件大小超过限制",
},
"en-US": {
selectAll: "Select All",
download: "Batch Download",
delete: "Delete",
upload: "Upload",
select: "Select",
confirmDelete: "Are you sure to delete {count} selected files?",
downloading: "Preparing to download {count} files",
deleting: "Deleting...",
name: "Name",
size: "Size",
date: "Date",
pending: "Pending",
selectFiles: "Select Files",
fileSizeError: "File size exceeds limit",
},
},
// =====================
// 静态icon合计
// =====================
staticIconMap: {
download: `
`,
delete: `
`,
select: `
`,
upload: `
`,
more: `
`,
pdf: `
`,
video: `
`,
default_file: `
`,
},
// =====================
// 生成唯一ID
// =====================
generateUniqueId: function () {
return "file_" + Math.random().toString(36).slice(2) + Date.now();
},
// =====================
// 获取文件图标
// =====================
getFileIcon: function (filename) {
var ext = filename.split(".").pop().toLowerCase();
return this.staticIconMap[ext] || this.staticIconMap["default_file"];
},
// =====================
// 更新语言
// =====================
updateLanguage: function (language) {
this.language = this.languages[language] || this.languages["zh-CN"];
this.render();
return this;
},
// =====================
// 渲染
// =====================
render: function () {
const html = `
main
${this.language.emptyTip1}
${this.language.emptyTip2}
${this.language.emptyTip3}
`;
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");
return this;
},
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.$fileList.find(".file-checkbox").prop("checked", checked);
self.updateToolbarState();
});
// 批量下载
this.$btnDownload.off("click").on("click", function () {
var idSet = self.selectedFiles || new Set();
var files = self.files.filter(function (item) {
return idSet.has(item._id);
});
if (typeof self.options?.onDownloadBatch === "function") {
self.options.onDownloadBatch(files);
return;
}
self.internalDownloadBatch(files);
});
// 批量删除
this.$btnDelete.off("click").on("click", function () {
var idSet = self.selectedFiles || new Set();
if (idSet.size === 0) return;
var deleteFiles = self.files.filter(function (item) {
return idSet.has(item._id);
});
// =====================
// 1. 优先走外部回调(如果你要接后端)
// =====================
if (typeof self.options?.onDeleteBatch === "function") {
self.options.onDeleteBatch(deleteFiles, function (success) {
if (!success) return;
// 成功后再清理本地
self.internalDeleteBatch(deleteFiles);
});
return;
}
// =====================
// 2. 没有外部逻辑 -> 直接本地删除
// =====================
self.internalDeleteBatch(deleteFiles);
});
// =====================
// 选择按钮点击事件(选择本地文件)
// =====================
this.$btnSelect.off("click").on("click", function () {
self.$fileInput.click();
});
// =====================
// 上传按钮点击事件(交给外部处理)
// =====================
this.$btnUpload.off("click").on("click", function () {
// 获取所有未上传的文件
const pendingFiles = self.files.filter((file) => !file.uploaded);
if (typeof self.options.onUpload === "function") {
self.options.onUpload(pendingFiles);
}
});
// =====================
// 文件选择事件
// =====================
this.$fileInput.off("change").on("change", function (e) {
var files = e.target.files;
if (!files || files.length === 0) return;
var maxSize = self.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 = self.language.fileSizeError || "文件大小超过限制";
alert(errorMsg + ": " + invalidFiles.map((f) => f.name).join(", "));
}
if (validFiles.length > 0) {
// 在本地临时插入文件
var tempFiles = validFiles.map(function (file) {
// 为图片文件创建本地临时连接
let url = "";
if (self.isImageFile(file.name)) {
url = URL.createObjectURL(file);
}
return {
_id: self.generateUniqueId(),
name: file.name,
size: file.size,
uploadDate: new Date().toISOString(),
uploaded: false, // 标记为未上传
url: url, // 图片文件使用本地临时连接,其他文件为空
file: file, // 保存原始文件对象
};
});
// 添加到文件列表
self.files = self.files.concat(tempFiles);
self.renderFileList();
self.updateToolbarState();
if (typeof self.options.onSelect === "function") {
self.options.onSelect(validFiles);
}
}
// 重置文件输入框,以便可以重复选择相同的文件
self.$fileInput.val("");
});
},
// =====================
// 文件判断
// =====================
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);
},
// =====================
// 更新工具栏状态
// =====================
updateToolbarState: function () {
var hasSelection = this.selectedFiles.size > 0;
this.$btnDownload.prop("disabled", !hasSelection);
this.$btnDelete.prop("disabled", !hasSelection);
var allSelected =
this.files.length > 0 && this.selectedFiles.size === this.files.length;
this.$checkAll.prop("checked", allSelected);
// 更新未上传文件数量
var pendingCount = this.files.filter((file) => !file.uploaded).length;
this.$pendingCount.text(pendingCount);
var self = this;
this.$fileList.find(".file-item").each(function () {
var id = $(this).data("id");
var isSelected = self.selectedFiles.has(id);
$(this).find(".file-checkbox").prop("checked", isSelected);
// 添加或移除选中样式
if (isSelected) {
$(this).addClass("file-item-selected");
} else {
$(this).removeClass("file-item-selected");
}
});
},
// =====================
// 加载文件
// =====================
loadFiles: function (files) {
var self = this;
this.files = files.map(function (file) {
return Object.assign({}, file, {
_id: self.generateUniqueId(),
uploaded: true,
});
});
this.selectedFiles.clear();
this.renderFileList();
this.updateToolbarState();
},
// =====================
// 渲染文件列表
// =====================
renderFileList: function () {
var self = this;
if (this.files.length === 0) {
this.$fileList.hide();
this.$emptyTip.show();
return;
}
this.$fileList.show();
this.$emptyTip.hide();
var html = "";
$.each(this.files, function (_, file) {
html += self.createFileItem(file);
});
this.$fileList.html(html);
this.bindFileItemEvents();
},
// =====================
// 格式化文件大小
// =====================
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];
},
// =====================
// 格式化日期
// =====================
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
);
},
// =====================
// 创建文件项
// =====================
createFileItem: function (file) {
const perm = this.options.permissions || {};
const downloadDisabled = !perm.download;
const deleteDisabled = !perm.delete;
let content = "";
if (this.isImageFile(file.name)) {
content = `
`;
} else {
content = this.getFileIcon(file.name);
}
// 未上传标记
const pendingBadge = file.uploaded
? ""
: `
${this.language.pending}
`;
// 预览状态(使用class标识)
const isPreview = this.previewFiles.has(file._id);
// 选中状态
const isSelected = this.selectedFiles.has(file._id);
return `
${content}
${pendingBadge}
${file.name}
${this.formatFileSize(file.size)}
${this.formatDate(file.uploadDate)}
${this.staticIconMap.download}
${this.staticIconMap.delete}
${this.staticIconMap.more}
`;
},
// =====================
// 绑定文件项事件
// =====================
bindFileItemEvents: function () {
var self = this;
// =====================
// 文件item点击事件(用于预览)
// =====================
this.$fileList
.off("click", ".file-item")
.on("click", ".file-item", function (e) {
var target = $(e.target);
// 如果点击的是 checkbox、download、delete、more 按钮,则不执行预览逻辑
if (
target.is(".file-checkbox") ||
target.closest(".file-action").length > 0
) {
return;
}
var id = $(this).data("id");
var file = self.files.find((f) => f._id === id);
if (!file) return;
// 切换预览状态(只允许同时预览一个文件)
var isPreview = self.previewFiles.has(id);
if (isPreview) {
// 取消预览
self.previewFiles.delete(id);
} else {
// 开启预览前,先清除其他文件的预览状态
self.previewFiles.clear();
self.previewFiles.add(id);
}
// 重新渲染文件列表以更新预览状态
self.renderFileList();
// 调用外部回调,传递文件和预览状态
if (typeof self.options.onPreview === "function") {
self.options.onPreview(file, !isPreview);
}
});
this.$fileList
.off("click", ".download")
.on("click", ".download", function (e) {
e.preventDefault();
e.stopPropagation();
var id = $(this).closest(".file-item").data("id");
var file = self.files.find((f) => f._id === id);
if (!file) return;
if (typeof self.options.onDownload === "function") {
self.options.onDownload([file]);
return;
}
self.internalDownload(file);
});
this.$fileList
.off("change", ".file-checkbox")
.on("change", ".file-checkbox", function () {
var id = $(this).closest(".file-item").data("id");
if ($(this).is(":checked")) {
self.selectedFiles.add(id);
} else {
self.selectedFiles.delete(id);
}
self.updateToolbarState();
});
this.$fileList
.off("click", ".delete")
.on("click", ".delete", function (e) {
e.preventDefault();
e.stopPropagation();
var id = $(this).closest(".file-item").data("id");
var file = self.files.find((f) => f._id === id);
if (!file) return;
if (typeof self.options.onDelete === "function") {
self.options.onDelete([file]);
return;
}
self.internalDelete(file);
});
// =====================
// 更多按钮点击事件(阻止冒泡)
// =====================
this.$fileList.off("click", ".more").on("click", ".more", function (e) {
e.preventDefault();
e.stopPropagation();
});
},
getUniqueFileName: function (fileName, files) {
// 保存原始文件名
const originalFileName = fileName;
let uniqueFileName = fileName;
let counter = 1;
// 如果文件名已经存在,则在文件名后加上 (1), (2), ...
while (files.has(uniqueFileName)) {
// 基于原始文件名为基础来添加计数器
const fileNameWithoutExtension = originalFileName.replace(
/\.[^/.]+$/,
"",
);
const extension = originalFileName.slice(
originalFileName.lastIndexOf("."),
);
uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}`;
counter++;
}
return uniqueFileName;
},
getFileBlob: async function (url) {
if (!url) {
throw new Error("无效的文件URL");
}
const res = await fetch(url);
if (!res.ok) {
throw new Error("网络请求失败,状态码:" + res.status);
}
// ⭐ 比 blob 更轻一点(后续 zip 更快)
const buffer = await res.arrayBuffer();
return buffer;
},
// =====================
// 内部下载文件
// =====================
internalDownload: async function (file) {
if (!file) {
console.warn("下载失败:文件对象无效", file);
return;
}
console.log("开始下载文件:", file.name);
try {
let blob;
if (file.uploaded && file.url) {
// 已上传的文件,从URL下载
const buffer = await this.getFileBlob(file.url);
blob = new Blob([buffer]);
} else if (file.file) {
// 未上传的文件,使用本地文件对象
blob = file.file;
} else {
console.warn("下载失败:文件没有URL或本地文件对象", file);
return;
}
saveAs(blob, file.name || "未命名文件");
} catch (err) {
console.error("文件下载失败:", err);
}
},
// =====================
// 内部下载所有文件
// =====================
internalDownloadBatch: async function (files, callback) {
if (!files || files.length === 0) return;
const zip = new JSZip();
const addedFiles = new Set();
const total = files.length;
let current = 0;
let failedCount = 0;
const concurrency = 3;
const downloadOne = async (item) => {
try {
let content;
if (item.uploaded && item.url) {
// 已上传的文件,从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 = this.getUniqueFileName(item.name, addedFiles);
zip.file(uniqueFileName, content);
addedFiles.add(uniqueFileName);
} catch (err) {
console.warn("跳过失败文件:", item.name, err);
failedCount++;
} finally {
current++;
}
};
const runner = async () => {
while (files.length) {
const item = files.shift();
await downloadOne(item);
}
};
try {
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");
console.log(
`下载完成:${total - failedCount}/${total},失败:${failedCount}`,
);
callback && callback();
} catch (err) {
console.error("Batch download failed:", err);
callback && callback();
}
},
// =====================
// 内部删除文件
// =====================
internalDelete: function (file) {
if (!file) return;
// 清理本地临时连接
if (file.url && file.uploaded === false) {
try {
URL.revokeObjectURL(file.url);
} catch (e) {
console.warn("Failed to revoke object URL:", e);
}
}
this.files = this.files.filter((f) => f._id !== file._id);
this.selectedFiles.delete(file._id);
this.updateToolbarState();
this.renderFileList();
},
// =====================
// 内部删除所有文件
// =====================
internalDeleteBatch: function (files) {
if (!files || files.length === 0) return;
files.forEach((file) => this.internalDelete(file));
this.updateToolbarState();
this.renderFileList();
},
// =====================
// 更新权限
// =====================
updatePermissions: function (permissions) {
if (!permissions) return;
this.options.permissions = permissions;
this.$btnSelect?.prop("disabled", !permissions.select);
this.$btnUpload?.prop("disabled", !permissions.upload);
this.$btnDownload?.prop("disabled", !permissions.download);
this.$btnDelete?.prop("disabled", !permissions.delete);
this.$fileList
.find(".file-action.download")
.toggleClass("is-disabled", !permissions.download);
this.$fileList
.find(".file-action.delete")
.toggleClass("is-disabled", !permissions.delete);
},
// =====================
// 外部方法:切换文件选中状态
// 参数:fileId - 文件ID,isSelected - 是否选中(可选,不传则切换)
// =====================
toggleSelect: function (fileId, isSelected) {
var file = this.files.find((f) => f._id === fileId);
if (!file) return;
var currentSelected = this.selectedFiles.has(fileId);
var newSelected =
isSelected !== undefined ? isSelected : !currentSelected;
if (newSelected) {
this.selectedFiles.add(fileId);
} else {
this.selectedFiles.delete(fileId);
}
this.updateToolbarState();
this.renderFileList();
return newSelected;
},
// =====================
// 外部方法:获取当前选中的文件列表
// =====================
getSelectedFiles: function () {
return this.files.filter((f) => this.selectedFiles.has(f._id));
},
// =====================
// 外部方法:获取当前预览的文件列表
// =====================
getPreviewFiles: function () {
return this.files.filter((f) => this.previewFiles.has(f._id));
},
// =====================
// 外部方法:获取所有文件列表
// =====================
getFiles: function () {
return this.files;
},
// =====================
// 外部方法:切换文件预览状态
// 参数:fileId - 文件ID,isPreview - 是否预览(可选,不传则切换)
// =====================
togglePreview: function (fileId, isPreview) {
var file = this.files.find((f) => f._id === fileId);
if (!file) return;
var currentPreview = this.previewFiles.has(fileId);
var newPreview = isPreview !== undefined ? isPreview : !currentPreview;
if (newPreview) {
// 开启预览前,先清除其他文件的预览状态(只允许同时预览一个)
this.previewFiles.clear();
this.previewFiles.add(fileId);
} else {
this.previewFiles.delete(fileId);
}
this.renderFileList();
// 调用外部回调
if (typeof this.options.onPreview === "function") {
this.options.onPreview(file, newPreview);
}
return newPreview;
},
};
// ==========================
// 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 = Object.create(G3File);
instance.init(this.id, options);
$this.data("G3File", instance);
// ========= 方法调用 =========
} else if (typeof options === "string") {
var method = options;
if (instance[method]) {
// 保存方法调用结果
methodResult = instance[method].apply(instance, args);
}
}
});
// 如果是方法调用,返回方法结果;否则返回实例
return methodResult !== undefined ? methodResult : instance || this;
};
})(jQuery);