u
This commit is contained in:
1 parent
e3d765a9d4
commit
04491e14a3
977 files changed
+67280
No files matched your search
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><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>
|
||||
|
After Width: | Height: | Size: 466 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><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>
|
||||
|
After Width: | Height: | Size: 405 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M12 15V3m9 12v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10l5 5l5-5"/></g></svg>
|
||||
|
After Width: | Height: | Size: 386 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><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>
|
||||
|
After Width: | Height: | Size: 368 B |
@@ -0,0 +1,324 @@
|
||||
/* 整体布局 */
|
||||
.g3-file-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.g3-file-container .header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
padding: 10px 14px;
|
||||
|
||||
background: #fafbfc;
|
||||
border-bottom: 1px solid #eef0f2;
|
||||
}
|
||||
|
||||
.g3-file-container .main {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
overflow: auto;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
align-items: flex-start;
|
||||
/* justify-content: center; */
|
||||
}
|
||||
|
||||
.g3-file-container .main > * {
|
||||
margin: 7px;
|
||||
}
|
||||
|
||||
.g3-file-container .empty-tip {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.g3-file-container .footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
padding: 10px 14px;
|
||||
|
||||
background: #fafbfc;
|
||||
border-top: 1px solid #eef0f2;
|
||||
}
|
||||
|
||||
.g3-file-container .header,
|
||||
.g3-file-container .footer {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footer div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer .pending-count {
|
||||
margin-right: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.footer .pending-count .pending-number {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* 全选 */
|
||||
.g3-file-container .check-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 12px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.g3-file-container .check-all input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.g3-file-container .check-all span {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.g3-file-container button {
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
background: #165dff;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
width: auto;
|
||||
|
||||
height: 28px;
|
||||
line-height: 1;
|
||||
padding: 0 15px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.g3-file-container button svg {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.g3-file-container button + button {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.g3-file-container button:hover:not(:disabled) {
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
.g3-file-container button:active:not(:disabled) {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.g3-file-container button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.g3-file-container .delete-all {
|
||||
background: #ff4d4f;
|
||||
}
|
||||
|
||||
.g3-file-container .file-item {
|
||||
width: calc(50% - 20px);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.18s ease;
|
||||
|
||||
border: 1px solid #eef0f2;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.g3-file-container .file-item:hover {
|
||||
border-color: #d6e4ff;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.file-item .file-preview {
|
||||
height: 120px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-item .file-preview svg,
|
||||
.file-item .file-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.file-item .file-checkbox {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.file-item .file-info {
|
||||
padding: 10px 10px 12px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.file-item .file-info span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.file-item .file-info .file-name {
|
||||
font-size: 13px;
|
||||
color: #1f2937;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.file-item .file-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.file-item .file-action {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 6px;
|
||||
|
||||
/* 👇 稍微更明显 */
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
|
||||
margin-left: 6px;
|
||||
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* hover:明显增强 */
|
||||
.file-item .file-action:hover {
|
||||
background: rgba(22, 93, 255, 0.12);
|
||||
color: #165dff;
|
||||
}
|
||||
|
||||
/* active:点击反馈 */
|
||||
.file-item .file-action:active {
|
||||
background: rgba(22, 93, 255, 0.18);
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.file-item .file-action.is-disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.g3-download-modal {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.g3-download-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.g3-download-box {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
width: 260px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.g3-download-bar {
|
||||
height: 6px;
|
||||
background: #eee;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.g3-download-bar-inner {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: #4caf50;
|
||||
transition: width 0.2s;
|
||||
}
|
||||
|
||||
/* 文件项选中样式 */
|
||||
.g3-file-container .file-item.file-item-selected {
|
||||
border-color: #165dff;
|
||||
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.2);
|
||||
background: rgba(22, 93, 255, 0.05);
|
||||
}
|
||||
|
||||
.g3-file-container .file-item.file-item-selected:hover {
|
||||
border-color: #165dff;
|
||||
box-shadow: 0 6px 18px rgba(22, 93, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 未上传标记 */
|
||||
.file-item .file-pending-badge {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,3 @@
|
||||
(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)});
|
||||
|
||||
//# sourceMappingURL=FileSaver.min.js.map
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/*!
|
||||
|
||||
JSZip v3.10.1 - A JavaScript class for generating and reading zip files
|
||||
<http://stuartk.com/jszip>
|
||||
|
||||
(c) 2009-2016 Stuart Knightley <stuart [at] stuartk.com>
|
||||
Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown.
|
||||
|
||||
JSZip uses the library pako released under the MIT license :
|
||||
https://github.com/nodeca/pako/blob/main/LICENSE
|
||||
*/
|
||||
|
||||
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=e()}}(function(){return function s(a,o,h){function u(r,e){if(!o[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(l)return l(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var i=o[r]={exports:{}};a[r][0].call(i.exports,function(e){var t=a[r][1][e];return u(t||e)},i,i.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,e=0;e<h.length;e++)u(h[e]);return u}({1:[function(e,t,r){"use strict";var d=e("./utils"),c=e("./support"),p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";r.encode=function(e){for(var t,r,n,i,s,a,o,h=[],u=0,l=e.length,f=l,c="string"!==d.getTypeOf(e);u<e.length;)f=l-u,n=c?(t=e[u++],r=u<l?e[u++]:0,u<l?e[u++]:0):(t=e.charCodeAt(u++),r=u<l?e.charCodeAt(u++):0,u<l?e.charCodeAt(u++):0),i=t>>2,s=(3&t)<<4|r>>4,a=1<f?(15&r)<<2|n>>6:64,o=2<f?63&n:64,h.push(p.charAt(i)+p.charAt(s)+p.charAt(a)+p.charAt(o));return h.join("")},r.decode=function(e){var t,r,n,i,s,a,o=0,h=0,u="data:";if(e.substr(0,u.length)===u)throw new Error("Invalid base64 input, it looks like a data url.");var l,f=3*(e=e.replace(/[^A-Za-z0-9+/=]/g,"")).length/4;if(e.charAt(e.length-1)===p.charAt(64)&&f--,e.charAt(e.length-2)===p.charAt(64)&&f--,f%1!=0)throw new Error("Invalid base64 input, bad content length.");for(l=c.uint8array?new Uint8Array(0|f):new Array(0|f);o<e.length;)t=p.indexOf(e.charAt(o++))<<2|(i=p.indexOf(e.charAt(o++)))>>4,r=(15&i)<<4|(s=p.indexOf(e.charAt(o++)))>>2,n=(3&s)<<6|(a=p.indexOf(e.charAt(o++))),l[h++]=t,64!==s&&(l[h++]=r),64!==a&&(l[h++]=n);return l}},{"./support":30,"./utils":32}],2:[function(e,t,r){"use strict";var n=e("./external"),i=e("./stream/DataWorker"),s=e("./stream/Crc32Probe"),a=e("./stream/DataLengthProbe");function o(e,t,r,n,i){this.compressedSize=e,this.uncompressedSize=t,this.crc32=r,this.compression=n,this.compressedContent=i}o.prototype={getContentWorker:function(){var e=new i(n.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),t=this;return e.on("end",function(){if(this.streamInfo.data_length!==t.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),e},getCompressedWorker:function(){return new i(n.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(e,t,r){return e.pipe(new s).pipe(new a("uncompressedSize")).pipe(t.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",t)},t.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,t,r){"use strict";var n=e("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(){return new n("STORE compression")},uncompressWorker:function(){return new n("STORE decompression")}},r.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,t,r){"use strict";var n=e("./utils");var o=function(){for(var e,t=[],r=0;r<256;r++){e=r;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[r]=e}return t}();t.exports=function(e,t){return void 0!==e&&e.length?"string"!==n.getTypeOf(e)?function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t[a])];return-1^e}(0|t,e,e.length,0):function(e,t,r,n){var i=o,s=n+r;e^=-1;for(var a=n;a<s;a++)e=e>>>8^i[255&(e^t.charCodeAt(a))];return-1^e}(0|t,e,e.length,0):0}},{"./utils":32}],5:[function(e,t,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(e,t,r){"use strict";var n=null;n="undefined"!=typeof Promise?Promise:e("lie"),t.exports={Promise:n}},{lie:37}],7:[function(e,t,r){"use strict";var n="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,i=e("pako"),s=e("./utils"),a=e("./stream/GenericWorker"),o=n?"uint8array":"array";function h(e,t){a.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=t,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,e.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new i[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var t=this;this._pako.onData=function(Line truncated
|
||||
Reference in new issue
Block a user