(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,
onPreview: null,
getActionMenu: null,
language: 'zh-CN',
maxFileSize: 10 * 1024 * 1024,
// 全局权限(支持对象或函数)
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: '关闭'
},
'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'
}
},
// =====================
// 静态icon
// =====================
staticIconMap: {
download: `
`,
delete: `
`,
select: `
`,
upload: `
`,
preview: `
`,
more: `
`
},
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);
});
},
// =====================
// 初始化
// =====================
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
// =====================
generateUniqueId: function () {
return "file_" + Math.random().toString(36).slice(2) + Date.now();
},
// =====================
// 获取文件图标
// =====================
getFileIcon: function (filename) {
var ext = filename.split('.').pop().toLowerCase();
var iconMap = {
pdf: '',
png: '',
jpg: '',
default: ''
};
// 不做处理
if (this.isImageFile(filename)) {
return ''
}
return iconMap[ext] || iconMap.default;
},
// =====================
// 渲染
// =====================
render: function () {
const html = `
main
${this.language.emptyTip1}
${this.language.emptyTip2}
${this.language.emptyTip3}
0 / 0
0%
Total: 0 | Success: 0 | Fail: 0
`;
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.$uiLock = this.$container.find('.ui-lock');
return this;
},
renderActionMenu: function () {
var actionMenu = this.$container.find('.g3-global-menu');
if (!actionMenu.length) {
this.$container.append('');
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 += ``;
});
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();
});
// =========================
// ⭐ 强制定位到 more 按钮
// =========================
var $anchor = $(actionMenuState.anchorEl).closest('.g3-file-action.more');
var rect = $anchor[0].getBoundingClientRect();
var menuWidth = 160;
var itemHeight = 36;
var menuHeight = actionMenuState.items.length * itemHeight;
var viewportW = window.innerWidth;
var viewportH = window.innerHeight;
// =========================
// ⭐ fixed 模式:不要 scrollY!
// =========================
var top = rect.bottom;
var left = rect.right - menuWidth;
var spaceBottom = viewportH - rect.bottom;
var spaceTop = rect.top;
var openUp = spaceBottom < menuHeight && spaceTop > menuHeight;
if (openUp) {
// 向上打开时,菜单顶部应该与 file-item 顶部对齐,而不是 file-action 顶部
var $fileItem = $anchor.closest('.g3-file-item');
var fileItemRect = $fileItem[0].getBoundingClientRect();
top = fileItemRect.top;
}
// 左侧兜底
if (rect.right < menuWidth) {
left = rect.left;
}
left = Math.max(8, Math.min(left, viewportW - menuWidth - 8));
actionMenu.css({
top: top,
left: left
});
requestAnimationFrame(function () {
actionMenu.addClass('is-open');
});
},
getFileById: function (id) {
return this.files.find(function (f) {
return f.id === id;
});
},
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;
// ===== 🚨 关键1:小图/中图直接不压 =====
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);
});
// ===== 🚨 关键2:只做“单次轻压” =====
// 不做循环,不做二分
let quality = 0.92;
// 只有超大图才稍微降一点
if (originSize > 4 * 1024 * 1024) {
quality = 0.88;
}
const blob = await getBlob(quality);
// ===== 🚨 关键3:防止压太小(核心修复)=====
if (blob && blob.size < minSize) {
// ❗直接回退高质量版本
const safe = await getBlob(0.97);
return safe || file;
}
return blob || file;
},
// =====================
// 绑定事件
// =====================
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);
}
});
// 文件选择
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();
}
});
},
// =====================
// 处理文件选择
// =====================
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);
},
// =====================
// 任务管理
// =====================
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();
this.lockUI();
return newTask.id;
},
updateTask: function (taskId, updates) {
const task = this.tasks.find(t => t.id === taskId);
if (task) {
Object.assign(task, updates);
this.updateTaskDock();
}
},
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();
},
// =====================
// Task Dock UI 管理
// =====================
showTaskDock: function () {
if (this.$taskDock) {
this.$taskDock.show();
this.bindTaskDockEvents();
}
},
hideTaskDock: function () {
if (this.$taskDock) {
this.$taskDock.hide();
}
},
minimizeTaskDock: function () {
if (this.$taskDock) {
const dockBody = this.$taskDock.find('.dock-body');
if (dockBody.is(':visible')) {
dockBody.hide();
} else {
dockBody.show();
}
}
},
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;
// 更新 UI
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('.task-type').text(currentTask.type);
this.$taskDock.find('.bar .bar-inner').css('width', `${currentTask.progress}%`);
this.$taskDock.find('.task-result').text(`Total: ${this.taskStats.total} | Success: ${this.taskStats.success} | Fail: ${this.taskStats.fail}`);
}
},
bindTaskDockEvents: function () {
const self = this;
// 绑定关闭按钮事件
this.$taskDock.find('.dock-actions button[title="close"]').off('click').on('click', function () {
self.clearTasks();
});
// 绑定最小化按钮事件
this.$taskDock.find('.dock-actions button[title="minimize"]').off('click').on('click', function () {
self.minimizeTaskDock();
});
},
// =====================
// UI Lock 功能
// =====================
lockUI: function () {
if (this.$uiLock) {
this.$uiLock.show();
}
},
unlockUI: function () {
if (this.$uiLock) {
this.$uiLock.hide();
}
},
isUILocked: function () {
return this.$uiLock && this.$uiLock.is(':visible');
},
// =====================
// 绑定文件项事件
// =====================
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);
});
},
// =====================
// 处理下载
// =====================
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]);
}
},
// =====================
// 处理删除
// =====================
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);
},
// =====================
// 内部下载
// =====================
internalDownload: async function (file) {
if (!file) {
console.warn('下载失败:文件对象无效', file);
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);
return;
}
saveAs(blob, file.name || '未命名文件');
} catch (err) {
console.error('文件下载失败:', err);
}
},
// =====================
// 内部批量下载
// =====================
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}`);
// 所有任务完成后,保持 dock 显示一段时间,让用户查看结果
setTimeout(() => {
this.clearTasks();
}, 3000);
} catch (err) {
console.error("批量下载失败:", err);
// 失败时清理任务
this.clearTasks();
}
},
// =====================
// 内部删除
// =====================
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();
},
// =====================
// 获取文件Blob
// =====================
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;
},
// =====================
// 获取唯一文件名
// =====================
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;
},
// =====================
// 判断是否为图片文件
// =====================
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);
},
// =====================
// 格式化文件大小
// =====================
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 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 = `
${this.staticIconMap.error || ''}
`;
} else {
content = this.getFileIcon(file.name);
}
const pendingBadge = file.status === 'pending' ? `
${this.language.pending}
` : '';
const deleteButton = `
${this.staticIconMap.delete}
`;
return `
${content}
${pendingBadge}
${deleteButton}
${file.name}
${this.formatFileSize(file.size)}
${this.formatDate(file.uploadDate)}
${this.staticIconMap.preview}
${this.staticIconMap.download}
${this.staticIconMap.more}
`;
},
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();
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);
});
},
// =====================
// 获取选中文件
// =====================
getSelectedFiles: function () {
return this.files.filter(file => this.selectedFiles.has(file.id));
},
// =====================
// 设置文件
// =====================
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;
},
// =====================
// 更新状态
// =====================
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;
},
// =====================
// 权限判断(唯一入口)
// =====================
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;
},
// =====================
// 更新权限配置
// =====================
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();
},
// =====================
// 销毁
// =====================
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);