diff --git a/code/app/app-rn4/src/app/money/(tabs)/_layout.tsx b/code/app/app-rn4/src/app/finance/(tabs)/_layout.tsx similarity index 100% rename from code/app/app-rn4/src/app/money/(tabs)/_layout.tsx rename to code/app/app-rn4/src/app/finance/(tabs)/_layout.tsx diff --git a/code/app/app-rn4/src/app/money/(tabs)/add/index.tsx b/code/app/app-rn4/src/app/finance/(tabs)/add/index.tsx similarity index 100% rename from code/app/app-rn4/src/app/money/(tabs)/add/index.tsx rename to code/app/app-rn4/src/app/finance/(tabs)/add/index.tsx diff --git a/code/app/app-rn4/src/app/money/(tabs)/ana/index.tsx b/code/app/app-rn4/src/app/finance/(tabs)/ana/index.tsx similarity index 100% rename from code/app/app-rn4/src/app/money/(tabs)/ana/index.tsx rename to code/app/app-rn4/src/app/finance/(tabs)/ana/index.tsx diff --git a/code/app/app-rn4/src/app/money/(tabs)/home/index.tsx b/code/app/app-rn4/src/app/finance/(tabs)/home/index.tsx similarity index 100% rename from code/app/app-rn4/src/app/money/(tabs)/home/index.tsx rename to code/app/app-rn4/src/app/finance/(tabs)/home/index.tsx diff --git a/code/app/app-rn4/src/app/money/sub/index.tsx b/code/app/app-rn4/src/app/finance/sub/index.tsx similarity index 100% rename from code/app/app-rn4/src/app/money/sub/index.tsx rename to code/app/app-rn4/src/app/finance/sub/index.tsx diff --git a/code/app/app-rn4/src/configs/pages.ts b/code/app/app-rn4/src/configs/pages.ts index 3ffaac38..405f7187 100644 --- a/code/app/app-rn4/src/configs/pages.ts +++ b/code/app/app-rn4/src/configs/pages.ts @@ -23,7 +23,7 @@ export type AppListItem = HeaderItem | GroupItem; export const SUB_APPS: SubAppItem[] = [ { - id: "/money/home", + id: "/finance/home", title: "记账", icon: IconAppFinance, group: "J", diff --git a/code/app/app-rn4/text.html b/code/app/app-rn4/text.html index 70d423f6..7039ad61 100644 --- a/code/app/app-rn4/text.html +++ b/code/app/app-rn4/text.html @@ -1,272 +1,264 @@ - - - 家庭系统 Pro Demo + +附件组件(最终版) - + +/* card */ +.card { + position: relative; + background: #fff; + border-radius: 10px; + border: 1px solid transparent; + display: flex; + flex-direction: column; + cursor: pointer; +} + +.card:hover { + border-color: #1677ff; +} + +.card.selected { + background: #eaf2ff; + border-color: #1677ff; +} + +/* checkbox(左上) */ +.check { + position: absolute; + top: 6px; + left: 6px; + z-index: 5; +} + +/* 状态(右上) */ +.status { + position: absolute; + top: 6px; + right: 6px; + background: #ff4d4f; + color: #fff; + font-size: 10px; + padding: 2px 5px; + border-radius: 3px; +} + +/* thumb */ +.thumb { + height: 90px; + background: #f0f2f5; + display:flex; + align-items:center; + justify-content:center; +} + +.thumb img { + width:100%; + height:100%; + object-fit:cover; +} + +/* info */ +.info { + padding: 8px; +} + +.name { + font-size: 13px; +} + +.meta { + font-size: 12px; + color: #888; +} + +/* actions */ +.actions { + display: flex; + border-top: 1px solid #eee; +} + +.actions button { + flex: 1; + border: none; + background: #fff; + padding: 6px 0; + cursor: pointer; + font-size: 12px; +} + +.actions button:hover { + background: #f5f6f8; +} + +.danger { + color: #ff4d4f; +} + +.more-menu { + position: absolute; + bottom: 35px; + right: 6px; + background: #fff; + border: 1px solid #eee; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + display: none; +} + +.more-menu div { + padding: 6px 10px; + cursor: pointer; +} + +.more-menu div:hover { + background: #f5f6f8; +} + - + -
+
- -
-
+ +function deleteFile(id) { + if (!confirm("确定删除?")) return; + const i = files.findIndex(f => f.id === id); + if (i > -1) files.splice(i, 1); + selected.delete(id); + render(); +} + +render(); + \ No newline at end of file diff --git a/code/file-preview/custom-file/index.css b/code/file-preview/custom-file/index.css index 3c9d611f..7c1294b1 100644 --- a/code/file-preview/custom-file/index.css +++ b/code/file-preview/custom-file/index.css @@ -322,3 +322,15 @@ border-radius: 10px; z-index: 1; } + +/* 预览状态样式 */ +.g3-file-container .file-item.file-item-preview { + border-color: #4caf50; + box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2); + background: rgba(76, 175, 80, 0.05); +} + +.g3-file-container .file-item.file-item-preview:hover { + border-color: #4caf50; + box-shadow: 0 6px 18px rgba(76, 175, 80, 0.2); +} diff --git a/code/file-preview/custom-file/index.js b/code/file-preview/custom-file/index.js index c8dc4cb0..c89c302d 100644 --- a/code/file-preview/custom-file/index.js +++ b/code/file-preview/custom-file/index.js @@ -1,9 +1,10 @@ (function ($) { - 'use strict'; + "use strict"; var G3File = { files: [], selectedFiles: new Set(), + previewFiles: new Set(), loadScripts: function (items, callback) { var self = this; @@ -25,7 +26,7 @@ // 防重复加载 var key = item.key; - if (window[key + '_loading']) { + if (window[key + "_loading"]) { var timer = setInterval(function () { if (item.check && item.check()) { clearInterval(timer); @@ -35,19 +36,19 @@ return; } - window[key + '_loading'] = true; + window[key + "_loading"] = true; - var script = document.createElement('script'); + var script = document.createElement("script"); script.src = item.src; script.onload = function () { - window[key + '_loading'] = false; + window[key + "_loading"] = false; done(); }; script.onerror = function () { - window[key + '_loading'] = false; - console.error(key + ' load failed'); + window[key + "_loading"] = false; + console.error(key + " load failed"); done(); }; @@ -61,36 +62,42 @@ init: function (containerId, options) { var self = this; - this.$container = $('#' + containerId); + this.$container = $("#" + containerId); this.options = $.extend({}, this.defaults, options); - this.language = this.languages[this.options.language] || this.languages['zh-CN']; + this.language = + this.languages[this.options.language] || this.languages["zh-CN"]; this.files = this.options.data || []; + this.selectedFiles = new Set(); // 选中文件集合 + this.previewFiles = new Set(); // 预览文件集合 - this.loadScripts([ - { - key: 'fileSaver', - src: '/custom-file/libs/FileSaver.min.js', - check: function () { - return typeof window.saveAs === 'function'; + 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); } }, - { - 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; }, @@ -104,56 +111,57 @@ onDownload: null, onSelect: null, onUpload: null, - language: 'zh-CN', + onPreview: null, + language: "zh-CN", maxFileSize: 10 * 1024 * 1024, // 默认最大10MB permissions: { select: true, upload: true, download: true, delete: true, - view: 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: '文件大小超过限制' + "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", }, - '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' - } }, // ===================== @@ -183,29 +191,29 @@ `, default_file: ` - ` + `, }, // ===================== // 生成唯一ID // ===================== generateUniqueId: function () { - return 'file_' + Math.random().toString(36).slice(2) + Date.now(); + 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']; + 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.language = this.languages[language] || this.languages["zh-CN"]; this.render(); return this; }, @@ -259,15 +267,15 @@ this.$container.html(html); - this.$fileList = this.$container.find('.main'); - this.$emptyTip = this.$container.find('.empty-tip'); - this.$checkAll = this.$container.find('.check-all-checkbox'); - this.$btnDownload = this.$container.find('.download-all'); - this.$btnDelete = this.$container.find('.delete-all'); - this.$btnUpload = this.$container.find('.upload'); - this.$btnSelect = this.$container.find('.select'); - this.$fileInput = this.$container.find('.file-input'); - this.$pendingCount = this.$container.find('.pending-number'); + this.$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; }, @@ -276,8 +284,8 @@ var self = this; // 选择 - this.$checkAll.off('change').on('change', function () { - var checked = $(this).is(':checked'); + this.$checkAll.off("change").on("change", function () { + var checked = $(this).is(":checked"); self.selectedFiles.clear(); @@ -287,20 +295,20 @@ }); } - self.$fileList.find('.file-checkbox').prop('checked', checked); + self.$fileList.find(".file-checkbox").prop("checked", checked); self.updateToolbarState(); }); // 批量下载 - this.$btnDownload.off('click').on('click', function () { + 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') { + if (typeof self.options?.onDownloadBatch === "function") { self.options.onDownloadBatch(files); return; } @@ -309,7 +317,7 @@ }); // 批量删除 - this.$btnDelete.off('click').on('click', function () { + this.$btnDelete.off("click").on("click", function () { var idSet = self.selectedFiles || new Set(); if (idSet.size === 0) return; @@ -321,7 +329,7 @@ // ===================== // 1. 优先走外部回调(如果你要接后端) // ===================== - if (typeof self.options?.onDeleteBatch === 'function') { + if (typeof self.options?.onDeleteBatch === "function") { self.options.onDeleteBatch(deleteFiles, function (success) { if (!success) return; @@ -340,17 +348,17 @@ // ===================== // 选择按钮点击事件(选择本地文件) // ===================== - this.$btnSelect.off('click').on('click', function () { + this.$btnSelect.off("click").on("click", function () { self.$fileInput.click(); }); // ===================== // 上传按钮点击事件(交给外部处理) // ===================== - this.$btnUpload.off('click').on('click', function () { + this.$btnUpload.off("click").on("click", function () { // 获取所有未上传的文件 - const pendingFiles = self.files.filter(file => !file.uploaded); - if (typeof self.options.onUpload === 'function') { + const pendingFiles = self.files.filter((file) => !file.uploaded); + if (typeof self.options.onUpload === "function") { self.options.onUpload(pendingFiles); } }); @@ -358,7 +366,7 @@ // ===================== // 文件选择事件 // ===================== - this.$fileInput.off('change').on('change', function (e) { + this.$fileInput.off("change").on("change", function (e) { var files = e.target.files; if (!files || files.length === 0) return; @@ -376,15 +384,15 @@ } if (invalidFiles.length > 0) { - var errorMsg = self.language.fileSizeError || '文件大小超过限制'; - alert(errorMsg + ': ' + invalidFiles.map(f => f.name).join(', ')); + 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 = ''; + let url = ""; if (self.isImageFile(file.name)) { url = URL.createObjectURL(file); } @@ -396,7 +404,7 @@ uploadDate: new Date().toISOString(), uploaded: false, // 标记为未上传 url: url, // 图片文件使用本地临时连接,其他文件为空 - file: file // 保存原始文件对象 + file: file, // 保存原始文件对象 }; }); @@ -405,13 +413,13 @@ self.renderFileList(); self.updateToolbarState(); - if (typeof self.options.onSelect === 'function') { + if (typeof self.options.onSelect === "function") { self.options.onSelect(validFiles); } } // 重置文件输入框,以便可以重复选择相同的文件 - self.$fileInput.val(''); + self.$fileInput.val(""); }); }, @@ -419,10 +427,10 @@ // 文件判断 // ===================== isImageFile: function (filename) { - if (!filename || typeof filename !== 'string') return false; + 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']); + const ext = filename.slice(filename.lastIndexOf(".") + 1).toLowerCase(); + const imageExts = new Set(["jpg", "jpeg", "png", "gif", "bmp", "webp"]); return imageExts.has(ext); }, @@ -433,33 +441,31 @@ updateToolbarState: function () { var hasSelection = this.selectedFiles.size > 0; - this.$btnDownload.prop('disabled', !hasSelection); - this.$btnDelete.prop('disabled', !hasSelection); + this.$btnDownload.prop("disabled", !hasSelection); + this.$btnDelete.prop("disabled", !hasSelection); var allSelected = - this.files.length > 0 && - this.selectedFiles.size === this.files.length; + this.files.length > 0 && this.selectedFiles.size === this.files.length; - this.$checkAll.prop('checked', allSelected); + this.$checkAll.prop("checked", allSelected); // 更新未上传文件数量 - var pendingCount = this.files.filter(file => !file.uploaded).length; + 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'); + 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); + $(this).find(".file-checkbox").prop("checked", isSelected); // 添加或移除选中样式 if (isSelected) { - $(this).addClass('file-item-selected'); + $(this).addClass("file-item-selected"); } else { - $(this).removeClass('file-item-selected'); + $(this).removeClass("file-item-selected"); } }); }, @@ -473,7 +479,7 @@ this.files = files.map(function (file) { return Object.assign({}, file, { _id: self.generateUniqueId(), - uploaded: true + uploaded: true, }); }); @@ -498,7 +504,7 @@ this.$fileList.show(); this.$emptyTip.hide(); - var html = ''; + var html = ""; $.each(this.files, function (_, file) { html += self.createFileItem(file); }); @@ -512,38 +518,50 @@ // ===================== formatFileSize: function (bytes) { if (bytes === null || bytes === undefined || isNaN(bytes) || bytes <= 0) { - return '-'; + return "-"; } var k = 1024; - var sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + 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]; + return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i]; }, // ===================== // 格式化日期 // ===================== formatDate: function (dateStr) { - if (!dateStr) return '-'; + if (!dateStr) return "-"; var date = new Date(dateStr); if (isNaN(date.getTime())) { - return '-'; + 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); + 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; + return ( + year + + "-" + + month + + "-" + + day + + " " + + hours + + ":" + + minutes + + ":" + + seconds + ); }, // ===================== @@ -555,7 +573,7 @@ const downloadDisabled = !perm.download; const deleteDisabled = !perm.delete; - let content = ''; + let content = ""; if (this.isImageFile(file.name)) { content = `${file.name}`; @@ -564,15 +582,23 @@ } // 未上传标记 - const pendingBadge = file.uploaded ? '' : ` + const pendingBadge = file.uploaded + ? "" + : `
${this.language.pending}
`; + // 预览状态(使用class标识) + const isPreview = this.previewFiles.has(file._id); + + // 选中状态 + const isSelected = this.selectedFiles.has(file._id); + return ` -
- +
+
${content}
${pendingBadge} @@ -583,11 +609,11 @@
-
+
${this.staticIconMap.download}
-
+
${this.staticIconMap.delete}
@@ -606,26 +632,57 @@ var self = this; // ===================== - // 文件item点击事件 + // 文件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", ".file-item") + .on("click", ".file-item", function (e) { + var target = $(e.target); + // 如果点击的是 checkbox、download、delete、more 按钮,则不执行预览逻辑 + if ( + target.is(".file-checkbox") || + target.closest(".file-action").length > 0 + ) { + return; + } - 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); + var id = $(this).data("id"); + var file = self.files.find((f) => f._id === id); if (!file) return; - if (typeof self.options.onDownload === 'function') { + // 切换预览状态(只允许同时预览一个文件) + var isPreview = self.previewFiles.has(id); + if (isPreview) { + // 取消预览 + self.previewFiles.delete(id); + } else { + // 开启预览前,先清除其他文件的预览状态 + self.previewFiles.clear(); + self.previewFiles.add(id); + } + + // 重新渲染文件列表以更新预览状态 + self.renderFileList(); + + // 调用外部回调,传递文件和预览状态 + if (typeof self.options.onPreview === "function") { + self.options.onPreview(file, !isPreview); + } + }); + + 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; } @@ -633,11 +690,12 @@ self.internalDownload(file); }); - this.$fileList.off('change', '.file-checkbox') - .on('change', '.file-checkbox', function () { - var id = $(this).closest('.file-item').data('id'); + this.$fileList + .off("change", ".file-checkbox") + .on("change", ".file-checkbox", function () { + var id = $(this).closest(".file-item").data("id"); - if ($(this).is(':checked')) { + if ($(this).is(":checked")) { self.selectedFiles.add(id); } else { self.selectedFiles.delete(id); @@ -646,17 +704,18 @@ self.updateToolbarState(); }); - this.$fileList.off('click', '.delete') - .on('click', '.delete', function (e) { + 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); + 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') { + if (typeof self.options.onDelete === "function") { self.options.onDelete([file]); return; } @@ -667,11 +726,10 @@ // ===================== // 更多按钮点击事件(阻止冒泡) // ===================== - this.$fileList.off('click', '.more') - .on('click', '.more', function (e) { - e.preventDefault(); - e.stopPropagation(); - }); + this.$fileList.off("click", ".more").on("click", ".more", function (e) { + e.preventDefault(); + e.stopPropagation(); + }); }, getUniqueFileName: function (fileName, files) { @@ -683,8 +741,13 @@ // 如果文件名已经存在,则在文件名后加上 (1), (2), ... while (files.has(uniqueFileName)) { // 基于原始文件名为基础来添加计数器 - const fileNameWithoutExtension = originalFileName.replace(/\.[^/.]+$/, ""); - const extension = originalFileName.slice(originalFileName.lastIndexOf(".")); + const fileNameWithoutExtension = originalFileName.replace( + /\.[^/.]+$/, + "", + ); + const extension = originalFileName.slice( + originalFileName.lastIndexOf("."), + ); uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}`; counter++; } @@ -694,13 +757,13 @@ getFileBlob: async function (url) { if (!url) { - throw new Error('无效的文件URL'); + throw new Error("无效的文件URL"); } const res = await fetch(url); if (!res.ok) { - throw new Error('网络请求失败,状态码:' + res.status); + throw new Error("网络请求失败,状态码:" + res.status); } // ⭐ 比 blob 更轻一点(后续 zip 更快) @@ -714,11 +777,11 @@ // ===================== internalDownload: async function (file) { if (!file) { - console.warn('下载失败:文件对象无效', file); + console.warn("下载失败:文件对象无效", file); return; } - console.log('开始下载文件:', file.name); + console.log("开始下载文件:", file.name); try { let blob; @@ -730,14 +793,13 @@ // 未上传的文件,使用本地文件对象 blob = file.file; } else { - console.warn('下载失败:文件没有URL或本地文件对象', file); + console.warn("下载失败:文件没有URL或本地文件对象", file); return; } - saveAs(blob, file.name || '未命名文件'); - + saveAs(blob, file.name || "未命名文件"); } catch (err) { - console.error('文件下载失败:', err); + console.error("文件下载失败:", err); } }, @@ -766,7 +828,7 @@ // 未上传的文件,使用本地文件对象 content = await item.file.arrayBuffer(); } else { - console.warn('跳过失败文件:', item.name, '没有URL或本地文件对象'); + console.warn("跳过失败文件:", item.name, "没有URL或本地文件对象"); failedCount++; return; } @@ -776,9 +838,8 @@ zip.file(uniqueFileName, content); addedFiles.add(uniqueFileName); - } catch (err) { - console.warn('跳过失败文件:', item.name, err); + console.warn("跳过失败文件:", item.name, err); failedCount++; } finally { current++; @@ -793,14 +854,12 @@ }; try { - await Promise.all( - Array.from({ length: concurrency }, runner) - ); + 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 pad = (n) => (n < 10 ? "0" + n : n); const fileName = now.getFullYear() + @@ -812,11 +871,10 @@ saveAs(blob, fileName + ".zip"); console.log( - `下载完成:${total - failedCount}/${total},失败:${failedCount}` + `下载完成:${total - failedCount}/${total},失败:${failedCount}`, ); callback && callback(); - } catch (err) { console.error("Batch download failed:", err); callback && callback(); @@ -833,10 +891,10 @@ try { URL.revokeObjectURL(file.url); } catch (e) { - console.warn('Failed to revoke object URL:', e); + console.warn("Failed to revoke object URL:", e); } } - this.files = this.files.filter(f => f._id !== file._id); + this.files = this.files.filter((f) => f._id !== file._id); this.selectedFiles.delete(file._id); this.updateToolbarState(); this.renderFileList(); @@ -847,7 +905,7 @@ // ===================== internalDeleteBatch: function (files) { if (!files || files.length === 0) return; - files.forEach(file => this.internalDelete(file)); + files.forEach((file) => this.internalDelete(file)); this.updateToolbarState(); this.renderFileList(); }, @@ -859,16 +917,92 @@ 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.$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.download") + .toggleClass("is-disabled", !permissions.download); - this.$fileList.find('.file-action.delete') - .toggleClass('is-disabled', !permissions.delete); + this.$fileList + .find(".file-action.delete") + .toggleClass("is-disabled", !permissions.delete); + }, + + // ===================== + // 外部方法:切换文件选中状态 + // 参数:fileId - 文件ID,isSelected - 是否选中(可选,不传则切换) + // ===================== + toggleSelect: function (fileId, isSelected) { + var file = this.files.find((f) => f._id === fileId); + if (!file) return; + + var currentSelected = this.selectedFiles.has(fileId); + var newSelected = + isSelected !== undefined ? isSelected : !currentSelected; + + if (newSelected) { + this.selectedFiles.add(fileId); + } else { + this.selectedFiles.delete(fileId); + } + + this.updateToolbarState(); + this.renderFileList(); + + return newSelected; + }, + + // ===================== + // 外部方法:获取当前选中的文件列表 + // ===================== + getSelectedFiles: function () { + return this.files.filter((f) => this.selectedFiles.has(f._id)); + }, + + // ===================== + // 外部方法:获取当前预览的文件列表 + // ===================== + getPreviewFiles: function () { + return this.files.filter((f) => this.previewFiles.has(f._id)); + }, + + // ===================== + // 外部方法:获取所有文件列表 + // ===================== + getFiles: function () { + return this.files; + }, + + // ===================== + // 外部方法:切换文件预览状态 + // 参数:fileId - 文件ID,isPreview - 是否预览(可选,不传则切换) + // ===================== + togglePreview: function (fileId, isPreview) { + var file = this.files.find((f) => f._id === fileId); + if (!file) return; + + var currentPreview = this.previewFiles.has(fileId); + var newPreview = isPreview !== undefined ? isPreview : !currentPreview; + + if (newPreview) { + // 开启预览前,先清除其他文件的预览状态(只允许同时预览一个) + this.previewFiles.clear(); + this.previewFiles.add(fileId); + } else { + this.previewFiles.delete(fileId); + } + + this.renderFileList(); + + // 调用外部回调 + if (typeof this.options.onPreview === "function") { + this.options.onPreview(file, newPreview); + } + + return newPreview; }, }; @@ -876,33 +1010,33 @@ // jQuery 插件封装 // ========================== $.fn.G3File = function (options) { - var args = Array.prototype.slice.call(arguments, 1); var instance; + var methodResult; // 存储方法调用结果 this.each(function () { var $this = $(this); - instance = $this.data('G3File'); + instance = $this.data("G3File"); // ========= 初始化 ========= if (!instance) { instance = Object.create(G3File); instance.init(this.id, options); - $this.data('G3File', instance); + $this.data("G3File", instance); // ========= 方法调用 ========= - } else if (typeof options === 'string') { + } else if (typeof options === "string") { var method = options; if (instance[method]) { - instance[method].apply(instance, args); + // 保存方法调用结果 + methodResult = instance[method].apply(instance, args); } } }); - // ⭐关键:返回 instance(支持 file.xxx) - return instance || this; + // 如果是方法调用,返回方法结果;否则返回实例 + return methodResult !== undefined ? methodResult : instance || this; }; - -})(jQuery); \ No newline at end of file +})(jQuery); diff --git a/code/file-preview/index.html b/code/file-preview/index.html index 87a457d3..46ee3b1d 100644 --- a/code/file-preview/index.html +++ b/code/file-preview/index.html @@ -31,6 +31,10 @@
+ + + +
@@ -54,6 +58,14 @@ onUpload: function () { console.log('Upload clicked'); alert('上传功能'); + }, + onPreview: function (file, isPreview) { + console.log('预览事件:', file.name, '预览状态:', isPreview); + if (isPreview) { + alert('开始预览文件: ' + file.name); + } else { + alert('停止预览文件: ' + file.name); + } } }); @@ -70,6 +82,45 @@ { name: '项目报告2.docx', size: 524288, uploadDate: '2024-02-20T14:45:00', url: '' }, ]); }); + + // 测试外部方法:切换选中状态 + $('#toggleSelectBtn').click(function () { + const allFiles = file.getFiles(); + if (allFiles.length === 0) { + alert('请先加载文件'); + return; + } + // 获取第一个文件的ID并切换选中状态 + const firstFileId = allFiles[0]._id; + const isSelected = file.toggleSelect(firstFileId); + console.log('文件', allFiles[0].name, '选中状态:', isSelected); + }); + + // 测试外部方法:获取选中文件 + $('#getSelectedBtn').click(function () { + const selectedFiles = file.getSelectedFiles(); + console.log('选中的文件:', selectedFiles); + alert('选中的文件数量: ' + selectedFiles.length + '\n文件列表: ' + selectedFiles.map(f => f.name).join(', ')); + }); + + // 测试外部方法:切换预览状态 + $('#togglePreviewBtn').click(function () { + const allFiles = file.getFiles(); + if (allFiles.length === 0) { + alert('请先加载文件'); + return; + } + const firstFileId = allFiles[0]._id; + const isPreview = file.togglePreview(firstFileId); + console.log('文件', allFiles[0].name, '预览状态:', isPreview); + }); + + // 测试外部方法:获取预览文件 + $('#getPreviewBtn').click(function () { + const previewFiles = file.getPreviewFiles(); + console.log('预览的文件:', previewFiles); + alert('预览的文件数量: ' + previewFiles.length + '\n文件列表: ' + previewFiles.map(f => f.name).join(', ')); + }); }); diff --git a/code/file-preview/test.html b/code/file-preview/test.html new file mode 100644 index 00000000..9fd5806d --- /dev/null +++ b/code/file-preview/test.html @@ -0,0 +1,168 @@ + + + + + + 附件管理示例 + + + + + + +
+ 已选 0 个 + + +
+ + + +
    +
  • + + +``` + 合同.pdf + +
    + + + +
    +
  • + +
  • + + + 图片.png + +
    + + + +
    +
  • + +
  • + + + 说明文档.docx + +
    + + + +
    +
  • +``` + +
+ + +