UNPKG

@eternalheart/ngx-file-preview

Version:

A powerful Angular file preview component library supporting multiple file formats including images, videos, PDFs, Office documents, text files and more.

1,099 lines (1,086 loc) 308 kB
import * as i0 from '@angular/core'; import { Injectable, inject, ApplicationRef, createComponent, ChangeDetectorRef, Input, Directive, HostListener, Component, HostBinding, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, Pipe, ViewChild, ChangeDetectionStrategy, EventEmitter, Output, ViewEncapsulation, ViewChildren, ContentChild } from '@angular/core'; import * as i1 from '@angular/common'; import { CommonModule, NgIf } from '@angular/common'; import { BehaviorSubject, Subject, from, firstValueFrom, fromEvent, timer, merge } from 'rxjs'; import * as XLSX from 'xlsx'; import { renderAsync } from 'docx-preview'; import * as i2 from 'ngx-extended-pdf-viewer'; import { NgxExtendedPdfViewerModule, NgxExtendedPdfViewerComponent } from 'ngx-extended-pdf-viewer'; import Hls from 'hls.js'; import { init } from 'pptx-preview'; import { filter, switchMap, takeUntil, tap } from 'rxjs/operators'; import MarkdownIt from 'markdown-it'; import hljs from 'highlight.js'; import * as i1$1 from '@angular/platform-browser'; class ThemeService { constructor(renderer) { this.renderer = renderer; this.THEME_KEY = 'fp-theme-mode'; this.themeSubject$ = new BehaviorSubject('dark'); this.autoConfig = { dark: { start: 18, end: 6 } }; this.systemThemeQuery = null; this.systemThemeListener = null; this.localDomElement = null; } /** * 绑定最外围元素 * @param domElement */ bindElement(domElement) { this.localDomElement = domElement; } ngOnInit() { if (window.matchMedia) { this.systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)'); this.systemThemeListener = (e) => { if (this.theme === 'auto') { this.checkAndApplyAutoTheme(); } }; this.systemThemeQuery.addEventListener('change', this.systemThemeListener); } this.applyTheme('dark'); } get theme() { return this.themeSubject$.getValue(); } getThemeObservable() { return this.themeSubject$.asObservable(); } setMode(mode) { this.themeSubject$.next(mode); if (mode === 'auto') { this.startAutoCheck(); } else { this.stopAutoCheck(); this.applyTheme(mode); } } setAutoConfig(config) { this.autoConfig = { ...this.autoConfig, ...config }; if (this.themeSubject$.getValue() === 'auto') { this.checkAndApplyAutoTheme(); } } startAutoCheck() { this.checkAndApplyAutoTheme(); this.autoChangeTimer = setInterval(() => { this.checkAndApplyAutoTheme(); }, 60000); // 每分钟检查一次 } stopAutoCheck() { if (this.autoChangeTimer) { clearInterval(this.autoChangeTimer); this.autoChangeTimer = null; } } checkAndApplyAutoTheme() { const hour = new Date().getHours(); const { start, end } = this.autoConfig.dark; // 检查是否在暗色时间范围内 const isDarkTime = start > end ? (hour >= start || hour < end) // 跨夜间 : (hour >= start && hour < end); // 同一天内 // 检查系统主题 const prefersDark = this.systemThemeQuery?.matches ?? false; // 优先使用时间判断,其次使用系统主题 this.applyTheme(isDarkTime || prefersDark ? 'dark' : 'light'); } applyTheme(theme) { // 更新当前主题 this.themeSubject$.next(theme); if (this.localDomElement) { // 移除现有主题 this.renderer.removeAttribute(this.localDomElement, 'data-nfp-theme'); // 应用新主题 if (theme === 'dark') { this.renderer.setAttribute(this.localDomElement, 'data-nfp-theme', 'dark'); } else { this.renderer.setAttribute(this.localDomElement, 'data-nfp-theme', 'light'); } } // 保存到本地存储 localStorage.setItem(this.THEME_KEY, theme); } toggleTheme() { const newTheme = this.theme === 'light' ? 'dark' : 'light'; this.setMode(newTheme); } ngOnDestroy() { this.stopAutoCheck(); // 清理系统主题监听 if (this.systemThemeQuery && this.systemThemeListener) { this.systemThemeQuery.removeEventListener('change', this.systemThemeListener); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ThemeService, deps: [{ token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ThemeService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ThemeService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i0.Renderer2 }] }); var loading$1 = "加载中..."; var list$1 = { title: "文件列表", empty: "暂无文件", total: "共${0}个文件" }; var preview$1 = { toolbar: { resetZoom: "重置缩放", zoomIn: "放大", zoomOut: "缩小", fullscreen: "全屏", download: "下载", close: "关闭", previous: "上一个", next: "下一个", reset: "重置", rotate: "旋转", rotate90: "旋转90度", "rotate-90": "旋转-90度", originSize: "原始尺寸", autoFit: "自适应", wrap: "换行", nowrap: "不换行", play: "播放", pause: "暂停", back15s: "后退15秒", forward15s: "前进15秒", pip: "画中画", light: "日间模式", dark: "夜间模式" }, error: { noFiles: "没有文件可预览!" } }; var zip$1 = { type: "类型", size: "大小", unknownSize: "大小未知", types: { zip: "ZIP 压缩文件", rar: "RAR 压缩文件", "7z": "7-Zip 压缩文件", tar: "TAR 归档文件", gz: "GZip 压缩文件", unknown: "压缩文件" } }; var types$1 = { audio: "音频文件", image: "图片文件", video: "视频文件", pdf: "PDF文档", word: "WORD文档", excel: "Excel文档", ppt: "PPT文档", txt: "文本文件", markdown: "Markdown文件", zip: "压缩文件", unknown: "未知文件" }; var unknownFileTips$1 = "暂不支持该文件类型的预览"; var ZH = { loading: loading$1, list: list$1, preview: preview$1, zip: zip$1, types: types$1, unknownFileTips: unknownFileTips$1 }; var loading = "Loading..."; var list = { title: "File list", empty: "No files", total: "A total of files is ${0}" }; var preview = { toolbar: { resetZoom: "Reset Zoom", zoomIn: "Zoom In", zoomOut: "Zoom Out", fullscreen: "Full Screen", download: "Download", close: "Close", previous: "Previous", next: "Next", reset: "Reset", rotate: "Rotate", rotate90: "Rotate 90 degrees", "rotate-90": "Rotate -90 degrees", originSize: "Original Size", autoFit: "Auto Fit", wrap: "Wrap", nowrap: "No Wrap", play: "Play", pause: "Pause", back15s: "Back 15 seconds", forward15s: "Forward 15 seconds", pip: "Picture in Picture", light: "Light Mode", dark: "Dark Mode" }, error: { noFiles: "No files can be previewed!" } }; var zip = { type: "Type", size: "Size", unknownSize: "Unknown Size", types: { zip: "ZIP Archive File", rar: "RAR Archive File", "7z": "7-Zip Archive File", tar: "TAR Archive File", gz: "GZip Archive File", unknown: "Archive File" } }; var types = { audio: "Audio File", image: "Image File", video: "Video File", pdf: "PDF Document", word: "Word Document", excel: "Excel Document", ppt: "PPT Document", txt: "Text File", markdown: "Markdown File", zip: "Archive File", unknown: "Unknown File" }; var unknownFileTips = "File preview is not supported for this file type."; var EN = { loading: loading, list: list, preview: preview, zip: zip, types: types, unknownFileTips: unknownFileTips }; const LangMapping = { 'zh': ZH, }; const I18nUtils = { /** * 获取语言包 * @param locale */ get(locale) { return new I18nParser(locale || 'zh'); }, /** * 注册语言包 */ register(locale, langJson) { LangMapping[locale] = langJson; } }; /** * 注册使用示例 */ I18nUtils.register('en', EN); /** * 单例模式优化语言包的获取 单个语言只会创建一个语言转化实例 */ class I18nParser { static { this.InstanceMap = {}; } constructor(locale) { this.locale = 'zh'; this.locale = locale; if (I18nParser.InstanceMap[locale]) return I18nParser.InstanceMap[locale]; I18nParser.InstanceMap[locale] = this; } // 翻译 t(key, ...args) { const translated = I18nParser.getValue(LangMapping[this.locale], key); if (args.length > 0) return translated.replace(/\${(\d+)}/g, (match, index) => args[index]); if (translated) return translated; return key; } // 获取深层值 static getValue(data, prop) { let ps = Array.isArray(prop) ? prop : prop.split('.'); try { return ps.length == 1 ? data[ps.shift()] : I18nParser.getValue(data[ps.shift()], ps); } catch (e) { return undefined; } } } const INITIAL_PREVIEW_STATE = { isVisible: false, currentIndex: 0, files: [] }; class PreviewService { constructor() { this.appRef = inject(ApplicationRef); this.lang = 'zh'; this.loading = new BehaviorSubject(false); // endregion // region 状态管理 this.stateSubject = new BehaviorSubject(INITIAL_PREVIEW_STATE); } /** * 初始化 * @param injector * @param envInjector */ init(injector, envInjector) { this.envInjector = envInjector; this.injector = injector; } /** * 设置语言 * @param lang */ setLang(lang) { this.lang = lang; } /** * 获取实际的lang parser */ getLangParser() { return I18nUtils.get(this.lang); } get state() { return this.stateSubject.getValue(); } getStateObservable() { return this.stateSubject.asObservable(); } previous() { const state = this.state; const newIndex = Math.max(0, state.currentIndex - 1); this.updatePreviewState(true, state.files, newIndex); } next() { const state = this.state; const newIndex = Math.min(state.files.length - 1, state.currentIndex + 1); this.updatePreviewState(true, state.files, newIndex); } updatePreviewState(isVisible, files, index) { const currentFile = files[index]; this.stateSubject.next({ isVisible, currentFile, currentIndex: index, files }); } /** * 设置加载中状态 * @param loading */ setLoading(loading) { this.loading.next(loading); } getLoadingObservable() { return this.loading.asObservable(); } get modalElement() { return this.modalRef?.location.nativeElement; } open(options) { const { files, index = 0 } = options; if (this.modalRef) { this.cleanupModal(); } try { this.modalRef = createComponent(PreviewModalComponent, { environmentInjector: this.envInjector, elementInjector: this.injector, }); Object.assign(this.modalRef.instance, options); this.injector.get(ThemeService).bindElement(this.modalRef.location.nativeElement); document.body.appendChild(this.modalRef.location.nativeElement); this.modalRef.changeDetectorRef.detectChanges(); this.updatePreviewState(true, files, index); this.appRef.attachView(this.modalRef.hostView); } catch (error) { console.error('Error creating preview-list modal:', error); this.cleanupModal(); } } close() { if (document.fullscreenElement) { document?.exitFullscreen(); } this.updatePreviewState(false, [], 0); this.cleanupModal(); } cleanupModal() { if (!this.modalRef) return; try { // 从 DOM 中移除模态框 const element = this.modalRef.location.nativeElement; if (element.parentNode) { element.parentNode.removeChild(element); } // 从 ApplicationRef 中分离视图 this.appRef.detachView(this.modalRef.hostView); // 销毁组件 this.modalRef.destroy(); } catch (error) { console.error('Error cleaning up modal:', error); } finally { this.modalRef = undefined; } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PreviewService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PreviewService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PreviewService, decorators: [{ type: Injectable }] }); class FileReaderService { constructor() { this.responseSubject = new Subject(); } async readFileData(file, fileType) { try { const response = await fetch(file.url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const result = { type: 'success' }; switch (fileType) { case 'arraybuffer': result.data = await response.arrayBuffer(); break; case 'text': result.text = await response.text(); break; case 'json': const text = await response.text(); try { result.json = JSON.parse(text); } catch (parseError) { throw new Error(`JSON parse failed: ${parseError.message}`); } break; } return result; } catch (error) { return { type: 'error', error: error instanceof Error ? error.message : 'Unknown error' }; } } readFile(file, fileType = 'arraybuffer') { return from(this.readFileData(file, fileType)); } ngOnDestroy() { this.responseSubject.complete(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FileReaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FileReaderService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FileReaderService, decorators: [{ type: Injectable }] }); class BasePreviewComponent { constructor() { this.fileReader = inject(FileReaderService); this.previewService = inject(PreviewService); this.cdr = inject(ChangeDetectorRef); } get isLoading() { return this.previewService.getLoadingObservable(); } t(key, ...args) { return this.previewService?.getLangParser()?.t(key, ...args); } async loadFile(fileType) { if (!this.file) return; this.startLoading(); try { const content = await firstValueFrom(this.fileReader.readFile(this.file, fileType)); await this.handleFileContent(content); } catch (error) { console.error('Failed to read file:', error); } finally { this.stopLoading(); } } startLoading() { this.previewService.setLoading(true); this.cdr.markForCheck(); } stopLoading() { this.previewService.setLoading(false); this.cdr.markForCheck(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BasePreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "17.3.12", type: BasePreviewComponent, isStandalone: true, inputs: { file: "file", themeMode: ["themeMode", "themeMode", (value) => value], autoThemeConfig: "autoThemeConfig" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: BasePreviewComponent, decorators: [{ type: Directive, args: [{ standalone: true, }] }], propDecorators: { file: [{ type: Input }], themeMode: [{ type: Input, args: [{ transform: (value) => value }] }], autoThemeConfig: [{ type: Input }] } }); class TooltipDirective { constructor(el, renderer, viewContainer, previewService) { this.el = el; this.renderer = renderer; this.viewContainer = viewContainer; this.previewService = previewService; this.delay = 500; this.positions = ['top', 'bottom', 'left', 'right']; this.currentPosition = 'top'; } onMouseEnter() { this.clearTimers(); this.showTimeout = setTimeout(() => this.show(), this.delay); } onMouseLeave() { this.clearTimers(); this.hideTimeout = setTimeout(() => this.hide(), 100); } show() { if (!this.content) return; if (!this.tooltip) { // 动态创建组件 const factory = this.viewContainer.createComponent(TooltipComponent); this.tooltip = factory.location.nativeElement; factory.instance.content = this.content; // 立即显示内容 this.renderer.addClass(this.tooltip, 'visible'); this.previewService.modalElement?.querySelector('.nfp-modal__overlay').appendChild(this.tooltip); factory.changeDetectorRef.detectChanges(); } // 计算最佳位置 const hostRect = this.el.nativeElement.getBoundingClientRect(); const tooltipRect = this.tooltip.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // 检查每个位置的可用空间 const spaces = { top: hostRect.top, bottom: viewportHeight - (hostRect.bottom), left: hostRect.left, right: viewportWidth - (hostRect.right) }; // 找到最佳位置 this.currentPosition = this.positions.reduce((best, current) => spaces[current] > spaces[best] ? current : best); // 根据位置设置样式类 this.positions.forEach(pos => this.renderer.removeClass(this.tooltip, pos)); this.renderer.addClass(this.tooltip, this.currentPosition); // 根据位置计算坐标 let top, left; switch (this.currentPosition) { case 'top': top = hostRect.top - tooltipRect.height - 8; left = hostRect.left + (hostRect.width - tooltipRect.width) / 2; break; case 'bottom': top = hostRect.bottom + 8; left = hostRect.left + (hostRect.width - tooltipRect.width) / 2; break; case 'left': top = hostRect.top + (hostRect.height - tooltipRect.height) / 2; left = hostRect.left - tooltipRect.width - 8; break; case 'right': top = hostRect.top + (hostRect.height - tooltipRect.height) / 2; left = hostRect.right + 8; break; } // 确保tooltip不超出视口 top = Math.max(8, Math.min(viewportHeight - tooltipRect.height - 8, top)); left = Math.max(8, Math.min(viewportWidth - tooltipRect.width - 8, left)); this.renderer.setStyle(this.tooltip, 'top', `${top}px`); this.renderer.setStyle(this.tooltip, 'left', `${left}px`); } hide() { if (this.tooltip) { this.renderer.removeClass(this.tooltip, 'visible'); setTimeout(() => { this.viewContainer.clear(); this.tooltip = null; }, 300); // 增加动画时间 } } clearTimers() { clearTimeout(this.showTimeout); clearTimeout(this.hideTimeout); } ngOnDestroy() { this.viewContainer.clear(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TooltipDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.ViewContainerRef }, { token: PreviewService }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: TooltipDirective, isStandalone: true, selector: "[tooltip]", inputs: { content: ["tooltip", "content"], delay: "delay" }, host: { listeners: { "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()" } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TooltipDirective, decorators: [{ type: Directive, args: [{ selector: '[tooltip]', standalone: true }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.ViewContainerRef }, { type: PreviewService }], propDecorators: { content: [{ type: Input, args: ['tooltip'] }], delay: [{ type: Input }], onMouseEnter: [{ type: HostListener, args: ['mouseenter'] }], onMouseLeave: [{ type: HostListener, args: ['mouseleave'] }] } }); class TooltipComponent { constructor() { this.content = ""; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TooltipComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: TooltipComponent, isStandalone: true, selector: "ngx-file-tooltip", inputs: { content: "content" }, ngImport: i0, template: `{{ content }}`, isInline: true, styles: [":host{position:absolute;background:#000000d9;color:#fff;font-size:14px;padding:6px 8px;border-radius:2px;box-shadow:0 3px 6px -4px #0000001f,0 6px 16px #00000014,0 9px 28px 8px #0000000d;max-width:250px;min-height:24px;word-wrap:break-word;z-index:999;pointer-events:none;opacity:0;display:flex;justify-content:center;align-items:center;transform:scale(.8);transform-origin:center;transition:opacity .2s ease-in-out,transform .2s ease-in-out}:host.visible{opacity:1;transform:scale(1)}:host:after{content:\"\";position:absolute;width:0;height:0;border:5px solid transparent}:host.top:after{border-top-color:#000000d9;bottom:-10px;left:50%;transform:translate(-50%)}:host.bottom:after{border-bottom-color:#000000d9;top:-10px;left:50%;transform:translate(-50%)}:host.left:after{border-left-color:#000000d9;right:-10px;top:50%;transform:translateY(-50%)}:host.right:after{border-right-color:#000000d9;left:-10px;top:50%;transform:translateY(-50%)}\n"] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TooltipComponent, decorators: [{ type: Component, args: [{ selector: 'ngx-file-tooltip', template: `{{ content }}`, standalone: true, styles: [":host{position:absolute;background:#000000d9;color:#fff;font-size:14px;padding:6px 8px;border-radius:2px;box-shadow:0 3px 6px -4px #0000001f,0 6px 16px #00000014,0 9px 28px 8px #0000000d;max-width:250px;min-height:24px;word-wrap:break-word;z-index:999;pointer-events:none;opacity:0;display:flex;justify-content:center;align-items:center;transform:scale(.8);transform-origin:center;transition:opacity .2s ease-in-out,transform .2s ease-in-out}:host.visible{opacity:1;transform:scale(1)}:host:after{content:\"\";position:absolute;width:0;height:0;border:5px solid transparent}:host.top:after{border-top-color:#000000d9;bottom:-10px;left:50%;transform:translate(-50%)}:host.bottom:after{border-bottom-color:#000000d9;top:-10px;left:50%;transform:translate(-50%)}:host.left:after{border-left-color:#000000d9;right:-10px;top:50%;transform:translateY(-50%)}:host.right:after{border-right-color:#000000d9;left:-10px;top:50%;transform:translateY(-50%)}\n"] }] }], propDecorators: { content: [{ type: Input }] } }); class PreviewIconComponent { constructor() { this.name = ""; this.svg = ""; this.size = '16px'; this.title = ""; this.cursor = 'pointer'; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PreviewIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "17.3.12", type: PreviewIconComponent, isStandalone: true, selector: "preview-icon", inputs: { name: "name", svg: "svg", size: ["size", "size", (v) => typeof v === 'number' ? `${v}px` : v], color: "color", themeMode: "themeMode", title: "title", cursor: "cursor" }, host: { properties: { "style.cursor": "this.cursor" } }, ngImport: i0, template: ` <ng-container *ngIf="name"> <i [tooltip]="title" class="fp-font-icon NGX-FILE-PREVIEW" [class]="'nfp-'+name" [style.width]="size" [style.font-size]="size" [style.color]="color ? color: (themeMode=='light'?'#333333':'#FFFFFF')"></i> </ng-container> <ng-container *ngIf="svg"> <svg class="fp-svg-icon" [style.width]="size" [style.height]="size" [tooltip]="title" aria-hidden="true"> <use [attr.xlink:href]="'#nfp-' + svg">"></use> </svg> </ng-container> `, isInline: true, styles: [":host{display:inline-block;line-height:0}:host .fp-svg-icon{width:1em;height:1em;vertical-align:-.15em;fill:currentColor;overflow:hidden}:host .fp-font-icon{color:#fff;display:inline-flex;justify-content:center;align-items:center;aspect-ratio:1;overflow:hidden}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: TooltipDirective, selector: "[tooltip]", inputs: ["tooltip", "delay"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: PreviewIconComponent, decorators: [{ type: Component, args: [{ selector: "preview-icon", template: ` <ng-container *ngIf="name"> <i [tooltip]="title" class="fp-font-icon NGX-FILE-PREVIEW" [class]="'nfp-'+name" [style.width]="size" [style.font-size]="size" [style.color]="color ? color: (themeMode=='light'?'#333333':'#FFFFFF')"></i> </ng-container> <ng-container *ngIf="svg"> <svg class="fp-svg-icon" [style.width]="size" [style.height]="size" [tooltip]="title" aria-hidden="true"> <use [attr.xlink:href]="'#nfp-' + svg">"></use> </svg> </ng-container> `, imports: [CommonModule, TooltipDirective], standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA], styles: [":host{display:inline-block;line-height:0}:host .fp-svg-icon{width:1em;height:1em;vertical-align:-.15em;fill:currentColor;overflow:hidden}:host .fp-font-icon{color:#fff;display:inline-flex;justify-content:center;align-items:center;aspect-ratio:1;overflow:hidden}\n"] }] }], propDecorators: { name: [{ type: Input }], svg: [{ type: Input }], size: [{ type: Input, args: [{ transform: (v) => typeof v === 'number' ? `${v}px` : v }] }], color: [{ type: Input }], themeMode: [{ type: Input }], title: [{ type: Input }], cursor: [{ type: Input }, { type: HostBinding, args: ['style.cursor'] }] } }); class I18nPipe { constructor(previewService) { this.previewService = previewService; } transform(key, ...args) { const parser = this.previewService.getLangParser(); return parser.t(key, ...args); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: I18nPipe, deps: [{ token: PreviewService }], target: i0.ɵɵFactoryTarget.Pipe }); } static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: I18nPipe, isStandalone: true, name: "i18n" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: I18nPipe, decorators: [{ type: Pipe, args: [{ name: 'i18n', standalone: true }] }], ctorParameters: () => [{ type: PreviewService }] }); class ExcelPreviewComponent extends BasePreviewComponent { constructor() { super(...arguments); this.scale = 1; this.sheets = []; this.currentSheet = ''; this.tableData = { headers: [], rows: [] }; this.displayRows = []; this.extraRows = 100; // 增加额外显示的空行数 this.extraColumns = Array(5).fill(0); this.visibleRows = []; this.SCALE_STEP = 0.1; this.MAX_SCALE = 3; this.MIN_SCALE = 0.1; this.isDragging = false; this.startX = 0; this.startY = 0; this.scrollLeft = 0; this.scrollTop = 0; this.DEFAULT_SCALE = 1; } get totalColumns() { const total = (this.tableData.headers.length + this.extraColumns.length) || 0; return Array(total).fill(0); } ngOnChanges(changes) { if (changes['file'] && this.file) { this.loadFile(); } } ngAfterViewInit() { this.setupDragListeners(); this.setupKeyboardListeners(); } ngOnDestroy() { this.removeDragListeners(); this.removeKeyboardListeners(); } async handleFileContent(content) { const { data } = content; this.workbook = XLSX.read(data, { type: 'array' }); this.sheets = this.workbook.SheetNames; if (this.sheets.length > 0) { await this.switchSheet(this.sheets[0]); } } setupDragListeners() { this.mouseMoveListener = (e) => this.onDrag(e); this.mouseUpListener = () => this.stopDrag(); document.addEventListener('mousemove', this.mouseMoveListener); document.addEventListener('mouseup', this.mouseUpListener); } removeDragListeners() { if (this.mouseMoveListener) { document.removeEventListener('mousemove', this.mouseMoveListener); } if (this.mouseUpListener) { document.removeEventListener('mouseup', this.mouseUpListener); } } startDrag(e) { // 如果点击的是滚动条,不启动拖动 const wrapper = this.tableWrapper.nativeElement; const rect = wrapper.getBoundingClientRect(); const isClickOnScrollbarX = e.clientY > (rect.bottom - 12); const isClickOnScrollbarY = e.clientX > (rect.right - 12); if (isClickOnScrollbarX || isClickOnScrollbarY) { return; } this.isDragging = true; this.startX = e.pageX - wrapper.offsetLeft; this.startY = e.pageY - wrapper.offsetTop; this.scrollLeft = wrapper.scrollLeft; this.scrollTop = wrapper.scrollTop; } onDrag(e) { if (!this.isDragging) return; e.preventDefault(); const wrapper = this.tableWrapper.nativeElement; const x = e.pageX - wrapper.offsetLeft; const y = e.pageY - wrapper.offsetTop; const walkX = (x - this.startX) * 1.5; // 增加一些移动速度 const walkY = (y - this.startY) * 1.5; wrapper.scrollLeft = this.scrollLeft - walkX; wrapper.scrollTop = this.scrollTop - walkY; } stopDrag() { this.isDragging = false; } async switchSheet(sheetName) { if (!this.workbook) return; try { const worksheet = this.workbook.Sheets[sheetName]; const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); // 确保所有行的长度一致 const maxLength = Math.max(...jsonData.map((row) => row?.length || 0), 0); this.displayRows = jsonData.map((row) => { const paddedRow = Array.isArray(row) ? [...row] : []; while (paddedRow.length < maxLength) { paddedRow.push(null); } return paddedRow; }); // 添加额外的空行 const emptyRows = Array(this.extraRows).fill(0).map(() => Array(maxLength).fill(null)); this.visibleRows = [...this.displayRows, ...emptyRows]; this.tableData = { headers: Array(maxLength).fill(''), rows: this.displayRows }; this.currentSheet = sheetName; this.cdr.markForCheck(); } catch (error) { console.error('切换工作表失败:', error); } } zoomIn() { if (this.scale < this.MAX_SCALE) { this.scale = Math.min(this.MAX_SCALE, this.scale + this.SCALE_STEP); this.applyZoom(); } } zoomOut() { if (this.scale > this.MIN_SCALE) { this.scale = Math.max(this.MIN_SCALE, this.scale - this.SCALE_STEP); this.applyZoom(); } } toggleFullscreen() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); } else { document.exitFullscreen(); } } getColumnName(index) { let name = ''; let num = index; do { name = String.fromCharCode(65 + (num % 26)) + name; num = Math.floor(num / 26) - 1; } while (num >= 0); return name; } getRowNumber(index) { return index + 1; } handleWheel(event) { if (event.ctrlKey || event.metaKey) { event.preventDefault(); const delta = event.deltaY || event.detail || 0; if (delta < 0) { this.zoomIn(); } else { this.zoomOut(); } } } applyZoom() { if (this.tableWrapper) { const wrapper = this.tableWrapper.nativeElement; // 保存当前滚动位置的相对百分比 const scrollLeftPercent = wrapper.scrollLeft / (wrapper.scrollWidth - wrapper.clientWidth); const scrollTopPercent = wrapper.scrollTop / (wrapper.scrollHeight - wrapper.clientHeight); // 应用缩放 wrapper.style.transform = `scale(${this.scale})`; // 在下一个事件循环中恢复滚动位置 setTimeout(() => { wrapper.scrollLeft = scrollLeftPercent * (wrapper.scrollWidth - wrapper.clientWidth); wrapper.scrollTop = scrollTopPercent * (wrapper.scrollHeight - wrapper.clientHeight); }); } this.cdr.markForCheck(); } setupKeyboardListeners() { this.keydownListener = (e) => { // 按下 Ctrl/Command + 0 重置缩放 if ((e.ctrlKey || e.metaKey) && e.key === '0') { e.preventDefault(); this.resetZoom(); } }; document.addEventListener('keydown', this.keydownListener); } removeKeyboardListeners() { if (this.keydownListener) { document.removeEventListener('keydown', this.keydownListener); } } resetZoom() { this.scale = this.DEFAULT_SCALE; this.applyZoom(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ExcelPreviewComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.12", type: ExcelPreviewComponent, isStandalone: true, selector: "ngx-excel-preview", viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true }, { propertyName: "tableWrapper", first: true, predicate: ["tableWrapper"], descendants: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: ` <div class="excel-container" #container> <div class="toolbar"> <div class="left-controls"> <button class="tool-btn" (click)="zoomOut()"> <preview-icon [themeMode]="themeMode" name="zoom-out" [title]="'preview.toolbar.zoomOut'|i18n"></preview-icon> </button> <span class="zoom-text" (click)="resetZoom()" [title]="'preview.toolbar.resetZoom'|i18n"> {{ (scale * 100).toFixed(0) }}% </span> <button class="tool-btn" (click)="zoomIn()"> <preview-icon [themeMode]="themeMode" name="zoom-in" [title]="'preview.toolbar.zoomIn'|i18n"></preview-icon> </button> </div> <div class="sheet-controls" *ngIf="sheets.length > 0"> <button class="sheet-btn" *ngFor="let sheet of sheets" [class.active]="currentSheet === sheet" (click)="switchSheet(sheet)"> {{ sheet }} </button> </div> <div class="right-controls"> <button class="tool-btn" (click)="toggleFullscreen()"> <preview-icon [themeMode]="themeMode" name="fullscreen" [title]="'preview.toolbar.fullscreen'|i18n"></preview-icon> </button> </div> </div> <div class="preview-container"> <div class="preview-content"> <div class="table-wrapper" #tableWrapper (mousedown)="startDrag($event)" (wheel)="handleWheel($event)" [class.dragging]="isDragging" [style.transform]="'scale(' + scale + ')'"> <table *ngIf="tableData"> <colgroup> <col class="row-header-col"> <col *ngFor="let header of tableData.headers" class="data-col"> <col *ngFor="let i of extraColumns" class="data-col"> </colgroup> <thead> <tr> <th class="corner-cell"></th> <th *ngFor="let header of tableData.headers; let i = index"> {{ getColumnName(i) }} </th> <th *ngFor="let i of extraColumns;let j=index" class="empty-column"> {{ getColumnName(tableData.headers.length + j) }} </th> </tr> </thead> <tbody> <tr *ngFor="let row of visibleRows; let rowIndex = index"> <td class="row-header">{{ getRowNumber(rowIndex) }}</td> <td *ngFor="let cell of row; let colIndex = index" [class.empty-cell]="!cell && cell !== 0"> {{ cell }} </td> <td *ngFor="let i of extraColumns" class="empty-cell"></td> </tr> </tbody> </table> </div> </div> </div> </div> `, isInline: true, styles: [":root{--nfp-primary-color: #177ddc;--nfp-primary-hover: #1890ff;--nfp-primary-active: #0050b3;--nfp-error-color: #d32029;--nfp-warning-color: #d89614;--nfp-success-color: #49aa19;--nfp-text-primary: rgba(0, 0, 0, .85);--nfp-text-secondary: rgba(0, 0, 0, .65);--nfp-text-disabled: rgba(0, 0, 0, .25);--nfp-bg-container: #ffffff;--nfp-bg-elevated: #fafafa;--nfp-bg-layout: #f0f2f5;--nfp-hover-bg: rgba(0, 0, 0, .04);--nfp-border-color: #d9d9d9;--nfp-split-color: rgba(0, 0, 0, .06);--nfp-scrollbar-bg: #ffffff;--nfp-scrollbar-thumb: #d9d9d9;--nfp-toolbar-bg: #fafafa;--nfp-toolbar-border: #d9d9d9;--nfp-toolbar-hover: rgba(0, 0, 0, .04);--nfp-toolbar-active: #e6f4ff;--nfp-preview-mask: rgba(0, 0, 0, .3);--nfp-preview-loading-bg: rgba(255, 255, 255, .8);--nfp-preview-toolbar-bg: rgba(0, 0, 0, .1);--nfp-theme-transition-duration: .3s}[data-nfp-theme=dark]{--nfp-primary-color: #177ddc;--nfp-primary-hover: #1890ff;--nfp-primary-active: #0050b3;--nfp-error-color: #a61d24;--nfp-warning-color: #d89614;--nfp-success-color: #49aa19;--nfp-text-primary: rgba(255, 255, 255, .85);--nfp-text-secondary: rgba(255, 255, 255, .65);--nfp-text-disabled: rgba(255, 255, 255, .25);--nfp-bg-container: #1a1a1a;--nfp-bg-elevated: #262626;--nfp-bg-layout: #141414;--nfp-hover-bg: rgba(255, 255, 255, .08);--nfp-border-color: #303030;--nfp-split-color: rgba(255, 255, 255, .12);--nfp-scrollbar-bg: #1a1a1a;--nfp-scrollbar-thumb: #404040;--nfp-toolbar-bg: #262626;--nfp-toolbar-border: #303030;--nfp-toolbar-hover: rgba(255, 255, 255, .08);--nfp-toolbar-active: #111b26;--nfp-preview-mask: rgba(0, 0, 0, .65);--nfp-preview-loading-bg: rgba(0, 0, 0, .8);--nfp-preview-toolbar-bg: rgba(0, 0, 0, .4);--nfp-theme-transition-duration: .3s}*{transition:background-color var(--nfp-theme-transition-duration) var(--theme-transition-timing),border-color var(--nfp-theme-transition-duration) var(--theme-transition-timing),color var(--nfp-theme-transition-duration) var(--theme-transition-timing)}.no-transition,.no-transition *{transition:none!important}\n", ":host{display:block;width:100%;height:100%}.excel-container{width:100%;height:100%;background:var(--nfp-bg-container);display:flex;flex-direction:column;border-radius:8px;overflow:hidden}.toolbar{height:48px;min-height:48px;background:var(--nfp-toolbar-bg);display:flex;justify-content:space-between;align-items:center;padding:0 16px;border-bottom:1px solid var(--nfp-toolbar-border);gap:16px}.left-controls{display:flex;align-items:center;gap:8px}.sheet-controls{flex:1;display:flex;align-items:center;gap:1px;overflow-x:auto;scrollbar-width:none}.sheet-controls::-webkit-scrollbar{display:none}.sheet-btn{background:var(--nfp-bg-container);border:none;color:var(--nfp-text-primary);padding:6px 16px;font-size:13px;cursor:pointer;white-space:nowrap;height:32px;display:flex;align-items:center;position:relative}.sheet-btn:hover{background:var(--nfp-toolbar-hover)}.sheet-btn.active{background:var(--nfp-toolbar-bg);color:var(--nfp-primary-color)}.sheet-btn.active:after{content:\"\";position:absolute;bottom:0;left:0;right:0;height:2px;background:var(--nfp-primary-color)}.preview-container{flex:1;position:relative;background:var(--nfp-toolbar-bg);display:flex;height:100%;flex-direction:column}.preview-content{width:100%;height:100%;display:flex;flex-direction:column;background:var(--nfp-toolbar-bg);overflow:hidden}.preview-content .table-wrapper{width:100%;height:100%;overflow:auto;cursor:default;transform-origin:0 0}.preview-content .table-wrapper.dragging,.preview-content .table-wrapper.dragging *{cursor:grab;-webkit-user-select:none;user-select:none}.preview-content .table-wrapper table{border-collapse:collapse;table-layout:fixed;background:var(--nfp-toolbar-bg);color:var(--nfp-text-primary);-webkit-user-select:none;user-select:none;width:max-content;min-width:100%}.preview-content .table-wrapper table .row-header-col{width:50px;min-width:50px}.preview-content .table-wrapper table .data-col{width:120px;min-width:120px}.preview-content .table-wrapper table thead{position:sticky;top:-1px;z-index:2;background:var(--nfp-toolbar-bg);margin-bottom:-1px}.preview-content .table-wrapper table tbody{background:var(--nfp-toolbar-bg)}.preview-content .table-wrapper table th,.preview-content .table-wrapper table td{height:24px;padding:4px 8px;border:1px solid var(--nfp-border-color);font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.preview-content .table-wrapper table th{background:var(--nfp-bg-elevated);font-weight:500;text-align:center;border-bottom:2px solid var(--nfp-border-color);color:var(--nfp-text-primary)}.preview-content .table-wrapper table .corner-cell{position:sticky;left:0;z-index:3;background:var(--nfp-toolbar-bg);border-right:2px solid var(--nfp-border-color);border-bottom:2px solid var(--nfp-border-color)}.preview-content .table-wrapper table .row-header{position:sticky;left:0;background:var(--nfp-bg-elevated);text-align:center;font-weight:500;z-index:1;border-right:2px solid var(--nfp-border-color);color:var(--nfp-text-primary)}.preview-content .table-wrapper table td{background:var(--nfp-toolbar-bg);text-align:left}.preview-content .table-wrapper table td.empty-cell{color:transparent;background:var(--nfp-bg-container)}.preview-content .table-wrapper table tbody tr:hover td{background:var(--nfp-hover-bg)}.preview-content .table-wrapper table tbody tr:hover td.empty-cell{background:var(--nfp-bg-container)}.preview-content .table-wrapper table tbody tr:hover td.row-header{background:var(--nfp-bg-elevated)}.preview-content .table-wrapper::-webkit-scrollbar{width:12px;height:12px}.preview-content .table-wrapper::-webkit-scrollbar-track{background:var(--nfp-scrollbar-bg)}.preview-content .table-wrapper::-webkit-scrollbar-thumb{background:var(--nfp-scrollbar-thumb);border:2px solid var(--nfp-scrollbar-bg);border-radius:6px}.preview-content .table-wrapper::-webkit-scrollbar-thumb:hover{background:var(--nfp-primary-color)}.tool-btn{background:transparent;border:none;color:var(--nfp-text-primary);width:32px;height:32px;padding:0;cursor:pointer;border-radius:4px;display:flex;align-items:center;justify-content:center;transition:all .2s}.tool-btn:hover{background:var(--nfp-toolbar-hover);color:var(--nfp-primary-color)}.zoom-text{color:var(--nfp-text-primary);font-size:13px;min-width:48px;text-align:center;cursor:pointer;padding:4px;border-radius:4px}.zoom-text:hover{background:var(--nfp-toolbar-hover);color:var(--nfp-primary-color)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: PreviewIconComponent, selector: "preview-icon", inputs: ["name", "svg", "size", "color", "themeMode", "title", "cursor"] }, { kind: "pipe", type: I18nPipe, name: "i18n" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ExcelPreviewComponent, decorators: [{ type: Component, args: [{ selector: 'ngx-excel-preview', standalone: true, imports: [CommonModule, PreviewIconComponent, I18nPipe], template: ` <div class="excel-container" #container> <div class="toolbar"> <div class="left-controls"> <button class="tool-btn" (click)="zoomOut()"> <preview-icon [themeMode]="themeMode" name="zoom-out" [title]="'preview.toolbar.zoomOut'|i18n"></preview-icon> </button> <span class="zoom-text" (click)="resetZoom()" [title]="'preview.toolbar.resetZoom'|i18n"> {{ (scale * 100).toFixed(0) }}% </span> <button class="tool-btn" (click)="zoomIn()"> <preview-icon [themeMode]="themeMode" name="zoom-in" [title]="'preview.toolbar.zoomIn'|i18n"></preview-icon> </button> </div> <div class="sheet-controls" *ngIf="sheets.length > 0"> <button class="sheet-btn" *ngFor="let sheet of sheets" [class.active]="currentSheet === sheet" (click)="switchSheet(sheet)"> {{ sheet }} </button> </div> <div class="right-controls"> <button class="tool-btn" (click)="toggleFullscreen()"> <preview-icon [themeMode]="themeMode" name="fullscreen" [title]="'preview.toolbar.fullscreen'|i18n"></preview-icon> </button> </div> </div> <div class="preview-container"> <div class="preview-content"> <div class="table-wrapper" #tableWrapper (mousedown)="startDrag($event)" (wheel)="handleWheel($event)" [class.dragging]="isDragging" [style.transform]="'scale(' + scale + ')'"> <table *ngIf="tableData"> <colgroup> <col class="row-header-col"> <col *ngFor="let header of tableData.headers" class="data-col"> <col *ngFor="let i of extraColumns" class="data-col"> </colgroup> <thead> <tr> <th class="corner-cell"></th> <th *ngFor="let header of tableData.headers; let i = index"> {{ getColumnName(i) }} </th> <th *ngFor="let i of extraColumns;let j=index" class="empty-column"> {{ getColumnName(tableData.headers.length + j) }} </th> </tr> </thead> <tbody> <tr *ngFor="let row of visibleRows; let rowIndex = index"> <td class="row-header">{{ getRowNumber(rowIndex) }}</td> <td *ngFor="let cell of row; let colIndex = index" [class.empty-cell]="!cell && cell !== 0"> {{ cell }} </td> <td *ngFor="let i of extraColumns" class="empty-cell"></td> </tr> </tbody> </table> </div> </div> </div> </div> `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":root{--nfp-primary-color: #177ddc;--nfp-primary-hover: #1890ff;--nfp-primary-active: #0050b3;--nfp-error-color: #d32029;--nfp-warning-color: #d89614;--nfp-success-color: #49aa19;--nfp-text-primary: rgba(0, 0, 0, .85);--nfp-text-secondary: rgba(0, 0, 0, .65);--nfp-text-disabled: rgba(0, 0, 0, .25);--nfp-bg-container: #ffffff;--nfp-bg-elevated: #fafafa;--n