1514 lines
47 KiB
JavaScript
1514 lines
47 KiB
JavaScript
(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: `
|
||
<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>
|
||
`
|
||
},
|
||
|
||
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: '<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;
|
||
},
|
||
|
||
// =====================
|
||
// 渲染
|
||
// =====================
|
||
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="task-dock">
|
||
<div class="dock-header">
|
||
<span class="title">Task Running</span>
|
||
<div class="dock-actions">
|
||
<button title="minimize">−</button>
|
||
<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="task-type"></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.$uiLock = this.$container.find('.ui-lock');
|
||
|
||
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();
|
||
});
|
||
|
||
// =========================
|
||
// ⭐ 强制定位到 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 = `
|
||
<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);
|
||
}
|
||
|
||
const pendingBadge = file.status === 'pending' ? `
|
||
<div class="g3-file-pending-badge">
|
||
${this.language.pending}
|
||
</div>
|
||
` : '';
|
||
|
||
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>
|
||
${pendingBadge}
|
||
${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' : ''}">
|
||
${this.staticIconMap.preview}
|
||
</div>
|
||
|
||
<div class="g3-file-action download ${downloadDisabled ? 'is-disabled' : ''}">
|
||
${this.staticIconMap.download}
|
||
</div>
|
||
|
||
<div class="g3-file-action more">
|
||
${this.staticIconMap.more}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
},
|
||
|
||
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;
|
||
|
||
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]) {
|
||
instance[method].apply(instance, args);
|
||
}
|
||
}
|
||
});
|
||
|
||
return instance || this;
|
||
};
|
||
|
||
})(jQuery); |