u
This commit is contained in:
1 parent
e3d765a9d4
commit
04491e14a3
977 files changed
+67280
No files matched your search
@@ -0,0 +1,908 @@
|
||||
(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);
|
||||
Reference in new issue
Block a user