Files
workspace/code/file-preview/custom-file/index.js
T
2026-04-25 17:28:57 +08:00

908 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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);