diff --git a/src/apps/preview/Preview.vue b/src/apps/preview/Preview.vue index 0ada1d8..4f28b8b 100644 --- a/src/apps/preview/Preview.vue +++ b/src/apps/preview/Preview.vue @@ -1 +1,73 @@ - + + + + diff --git a/src/apps/preview/index.ts b/src/apps/preview/index.ts index c04fd68..ea41aee 100644 --- a/src/apps/preview/index.ts +++ b/src/apps/preview/index.ts @@ -1,11 +1,7 @@ +// 预览应用 — Vue 组件化 import { h, render } from 'vue' -// 预览应用 — 从 js/apps2.js 手动转写 -import { el, iconImg, clamp } from '../../utils' -import { fs as FS } from '../../composables/useFS' -import { wm as WM } from '../../composables/useWM' import { Apps, stdMenus } from '../../composables/useApps' import PreviewComponent from './Preview.vue' -import { WALLPAPERS } from '../../composables/useSettings' export const PreviewApp = { id: 'preview', name: '预览', icon: '/assets/icons/preview.svg', @@ -20,50 +16,9 @@ export const PreviewApp = { ] }) }, - render(win: any, args: any) { - const st = win.appState = { scale: 1, rot: 0, path: args.path || null, isPdf: false } - win.body.classList.add('preview-body') - const zOut = el('button', { class: 'fb-btn', text: '−', title: '缩小' }) - const zIn = el('button', { class: 'fb-btn', text: '+', title: '放大' }) - const rotBtn = el('button', { class: 'fb-btn', html: '⟳', title: '旋转' }) - const infoEl = el('span', { class: 'preview-info' }) - const toolbar = el('div', { class: 'fb-toolbar preview-toolbar' }, zOut, zIn, rotBtn, infoEl) - const stage = el('div', { class: 'preview-stage' }) - win.body.append(toolbar, stage) - st.zoom = (d: number) => { st.scale = clamp(st.scale + d, 0.2, 4); apply() } - st.zoomReset = () => { st.scale = 1; st.rot = 0; apply() } - zIn.addEventListener('click', () => st.zoom(0.2)) - zOut.addEventListener('click', () => st.zoom(-0.2)) - rotBtn.addEventListener('click', () => { st.rot = (st.rot + 90) % 360; apply() }) - let contentEl: HTMLElement | null = null - const apply = () => { if (contentEl) contentEl.style.transform = `scale(${st.scale}) rotate(${st.rot}deg)`; infoEl.textContent = `缩放 ${Math.round(st.scale * 100)}%` + (st.isPdf ? ' · 第 1 页,共 1 页' : '') } - const showError = (msg: string) => { stage.innerHTML = ''; stage.append(el('div', { class: 'empty-state' }, el('div', { class: 'es-icon', text: '🚫' }), el('div', { text: '无法打开此文件' }), el('div', { class: 'preview-err-sub', text: msg }))); infoEl.textContent = '' } - if (args.asset) st.path = args.asset - if (!st.path) { - const gallery = el('div', { class: 'preview-gallery' }) - WALLPAPERS.forEach((w: any) => { - const im = iconImg(w.src, '', w.name) - const cell = el('div', { class: 'preview-cell' }, im, el('div', { class: 'photo-name', text: w.name })) - cell.addEventListener('click', () => { Apps.open('preview', { asset: w.src, name: w.name }) }) - gallery.append(cell) - }) - stage.append(gallery); infoEl.textContent = '点击查看大图'; WM.setTitle(win, '图片集 — 预览'); return - } - const asset = args.asset || null - const ext = (st.path.split('.').pop() || '').toLowerCase() - if (asset || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) { - const src = asset || st.path - contentEl = el('img', { class: 'preview-img', alt: args.name || FS.baseName(src) }) as HTMLElement - ;(contentEl as any).addEventListener('error', () => showError('图片数据不可用(虚拟文件或资源缺失)。')) - ;(contentEl as HTMLImageElement).src = src - stage.append(el('div', { class: 'preview-canvas' }, contentEl)) - WM.setTitle(win, (args.name || FS.baseName(src)) + ' — 预览'); apply() - } else if (ext === 'pdf') { - contentEl = el('div', { class: 'preview-pdf' }, el('div', { class: 'preview-pdf-badge', text: 'PDF' }), el('div', { class: 'preview-pdf-name', text: FS.baseName(st.path) }), el('div', { class: 'preview-pdf-sub', text: '虚拟 PDF 文档 · 第 1 页,共 1 页' })) - st.isPdf = true; stage.append(el('div', { class: 'preview-canvas' }, contentEl)); apply() - } else { - try { const txt = FS.read(st.path); contentEl = el('pre', { class: 'preview-text', text: txt }); stage.append(el('div', { class: 'preview-canvas' }, contentEl)); apply() } catch (e: any) { showError(e.message) } - } - } + render(win: any) { + const vnode = h(PreviewComponent, { win }) + render(vnode, win.body) + }, } Apps.register(PreviewApp) diff --git a/tests/cases/26-preview.test.ts b/tests/cases/26-preview.test.ts new file mode 100644 index 0000000..c4ff30b --- /dev/null +++ b/tests/cases/26-preview.test.ts @@ -0,0 +1,36 @@ +/** + * 26-preview: Preview Vue 组件化测试 + */ +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { h, render, nextTick } from 'vue' +import PreviewComponent from '../../src/apps/preview/Preview.vue' +import { setupDOM } from '../helpers' + +function makeMockWin() { + const el = document.createElement('div'); el.className = 'window' + document.getElementById('window-layer')?.appendChild(el) + const body = document.createElement('div'); body.className = 'win-body' + el.appendChild(body) + return { id: 'pv', appId: 'preview', el, body, timers: [] as any[], data: {} } +} + +describe('26-preview — Preview Vue 组件化', () => { + let win: any + beforeEach(() => { setupDOM(); win = makeMockWin() }) + afterEach(() => { render(null, win.body); win.el.remove() }) + + function mount() { const v = h(PreviewComponent, { win }); render(v, win.body) } + + it('挂载后渲染画廊', () => { mount(); expect(win.body.querySelector('.preview-gallery')).toBeTruthy() }) + it('工具栏有缩放/旋转按钮', () => { + mount() + expect(win.body.querySelector('.fb-btn[title="放大"]')).toBeTruthy() + expect(win.body.querySelector('.fb-btn[title="缩小"]')).toBeTruthy() + }) + it('暴露 zoom/zoomReset 给菜单', () => { + mount() + expect(typeof win.appState.zoom).toBe('function') + expect(typeof win.appState.zoomReset).toBe('function') + }) + it('卸载后 DOM 为空', () => { mount(); render(null, win.body); expect(win.body.children.length).toBe(0) }) +})