908 lines
34 KiB
JavaScript
908 lines
34 KiB
JavaScript
(function ($) {
|
||
'use strict';
|
||
|
||
var G3File = {
|
||
files: [],
|
||
selectedFiles: 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.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,
|
||
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: `
|
||
<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="14" height="14" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11v6m4-6v6m5-11v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></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>
|
||
`,
|
||
more: `
|
||
<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"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></g></svg>
|
||
`,
|
||
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"/><path fill="#464648" d="M9.052 4.534H7.745v4.8h1.028V7.715L9 7.728a2 2 0 0 0 .647-.117a1.4 1.4 0 0 0 .493-.291a1.2 1.2 0 0 0 .335-.454a2.1 2.1 0 0 0 .105-.908a2.2 2.2 0 0 0-.114-.644a1.17 1.17 0 0 0-.687-.65a2 2 0 0 0-.409-.104a2 2 0 0 0-.319-.026m-.189 2.294h-.089v-1.48h.193a.57.57 0 0 1 .459.181a.92.92 0 0 1 .183.558c0 .246 0 .469-.222.626a.94.94 0 0 1-.524.114m3.671-2.306c-.111 0-.219.008-.295.011L12 4.538h-.78v4.8h.918a2.7 2.7 0 0 0 1.028-.175a1.7 1.7 0 0 0 .68-.491a1.9 1.9 0 0 0 .373-.749a3.7 3.7 0 0 0 .114-.949a4.4 4.4 0 0 0-.087-1.127a1.8 1.8 0 0 0-.4-.733a1.6 1.6 0 0 0-.535-.4a2.4 2.4 0 0 0-.549-.178a1.3 1.3 0 0 0-.228-.017m-.182 3.937h-.1V5.392h.013a1.06 1.06 0 0 1 .6.107a1.2 1.2 0 0 1 .324.4a1.3 1.3 0 0 1 .142.526c.009.22 0 .4 0 .549a3 3 0 0 1-.033.513a1.8 1.8 0 0 1-.169.5a1.1 1.1 0 0 1-.363.36a.67.67 0 0 1-.416.106m5.08-3.915H15v4.8h1.028V7.434h1.3v-.892h-1.3V5.43h1.4v-.892"/><path fill="#dd2025" d="M21.781 20.255s3.188-.578 3.188.511s-1.975.646-3.188-.511m-2.357.083a7.5 7.5 0 0 0-1.473.489l.4-.9c.4-.9.815-2.127.815-2.127a14 14 0 0 0 1.658 2.252a13 13 0 0 0-1.4.288Zm-1.262-6.5c0-.949.307-1.208.546-1.208s.508.115.517.939a10.8 10.8 0 0 1-.517 2.434a4.4 4.4 0 0 1-.547-2.162Zm-4.649 10.516c-.978-.585 2.051-2.386 2.6-2.444c-.003.001-1.576 3.056-2.6 2.444M25.9 20.895c-.01-.1-.1-1.207-2.07-1.16a14 14 0 0 0-2.453.173a12.5 12.5 0 0 1-2.012-2.655a11.8 11.8 0 0 0 .623-3.1c-.029-1.2-.316-1.888-1.236-1.878s-1.054.815-.933 2.013a9.3 9.3 0 0 0 .665 2.338s-.425 1.323-.987 2.639s-.946 2.006-.946 2.006a9.6 9.6 0 0 0-2.725 1.4c-.824.767-1.159 1.356-.725 1.945c.374.508 1.683.623 2.853-.91a23 23 0 0 0 1.7-2.492s1.784-.489 2.339-.623s1.226-.24 1.226-.24s1.629 1.639 3.2 1.581s1.495-.939 1.485-1.035"/><path fill="#909090" d="M23.954 2.077V7.95h5.633z"/><path fill="#f4f4f4" d="M24.031 2v5.873h5.633z"/><path fill="#fff" d="M8.975 4.457H7.668v4.8H8.7V7.639l.228.013a2 2 0 0 0 .647-.117a1.4 1.4 0 0 0 .493-.291a1.2 1.2 0 0 0 .332-.454a2.1 2.1 0 0 0 .105-.908a2.2 2.2 0 0 0-.114-.644a1.17 1.17 0 0 0-.687-.65a2 2 0 0 0-.411-.105a2 2 0 0 0-.319-.026m-.189 2.294h-.089v-1.48h.194a.57.57 0 0 1 .459.181a.92.92 0 0 1 .183.558c0 .246 0 .469-.222.626a.94.94 0 0 1-.524.114m3.67-2.306c-.111 0-.219.008-.295.011l-.235.006h-.78v4.8h.918a2.7 2.7 0 0 0 1.028-.175a1.7 1.7 0 0 0 .68-.491a1.9 1.9 0 0 0 .373-.749a3.7 3.7 0 0 0 .114-.949a4.4 4.4 0 0 0-.087-1.127a1.8 1.8 0 0 0-.4-.733a1.6 1.6 0 0 0-.535-.4a2.4 2.4 0 0 0-.549-.178a1.3 1.3 0 0 0-.228-.017m-.182 3.937h-.1V5.315h.013a1.06 1.06 0 0 1 .6.107a1.2 1.2 0 0 1 .324.4a1.3 1.3 0 0 1 .142.526c.009.22 0 .4 0 .549a3 3 0 0 1-.033.513a1.8 1.8 0 0 1-.169.5a1.1 1.1 0 0 1-.363.36a.67.67 0 0 1-.416.106m5.077-3.915h-2.43v4.8h1.028V7.357h1.3v-.892h-1.3V5.353h1.4v-.892"/></svg>
|
||
`,
|
||
video: `
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="#e75749" d="M4.5 2.375A2.56 2.56 0 0 1 5.861 2h20.366a2.545 2.545 0 0 1 2.423 1.951a3.4 3.4 0 0 1 .072.862v11.176h-1.573a2 2 0 0 0-.22.022a5 5 0 0 0-.581-.011c-.3.007-.61-.014-.914.014a2.3 2.3 0 0 0-.4-.02h-4.739c-.2 0-.392.008-.587-.01l.1-.065l-6.353-3.813v3.889c-2.213-.005-4.427 0-6.64 0a2 2 0 0 0-.251.021c-.212-.032-.427-.016-.642-.016c-.284.006-.57-.015-.853.015a2 2 0 0 0-.253-.022H3.275c.006-3.635 0-7.27 0-10.9a4.8 4.8 0 0 1 .071-1.131A2.54 2.54 0 0 1 4.5 2.375m.54 2.188a.64.64 0 0 0-.489.606v1.316a.64.64 0 0 0 .627.6h1.309a.64.64 0 0 0 .6-.608c.005-.426 0-.853 0-1.279a.64.64 0 0 0-.329-.575a.7.7 0 0 0-.358-.078H5.264a1 1 0 0 0-.224.018m20.341 0a.64.64 0 0 0-.474.607v1.306a.64.64 0 0 0 .6.608c.436.005.872 0 1.308 0a.64.64 0 0 0 .627-.606c.006-.438 0-.876 0-1.314a.64.64 0 0 0-.594-.621c-.348-.009-.7 0-1.045 0a2 2 0 0 0-.422.024ZM5.042 8.38a.64.64 0 0 0-.452.4a1.1 1.1 0 0 0-.041.386v1.135a.637.637 0 0 0 .6.6c.4.008.795 0 1.192 0a.8.8 0 0 0 .395-.065a.64.64 0 0 0 .36-.574V8.983a.64.64 0 0 0-.6-.624c-.338-.008-.677 0-1.016 0a2.4 2.4 0 0 0-.438.021m20.338 0a.64.64 0 0 0-.433.4a1.1 1.1 0 0 0-.041.387v1.131a.64.64 0 0 0 .608.607c.406.006.812 0 1.218 0a.74.74 0 0 0 .38-.078a.64.64 0 0 0 .335-.558V8.985a.637.637 0 0 0-.593-.622c-.349-.009-.7 0-1.047 0a2 2 0 0 0-.428.022ZM5.041 12.2a.64.64 0 0 0-.491.608c0 .438-.005.877 0 1.315a.64.64 0 0 0 .627.6h1.3a.64.64 0 0 0 .608-.607v-1.307a.64.64 0 0 0-.605-.626c-.348-.007-.7 0-1.045 0a2 2 0 0 0-.394.017m20.335.006a.64.64 0 0 0-.43.394a1.1 1.1 0 0 0-.041.386v1.131a.64.64 0 0 0 .607.608c.435.005.87 0 1.306 0a.64.64 0 0 0 .628-.605c.006-.438 0-.876 0-1.313a.637.637 0 0 0-.592-.622c-.349-.01-.7 0-1.046 0a2 2 0 0 0-.431.015Z"/><path fill="#fff" d="M13.458 12.1q3.176 1.908 6.353 3.813l-.1.065q-3.127 1.872-6.251 3.748V15.99q-.003-1.941-.002-3.89"/><path fill="#c0392b" d="M3.275 15.988h1.541a2 2 0 0 1 .253.022a.65.65 0 0 0-.409.273a.9.9 0 0 0-.111.518v1.045a.686.686 0 0 0 .685.689h1.192a.68.68 0 0 0 .657-.569c.008-.447 0-.9 0-1.343a.63.63 0 0 0-.521-.613a2 2 0 0 1 .251-.021h6.64v3.736q3.127-1.871 6.251-3.748c.2.018.391.007.587.01h4.741a2.3 2.3 0 0 1 .4.02a.64.64 0 0 0-.475.385a1 1 0 0 0-.045.407v1.014a.677.677 0 0 0 .691.719h1.16a.684.684 0 0 0 .686-.658v-.984a1.07 1.07 0 0 0-.1-.589a.64.64 0 0 0-.42-.292a2 2 0 0 1 .22-.022h1.573c.011 1.721 0 3.443 0 5.165v6.293a2.57 2.57 0 0 1-.612 1.651a2.54 2.54 0 0 1-1.563.868a8 8 0 0 1-.981.033H6.724a9 9 0 0 1-1.368-.048a2.56 2.56 0 0 1-2-1.846a2.8 2.8 0 0 1-.09-.746V16.891c.007-.3-.002-.602.009-.903m1.795 3.843a.6.6 0 0 0-.386.234a.76.76 0 0 0-.134.464v1.162a.685.685 0 0 0 .686.658h1.283a.687.687 0 0 0 .568-.716v-1.191a.62.62 0 0 0-.2-.458a.7.7 0 0 0-.487-.17H5.264a1 1 0 0 0-.194.017m20.366 0a.62.62 0 0 0-.522.58v1.222a.676.676 0 0 0 .69.718c.428 0 .857.005 1.285 0a.7.7 0 0 0 .562-.684v-1.013a.9.9 0 0 0-.123-.568a.66.66 0 0 0-.532-.267h-.954a2.3 2.3 0 0 0-.406.011ZM5.071 23.648a.6.6 0 0 0-.4.249a.77.77 0 0 0-.124.448v1.132a.7.7 0 0 0 .6.688h1.376a.687.687 0 0 0 .567-.717v-1.189a.62.62 0 0 0-.2-.459a.7.7 0 0 0-.484-.17H5.265a1 1 0 0 0-.194.018m20.362 0a.62.62 0 0 0-.492.427a1 1 0 0 0-.029.33V25.6a.68.68 0 0 0 .656.567h1.193a.69.69 0 0 0 .686-.719v-1.164a.66.66 0 0 0-.286-.545a1.46 1.46 0 0 0-.787-.11c-.312.01-.629-.02-.94.018Z"/></svg>
|
||
`,
|
||
default_file: `
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><g fill="none"><path fill="url(#SVGsXCcfeOf)" 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"/><path fill="url(#SVGUVHt30XG)" fill-opacity=".5" 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"/><path fill="url(#SVG176kqd6n)" d="M8 4.5V1l5 5H9.5A1.5 1.5 0 0 1 8 4.5"/><defs><linearGradient id="SVGsXCcfeOf" x1="9.994" x2="11.264" y1="1" y2="12.781" gradientUnits="userSpaceOnUse"><stop stop-color="#6CE0FF"/><stop offset="1" stop-color="#4894FE"/></linearGradient><linearGradient id="SVG176kqd6n" x1="10.492" x2="9.242" y1="3.083" y2="5.167" gradientUnits="userSpaceOnUse"><stop stop-color="#9FF0F9"/><stop offset="1" stop-color="#B3E0FF"/></linearGradient><radialGradient id="SVGUVHt30XG" cx="0" cy="0" r="1" gradientTransform="rotate(130.372 6.372 3.818)scale(8.35524 4.87457)" gradientUnits="userSpaceOnUse"><stop offset=".362" stop-color="#4A43CB"/><stop offset="1" stop-color="#4A43CB" stop-opacity="0"/></radialGradient></defs></g></svg>
|
||
`
|
||
},
|
||
|
||
// =====================
|
||
// 生成唯一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 = `
|
||
<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.download}
|
||
</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>
|
||
`;
|
||
|
||
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 = `<img src="${file.url}" alt="${file.name}" class="file-img" />`;
|
||
} else {
|
||
content = this.getFileIcon(file.name);
|
||
}
|
||
|
||
// 未上传标记
|
||
const pendingBadge = file.uploaded ? '' : `
|
||
<div class="file-pending-badge">
|
||
${this.language.pending}
|
||
</div>
|
||
`;
|
||
|
||
return `
|
||
<div class="file-item" data-id="${file._id}">
|
||
<input type="checkbox" class="file-checkbox" />
|
||
<div class="file-preview">${content}</div>
|
||
${pendingBadge}
|
||
|
||
<div class="file-info">
|
||
<span class="file-name">${file.name}</span>
|
||
<span>${this.formatFileSize(file.size)}</span>
|
||
<span>${this.formatDate(file.uploadDate)}</span>
|
||
</div>
|
||
|
||
<div class="file-actions">
|
||
<div class="file-action download ${downloadDisabled ? 'is-disabled' : ''}">
|
||
${this.staticIconMap.download}
|
||
</div>
|
||
|
||
<div class="file-action delete ${deleteDisabled ? 'is-disabled' : ''}">
|
||
${this.staticIconMap.delete}
|
||
</div>
|
||
|
||
<div class="file-action more">
|
||
${this.staticIconMap.more}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
},
|
||
|
||
// =====================
|
||
// 绑定文件项事件
|
||
// =====================
|
||
bindFileItemEvents: function () {
|
||
var self = this;
|
||
|
||
// =====================
|
||
// 文件item点击事件
|
||
// =====================
|
||
this.$fileList.off('click', '.file-item')
|
||
.on('click', '.file-item', function (e) {
|
||
var $checkbox = $(this).find('.file-checkbox');
|
||
$checkbox.prop('checked', !$checkbox.is(':checked'));
|
||
$checkbox.trigger('change');
|
||
});
|
||
|
||
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);
|
||
},
|
||
};
|
||
|
||
// ==========================
|
||
// 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 = Object.create(G3File);
|
||
instance.init(this.id, options);
|
||
|
||
$this.data('G3File', instance);
|
||
|
||
// ========= 方法调用 =========
|
||
} else if (typeof options === 'string') {
|
||
var method = options;
|
||
|
||
if (instance[method]) {
|
||
instance[method].apply(instance, args);
|
||
}
|
||
}
|
||
});
|
||
|
||
// ⭐关键:返回 instance(支持 file.xxx)
|
||
return instance || this;
|
||
};
|
||
|
||
})(jQuery); |