diff --git a/package.json b/package.json index 5116e8002..f3befb288 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,6 @@ "highlight.js": "^11.11.1", "hls.js": "^1.6.16", "ini": "^7.0.0", - "just-once": "^2.2.0", "katex": "^0.16.45", "libheif-js": "^1.19.8", "lightgallery": "^2.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6fca3a96..ad0444211 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,9 +95,6 @@ importers: ini: specifier: ^7.0.0 version: 7.0.0 - just-once: - specifier: ^2.2.0 - version: 2.2.0 katex: specifier: ^0.16.45 version: 0.16.45 @@ -2589,9 +2586,6 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} - just-once@2.2.0: - resolution: {integrity: sha512-Wo547FgUOUZ98jbrZ1KX8nRezdEdtgIlC2NK1u1RvR1oZ/WoU++FjprP8J8hRbaox776MHyeMZZED4DvhhHVjg==} - katex@0.16.45: resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} hasBin: true @@ -3697,9 +3691,9 @@ snapshots: '@babel/helper-compilation-targets@7.28.6': dependencies: - '@babel/compat-data': 7.29.0 + '@babel/compat-data': 7.29.3 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 + browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -6260,8 +6254,6 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 - just-once@2.2.0: {} - katex@0.16.45: dependencies: commander: 8.3.0 diff --git a/src/app/index.css b/src/app/index.css index ce79082f0..0698907d2 100644 --- a/src/app/index.css +++ b/src/app/index.css @@ -48,3 +48,8 @@ .hope-select__option { flex-shrink: 0; } + +/* Fixes: https://github.com/OpenListTeam/OpenList-Frontend/issues/26 */ +.hope-notification__list { + width: unset; +} diff --git a/src/components/Base.tsx b/src/components/Base.tsx index b065f693d..30c996b88 100644 --- a/src/components/Base.tsx +++ b/src/components/Base.tsx @@ -14,12 +14,24 @@ import { SelectOptionText, SelectTrigger, SelectValue, - Icon, + IconButton, + Tooltip, + VStack, } from "@hope-ui/solid" import { SwitchColorMode } from "./SwitchColorMode" -import { ComponentProps, For, mergeProps, Show, JSXElement } from "solid-js" +import { + ComponentProps, + For, + mergeProps, + Show, + JSXElement, + createSignal, + onMount, + onCleanup, +} from "solid-js" import { AiOutlineFullscreen, AiOutlineFullscreenExit } from "solid-icons/ai" -import { hoverColor } from "~/utils" +import { BsFullscreen, BsFullscreenExit } from "solid-icons/bs" +import { useT } from "~/hooks" export const Error = (props: { msg: string @@ -63,35 +75,109 @@ export const Error = (props: { ) } -export const BoxWithFullScreen = (props: Parameters[0]) => { - const { isOpen, onToggle } = createDisclosure() +export const BoxWithFullScreen = ( + props: Parameters[0] & { extraButtons?: JSXElement }, +) => { + const { isOpen: isFullView, onToggle } = createDisclosure() + const [isFullScreen, setIsFullScreen] = createSignal(false) + let containerRef: HTMLDivElement + const t = useT() + + const toggleFullscreen = () => { + if (!document.fullscreenElement) { + containerRef!.requestFullscreen() + } else { + document.exitFullscreen() + } + } + + onMount(() => { + const fsHandler = () => setIsFullScreen(!!document.fullscreenElement) + document.addEventListener("fullscreenchange", fsHandler) + onCleanup(() => { + document.removeEventListener("fullscreenchange", fsHandler) + }) + }) + return ( {props.children} - + spacing="$2" + opacity="0.7" + _hover={{ opacity: "1" }} + transition="opacity 0.3s ease" + pointerEvents="auto" + zIndex="$docked" + > + {props.extraButtons} + + {/* Full view toggle */} + + : } + onClick={onToggle} + colorScheme="neutral" + size="sm" + /> + + + + {/* Native fullscreen toggle */} + + + ) : ( + + ) + } + onClick={toggleFullscreen} + colorScheme="neutral" + size="sm" + /> + + ) } diff --git a/src/components/EncodingSelect.tsx b/src/components/EncodingSelect.tsx index 260b175dc..ae79f1ccb 100644 --- a/src/components/EncodingSelect.tsx +++ b/src/components/EncodingSelect.tsx @@ -80,7 +80,7 @@ export function EncodingSelect(props: { _hover={{ opacity: 1, }} - zIndex={1} + zIndex="$docked" > ({ diff --git a/src/components/Markdown.tsx b/src/components/Markdown.tsx index e1c66c47f..803842ad1 100644 --- a/src/components/Markdown.tsx +++ b/src/components/Markdown.tsx @@ -1,7 +1,6 @@ import { Anchor, Box, List, ListItem, useColorModeValue } from "@hope-ui/solid" import { createStorageSignal } from "@solid-primitives/storage" import { clsx } from "clsx" -import once from "just-once" import rehypeRaw from "rehype-raw" import rehypeSanitize, { defaultSchema } from "rehype-sanitize" import rehypeStringify from "rehype-stringify" @@ -14,7 +13,15 @@ import { unified } from "unified" import { useCDN, useParseText, useRouter } from "~/hooks" import { useScrollListener } from "~/pages/home/toolbar/BackTop.jsx" import { getMainColor, getSettingBool, me } from "~/store" -import { api, notify, pathDir, pathJoin, pathResolve } from "~/utils" +import { + api, + loadCSS, + loadScriptIIFE, + notify, + pathDir, + pathJoin, + pathResolve, +} from "~/utils" import { isMobile } from "~/utils/compatibility.js" import hljs from "highlight.js" import { EncodingSelect } from "." @@ -148,25 +155,6 @@ function MarkdownToc(props: { const { katexCSSPath, mermaidJSPath } = useCDN() -const insertKatexCSS = once(() => { - const link = document.createElement("link") - link.rel = "stylesheet" - link.href = katexCSSPath() - document.head.appendChild(link) -}) - -const loadMermaidJS = once( - () => - new Promise((resolve, reject) => { - if (window.mermaid) return resolve() - const script = document.createElement("script") - script.src = mermaidJSPath() - script.onload = () => resolve() - script.onerror = () => reject(new Error("Failed to load mermaid")) - document.body.appendChild(script) - }), -) - async function renderMarkdown( content: string, sanitize: boolean, @@ -180,15 +168,17 @@ async function renderMarkdown( if (hasMath) { const { default: remarkMath } = await import("remark-math") processor.use(remarkMath) - insertKatexCSS() - } - if (hasMermaid) { - await loadMermaidJS().catch(() => + await loadCSS(katexCSSPath(), "katex").catch(() => notify.error( - "Failed to load mermaid.js, mermaid diagrams will not be rendered", + "Failed to load KaTeX CSS, math formulas will not be rendered", ), ) } + if (hasMermaid) { + await loadScriptIIFE(mermaidJSPath(), "mermaid").catch(() => + notify.error("Failed to load Mermaid JS, diagrams will not be rendered"), + ) + } processor.use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw) diff --git a/src/components/SwitchLanguage.tsx b/src/components/SwitchLanguage.tsx index 9611ec387..e4c1d9f91 100644 --- a/src/components/SwitchLanguage.tsx +++ b/src/components/SwitchLanguage.tsx @@ -59,7 +59,7 @@ export const SwitchLanguage = ( pos="fixed" top={0} bg={useColorModeValue("$blackAlpha4", "$whiteAlpha4")()} - zIndex="9000" + zIndex="$notification" > { const static_path = joinBase("static") + // OpenList Resource CDN: https://github.com/OpenListTeam/OpenList-Resource + const resource = "https://res.oplist.org.cn" + + // npmmirror CDN, whitelist + // Available: https://github.com/cnpm/unpkg-white-list const npm = (name: string, version: string, path: string) => { - // Available: https://github.com/cnpm/unpkg-white-list // https://registry.npmmirror.com/monaco-editor/0.55.1/files/min/vs/loader.js return `https://registry.npmmirror.com/${name}/${version}/files/${path}` @@ -13,49 +21,74 @@ export const useCDN = () => { // return `https://cdn.jsdelivr.net/npm/${name}@${version}/${path}` } + // Read version from package.json dependencies (strips ^ ~ prefixes) + const dep = (name: string) => { + const ver = (pkgDeps as Record)[name] + if (!ver) + throw new Error( + `[useCDN] "${name}" not found in package.json dependencies`, + ) + return ver.replace(/^[^\d]*/, "") + } + + const res = (path: string) => { + return `${resource}/${path}` + } + const monacoPath = () => { return import.meta.env.VITE_LITE === "true" - ? npm("monaco-editor", "0.55.1", "min/vs") + ? npm("monaco-editor", dep("monaco-editor"), "min/vs") : `${static_path}/monaco-editor/vs` } const katexCSSPath = () => { return import.meta.env.VITE_LITE === "true" - ? npm("katex", "0.16.45", "dist/katex.min.css") + ? npm("katex", dep("katex"), "dist/katex.min.css") : `${static_path}/katex/katex.min.css` } const mermaidJSPath = () => { return import.meta.env.VITE_LITE === "true" - ? npm("mermaid", "11.15.0", "dist/mermaid.min.js") + ? npm("mermaid", dep("mermaid"), "dist/mermaid.min.js") : `${static_path}/mermaid/mermaid.min.js` } const libHeifPath = () => { return import.meta.env.VITE_LITE === "true" - ? npm(packageJson.name, packageJson.version, "dist/static/libheif") + ? npm(pkgName, pkgVersion, "dist/static/libheif") : `${static_path}/libheif` } const libAssPath = () => { return import.meta.env.VITE_LITE === "true" - ? npm(packageJson.name, packageJson.version, "dist/static/libass-wasm") + ? npm(pkgName, pkgVersion, "dist/static/libass-wasm") : `${static_path}/libass-wasm` } const fontsPath = () => { return import.meta.env.VITE_LITE === "true" - ? npm(packageJson.name, packageJson.version, "dist/static/fonts") + ? npm(pkgName, pkgVersion, "dist/static/fonts") : `${static_path}/fonts` } + // Office preview libs — always served from resource CDN (not bundled locally) + const pptBasePath = () => res("ppt.js") + const docxPreviewPath = () => res("docxjs/dist/docx-preview.min.js") + const excelJSPath = () => res("exceljs/exceljs.min.js") + const rufflePath = () => res("ruffle/ruffle.js") + return { npm, + res, monacoPath, katexCSSPath, mermaidJSPath, libHeifPath, libAssPath, fontsPath, + pptBasePath, + docxPreviewPath, + excelJSPath, + rufflePath, } } diff --git a/src/lang/en/home.json b/src/lang/en/home.json index 64a73c8a4..104cc2e83 100644 --- a/src/lang/en/home.json +++ b/src/lang/en/home.json @@ -23,6 +23,13 @@ "tr-installing": "TrollStore Installing", "open_in_new_window": "Open in new window", "auto_next": "Auto next", + "fullscreen": "Fullscreen", + "fullscreen_failed": "Failed to toggle fullscreen mode", + "exit_fullscreen": "Exit fullscreen", + "fullview": "Full view", + "exit_fullview": "Exit Full view", + "auto_fit": "Auto fit", + "reset_zoom": "Reset zoom", "names": { "download": "Direct Download", "html": "HTML Render", @@ -37,7 +44,6 @@ "audio": "Audio Player", "ipa": "IPA Installer", "plist": "Plist Installer", - "heic": "HEIC Viewer", "pdf": "PDF Previewer", "ppt": "PPT Previewer", "xls": "XLS Previewer", diff --git a/src/pages/home/previews/doc.tsx b/src/pages/home/previews/doc.tsx index 775ce7f0a..db0ac1fef 100644 --- a/src/pages/home/previews/doc.tsx +++ b/src/pages/home/previews/doc.tsx @@ -1,9 +1,13 @@ import { BoxWithFullScreen, FullLoading, Error as Erro } from "~/components" -import { objStore } from "~/store" -import { Box, IconButton, Tooltip } from "@hope-ui/solid" +import { Box, Button, IconButton, Tooltip } from "@hope-ui/solid" +import { loadScriptIIFE } from "~/utils" import { createSignal, onMount, onCleanup, Show } from "solid-js" -import { useT } from "~/hooks" -import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" +import { useLink, useT, useCDN } from "~/hooks" + +import { + HiOutlineMagnifyingGlassPlus, + HiOutlineMagnifyingGlassMinus, +} from "solid-icons/hi" // 声明全局docx类型 declare global { @@ -14,50 +18,15 @@ declare global { const DocViewerApp = () => { const t = useT() + const { currentObjLink } = useLink() + const { npm, docxPreviewPath } = useCDN() const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) - const [isFullscreen, setIsFullscreen] = createSignal(false) + // null = auto-fit, number = manual zoom level + const [zoom, setZoom] = createSignal(null) let containerRef: HTMLDivElement | undefined let resultRef: HTMLDivElement | undefined - // 加载外部脚本 - const loadScript = (src: string, id: string): Promise => { - return new Promise((resolve, reject) => { - // 检查脚本是否已加载 - if (document.getElementById(id)) { - resolve() - return - } - - const script = document.createElement("script") - script.src = src - script.id = id - script.type = "text/javascript" - script.onload = () => resolve() - script.onerror = () => reject(new Error(`Failed to load script: ${src}`)) - document.head.appendChild(script) - }) - } - - // 加载CSS文件 - const loadCSS = (href: string, id: string): Promise => { - return new Promise((resolve, reject) => { - // 检查CSS是否已加载 - if (document.getElementById(id)) { - resolve() - return - } - - const link = document.createElement("link") - link.rel = "stylesheet" - link.href = href - link.id = id - link.onload = () => resolve() - link.onerror = () => reject(new Error(`Failed to load CSS: ${href}`)) - document.head.appendChild(link) - }) - } - // 初始化DOCX预览 const initDocViewer = async () => { try { @@ -65,14 +34,13 @@ const DocViewerApp = () => { setError(false) // 加载jszip和docx-preview库 - await loadScript( - "https://unpkg.com/jszip/dist/jszip.min.js", - "jszip-script", - ) - await loadScript( - "https://res.oplist.org.cn/docxjs/dist/docx-preview.min.js", - "docx-preview-script", + // 加载前清理其他版本的 jszip,避免全局变量冲突 + document.getElementById("jszip-2.6.1-script")?.remove() + await loadScriptIIFE( + npm("jszip", "3.10.1", "dist/jszip.min.js"), + "jszip-3.10.1-script", ) + await loadScriptIIFE(docxPreviewPath(), "docx-preview-script") // 等待docx库加载完成 if (!window.docx) { @@ -80,7 +48,7 @@ const DocViewerApp = () => { } // 获取文件URL并下载 - const fileUrl = objStore.raw_url + const fileUrl = currentObjLink() const response = await fetch(fileUrl) if (!response.ok) { throw new Error("Failed to fetch document file") @@ -118,66 +86,102 @@ const DocViewerApp = () => { } } - // 全屏切换 - const toggleFullscreen = () => { - if (!containerRef) return - - if (!document.fullscreenElement) { - containerRef.requestFullscreen().then(() => { - setIsFullscreen(true) - }) + // 应用缩放 + const applyScale = () => { + if (!resultRef || !containerRef) return + const wrapper = resultRef.querySelector( + ".docx-preview-container", + ) as HTMLElement + if (!wrapper) return + const z = zoom() + if (z === null) { + // auto-fit: 缩放到容器宽度 + const containerWidth = containerRef.clientWidth + const contentWidth = wrapper.scrollWidth + if (contentWidth > containerWidth) { + wrapper.style.zoom = `${containerWidth / contentWidth}` + } else { + wrapper.style.zoom = "" + } } else { - document.exitFullscreen().then(() => { - setIsFullscreen(false) - }) + wrapper.style.zoom = `${z}` } } - // 监听全屏变化 - const handleFullscreenChange = () => { - if (!document.fullscreenElement) { - setIsFullscreen(false) - } + // 缩放控制 + const zoomStep = 0.1 + const zoomIn = () => { + const current = zoom() ?? 1 + setZoom(Math.min(current + zoomStep, 3)) + applyScale() + } + const zoomOut = () => { + const current = zoom() ?? 1 + setZoom(Math.max(current - zoomStep, 0.3)) + applyScale() + } + const zoomReset = () => { + setZoom(null) + applyScale() + } + + const setupResponsiveScale = () => { + if (!resultRef || !containerRef) return + const result = resultRef + const container = containerRef + const observer = new ResizeObserver(() => { + if (zoom() === null) applyScale() + }) + observer.observe(container) + onCleanup(() => observer.disconnect()) } onMount(() => { initDocViewer() - document.addEventListener("fullscreenchange", handleFullscreenChange) - }) - - onCleanup(() => { - document.removeEventListener("fullscreenchange", handleFullscreenChange) - // 清理加载的脚本和样式(可选) + setupResponsiveScale() }) return ( - {/* 全屏按钮 */} + {/* 缩放控制 */} + } + onClick={zoomOut} + /> - : } - onClick={toggleFullscreen} - /> + + } + onClick={zoomIn} + /> {/* DOCX容器 */} @@ -195,8 +199,7 @@ const DocViewerApp = () => { ref={resultRef} id="docx-container" style={{ - width: "100%", - height: "100%", + "min-width": "0", padding: "20px", display: loading() || error() ? "none" : "block", }} diff --git a/src/pages/home/previews/flash.tsx b/src/pages/home/previews/flash.tsx index 7c52baecf..886c6bc53 100644 --- a/src/pages/home/previews/flash.tsx +++ b/src/pages/home/previews/flash.tsx @@ -1,6 +1,7 @@ -import { Error, FullLoading } from "~/components" -import { useRouter, useT } from "~/hooks" +import { BoxWithFullScreen, Error, FullLoading } from "~/components" +import { useCDN, useRouter, useT } from "~/hooks" import { objStore } from "~/store" +import { loadScriptIIFE } from "~/utils" import { onCleanup, onMount, createSignal, Show } from "solid-js" const Preview = () => { @@ -8,7 +9,7 @@ const Preview = () => { const { replace } = useRouter() const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) - + const { rufflePath } = useCDN() // 获取当前目录下所有SWF文件 let swfFiles = objStore.objs.filter((obj) => obj.name.toLowerCase().endsWith(".swf"), @@ -41,7 +42,7 @@ const Preview = () => { ruffleScript?.remove() }) - const initRufflePlayer = () => { + const initRufflePlayer = async () => { setLoading(true) setError(false) @@ -49,30 +50,16 @@ const Preview = () => { const oldPlayer = document.getElementById("ruffle-player") oldPlayer?.remove() - // 检查是否已加载Ruffle - if (window.RufflePlayer) { - createPlayer() - return - } - // 动态加载Ruffle脚本 - const script = document.createElement("script") - // script.src = "https://unpkg.com/@ruffle-rs/ruffle" - script.src = "https://res.oplist.org.cn/ruffle/ruffle.js" - script.async = true - script.id = "ruffle-script" - - script.onload = () => { - createPlayer() - } - - script.onerror = () => { - setError(true) - setLoading(false) - console.error("无法加载Ruffle播放器") - } - - document.head.appendChild(script) + await loadScriptIIFE(rufflePath(), "ruffle-script") + .then(() => { + createPlayer() + }) + .catch((err) => { + console.error("Failed to load Ruffle script:", err) + setError(true) + setLoading(false) + }) } const createPlayer = () => { @@ -107,27 +94,29 @@ const Preview = () => { } return ( -
- {/* 加载状态 */} - - - - - {/* 错误状态 */} - - - -
+ +
+ {/* 加载状态 */} + + + + + {/* 错误状态 */} + + + +
+
) } diff --git a/src/pages/home/previews/heic.tsx b/src/pages/home/previews/heic.tsx deleted file mode 100644 index 0f14cf7a1..000000000 --- a/src/pages/home/previews/heic.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { Error, FullLoading } from "~/components" -import { useCDN, useRouter, useT } from "~/hooks" -import { objStore } from "~/store" -import { onCleanup, onMount, createSignal, Show } from "solid-js" - -const Preview = () => { - const t = useT() - const { replace } = useRouter() - const [loading, setLoading] = createSignal(true) - const [error, setError] = createSignal(false) - const { libHeifPath } = useCDN() - - // 获取当前目录下所有HEIC文件 - let heicFiles = objStore.objs.filter((obj) => - /\.(heic|heif|avif|vvc|avc|jpeg|jpg)$/i.test(obj.name), - ) - - if (heicFiles.length === 0) { - heicFiles = [objStore.obj] - } - - // // 键盘导航功能:左右箭头切换文件 - // const onKeydown = (e: KeyboardEvent) => { - // const index = heicFiles.findIndex((f) => f.name === objStore.obj.name) - // if (e.key === "ArrowLeft" && index > 0) { - // replace(heicFiles[index - 1].name) - // } else if (e.key === "ArrowRight" && index < heicFiles.length - 1) { - // replace(heicFiles[index + 1].name) - // } - // } - - let libheif: any - let decoder: any - let canvas: HTMLCanvasElement | undefined - - onMount(() => { - // window.addEventListener("keydown", onKeydown) - initLibheif() - }) - - onCleanup(() => { - // window.removeEventListener("keydown", onKeydown) - if (libheif && decoder) { - // decoder.free() - decoder = null - } - libheif = null - }) - - // 初始化libheif库 - const initLibheif = async () => { - setLoading(true) - setError(false) - - try { - // 动态加载libheif脚本 - if (!window.libheif) { - await loadScript(`${libHeifPath()}/libheif.js`, "libheif-script") - } - - // 加载WASM文件 - const wasmBinary = await fetchWasm(`${libHeifPath()}/libheif.wasm`) - - // 初始化libheif - libheif = window.libheif({ wasmBinary }) - decoder = new libheif.HeifDecoder() - - // 加载并解码当前HEIC文件 - await loadAndDecode(objStore.raw_url) - } catch (e) { - console.error("HEIC初始化失败:", e) - setError(true) - setLoading(false) - } - } - - // 加载脚本 - const loadScript = (src: string, id: string) => - new Promise((resolve, reject) => { - const script = document.createElement("script") - script.src = src - script.id = id - script.onload = () => resolve() - script.onerror = () => reject(`脚本加载失败: ${src}`) - document.head.appendChild(script) - }) - - // 获取WASM文件 - const fetchWasm = async (url: string) => { - const response = await fetch(url) - if (!response.ok) throw `WASM加载失败: ${url}` - return await response.arrayBuffer() - } - - // 加载并解码HEIC文件 - const loadAndDecode = async (url: string) => { - try { - setLoading(true) - setError(false) - - // 获取HEIC文件 - const response = await fetch(url) - if (!response.ok) throw "文件获取失败" - const buffer = await response.arrayBuffer() - - // 解码HEIC文件 - const images = decoder.decode(buffer) - if (!images || images.length === 0) { - throw "没有可解码的图像" - } - - // 显示第一张图像 - const image = images[0] - await displayImage(image) - setLoading(false) - } catch (e) { - console.error("HEIC解码失败:", e) - setError(true) - setLoading(false) - } - } - - // 在canvas上显示图像 - const displayImage = (image: any) => { - return new Promise((resolve) => { - if (!canvas) return resolve() - - const width = image.get_width() - const height = image.get_height() - - // 调整canvas尺寸 - canvas.width = width - canvas.height = height - - // 创建ImageData对象 - const imageData = new ImageData(width, height) - - // 显示图像 - image.display(imageData, (displayData: ImageData | null) => { - if (!displayData || !canvas) return resolve() - - const ctx = canvas.getContext("2d") - if (!ctx) return resolve() - - ctx.putImageData(displayData, 0, 0) - resolve() - }) - }) - } - - return ( -
- - - {/* 加载状态 */} - - - - - {/* 错误状态 */} - - - -
- ) -} - -export default Preview diff --git a/src/pages/home/previews/iframe.tsx b/src/pages/home/previews/iframe.tsx index c35ed7748..8b5e0d93b 100644 --- a/src/pages/home/previews/iframe.tsx +++ b/src/pages/home/previews/iframe.tsx @@ -1,7 +1,7 @@ import { BoxWithFullScreen } from "~/components" import { objStore } from "~/store" -import { Icon, hope } from "@hope-ui/solid" -import { convertURL, hoverColor } from "~/utils" +import { hope, Tooltip, IconButton } from "@hope-ui/solid" +import { convertURL } from "~/utils" import { Component, createMemo } from "solid-js" import { useLink } from "~/hooks" import { TbExternalLink } from "solid-icons/tb" @@ -17,23 +17,24 @@ const IframePreview = (props: { scheme: string }) => { }) }) return ( - + + } + onClick={() => { + window.open(iframeSrc(), "_blank") + }} + colorScheme="neutral" + size="sm" + /> + + } + > - { - window.open(iframeSrc(), "_blank") - }} - cursor="pointer" - rounded="$md" - bgColor={hoverColor()} - p="$1" - boxSize="$7" - /> ) } diff --git a/src/pages/home/previews/image.tsx b/src/pages/home/previews/image.tsx index 44bf9a21b..e9031986d 100644 --- a/src/pages/home/previews/image.tsx +++ b/src/pages/home/previews/image.tsx @@ -1,66 +1,570 @@ -import { Error, FullLoading, ImageWithError } from "~/components" -import { useRouter, useT } from "~/hooks" +import { + Box, + Center, + Flex, + HStack, + IconButton, + Spacer, + Text, + Tooltip, + VStack, +} from "@hope-ui/solid" +import { + BsArrowClockwise, + BsArrowCounterclockwise, + BsInfoCircle, + BsZoomIn, + BsZoomOut, +} from "solid-icons/bs" +import { FaSolidAngleLeft, FaSolidAngleRight } from "solid-icons/fa" +import { + TbArrowAutofitHeight, + TbArrowAutofitWidth, + TbArrowAutofitContent, +} from "solid-icons/tb" +import { + createEffect, + createSignal, + Match, + onCleanup, + onMount, + Show, + Switch, +} from "solid-js" +import { + BoxWithFullScreen, + Error, + FullLoading, + ImageWithError, +} from "~/components" +import { useCDN, useRouter, useT } from "~/hooks" import { objStore } from "~/store" import { Obj, ObjType } from "~/types" -import { onCleanup, onMount } from "solid-js" +import { ext, formatDate, getFileSize, loadScriptIIFE } from "~/utils" + +const HEIF_EXTS = new Set(["heic", "heif", "avif", "vvc", "avc"]) +const isHeif = (name: string) => HEIF_EXTS.has(ext(name)) +const ZOOM_MIN = 0.1 +const ZOOM_MAX = 10 +const ZOOM_WHEEL_FACTOR = 1.08 +const ZOOM_BTN_STEP = 0.25 interface PreviewProps { images?: Obj[] navigate?: (name: string) => void } +// ── HEIF decoder ──────────────────────────────────────────────────── +const HeifView = (props: { + src: string + onLoad?: (w: number, h: number) => void + style?: any +}) => { + const t = useT() + const { libHeifPath } = useCDN() + const [loading, setLoading] = createSignal(true) + const [error, setError] = createSignal(false) + let canvas: HTMLCanvasElement | undefined + let libheif: any + let decoder: any + + const decode = async (url: string) => { + try { + setLoading(true) + setError(false) + if (!window.libheif) { + await loadScriptIIFE(`${libHeifPath()}/libheif.js`, "libheif-script") + } + if (!libheif) { + const wasm = await fetch(`${libHeifPath()}/libheif.wasm`).then((r) => { + if (!r.ok) throw "WASM load failed" + return r.arrayBuffer() + }) + libheif = window.libheif({ wasmBinary: wasm }) + decoder = new libheif.HeifDecoder() + } + const buffer = await fetch(url).then((r) => { + if (!r.ok) throw "File fetch failed" + return r.arrayBuffer() + }) + const images = decoder.decode(buffer) + if (!images?.length) throw "No decodable image" + const img = images[0] + const w = img.get_width() + const h = img.get_height() + if (!canvas) return + canvas.width = w + canvas.height = h + const imageData = new ImageData(w, h) + await new Promise((resolve) => { + img.display(imageData, (data: ImageData | null) => { + if (!data || !canvas) return resolve() + canvas.getContext("2d")?.putImageData(data, 0, 0) + resolve() + }) + }) + props.onLoad?.(w, h) + setLoading(false) + } catch (e) { + console.error("HEIF decode failed:", e) + setError(true) + setLoading(false) + } + } + + createEffect(() => { + if (props.src) decode(props.src) + }) + onCleanup(() => { + decoder = null + libheif = null + }) + + return ( + <> + + + + + + + + + ) +} + +// ── Preview ───────────────────────────────────────────────────────── const Preview = (props: PreviewProps) => { const t = useT() const { replace } = useRouter() + + const [scale, setScale] = createSignal(1) + const [rotation, setRotation] = createSignal(0) + const [fitMode, setFitMode] = createSignal<"contain" | "height" | "width">( + "contain", + ) + const [tx, setTx] = createSignal(0) + const [ty, setTy] = createSignal(0) + const [dragging, setDragging] = createSignal(false) + const [showInfo, setShowInfo] = createSignal(false) + const [imgSize, setImgSize] = createSignal({ w: 0, h: 0 }) + const [isFullscreen, setIsFullscreen] = createSignal(false) + + let containerRef!: HTMLDivElement + let areaRef!: HTMLDivElement + let dragOX = 0 + let dragOY = 0 + let startTx = 0 + let startTy = 0 + let images = - props.images || objStore.objs.filter((obj) => obj.type === ObjType.IMAGE) - if (images.length === 0) { - images = [objStore.obj] - } + props.images || + objStore.objs.filter((o) => o.type === ObjType.IMAGE || isHeif(o.name)) + if (images.length === 0) images = [objStore.obj] + + const curIdx = () => images.findIndex((f) => f.name === objStore.obj.name) + // ── reset on image change ── + createEffect(() => { + objStore.obj.name + setScale(1) + setRotation(0) + setTx(0) + setTy(0) + setImgSize({ w: 0, h: 0 }) + }) + + // ── navigation ── + const goTo = (obj: Obj) => { + if (props.navigate) props.navigate(obj.name) + else replace(obj.name) + } const prev = () => { - const index = images.findIndex((f) => f.name === objStore.obj.name) - if (index > 0) { - if (props.navigate) { - props.navigate(images[index - 1].name) - } else { - replace(images[index - 1].name) - } - } + const i = curIdx() + if (i > 0) goTo(images[i - 1]) } - const next = () => { - const index = images.findIndex((f) => f.name === objStore.obj.name) - if (index < images.length - 1) { - if (props.navigate) { - props.navigate(images[index + 1].name) - } else { - replace(images[index + 1].name) - } - } + const i = curIdx() + if (i < images.length - 1) goTo(images[i + 1]) } - const onKeydown = (e: KeyboardEvent) => { - if (e.key === "ArrowLeft") { - prev() - } else if (e.key === "ArrowRight") { - next() + // ── transforms ── + const resetTransform = () => { + setScale(1) + setRotation(0) + setTx(0) + setTy(0) + } + const zoomIn = () => setScale((s) => Math.min(s + ZOOM_BTN_STEP, ZOOM_MAX)) + const zoomOut = () => setScale((s) => Math.max(s - ZOOM_BTN_STEP, ZOOM_MIN)) + const rotL = () => setRotation((r) => r - 90) + const rotR = () => setRotation((r) => r + 90) + const fitPage = () => { + setFitMode("contain") + setScale(1) + setTx(0) + setTy(0) + } + const fitHeight = () => { + setFitMode("height") + setScale(1) + setTx(0) + setTy(0) + } + const fitWidth = () => { + setFitMode("width") + setScale(1) + setTx(0) + setTy(0) + } + + // ── wheel zoom (towards cursor) ── + const onWheel = (e: WheelEvent) => { + e.preventDefault() + const rect = areaRef.getBoundingClientRect() + const cx = e.clientX - rect.left - rect.width / 2 + const cy = e.clientY - rect.top - rect.height / 2 + const oldS = scale() + const factor = e.deltaY < 0 ? ZOOM_WHEEL_FACTOR : 1 / ZOOM_WHEEL_FACTOR + const newS = Math.min(Math.max(oldS * factor, ZOOM_MIN), ZOOM_MAX) + const r = newS / oldS + setTx(cx - r * (cx - tx())) + setTy(cy - r * (cy - ty())) + setScale(newS) + } + + // ── drag ── + const onMouseDown = (e: MouseEvent) => { + if (scale() <= 1 || e.button !== 0) return + e.preventDefault() + setDragging(true) + dragOX = e.clientX + dragOY = e.clientY + startTx = tx() + startTy = ty() + } + const onMouseMove = (e: MouseEvent) => { + if (!dragging()) return + setTx(startTx + (e.clientX - dragOX)) + setTy(startTy + (e.clientY - dragOY)) + } + const onMouseUp = () => setDragging(false) + const onDblClick = () => (scale() === 1 ? setScale(2) : resetTransform()) + + // ── image load ── + const onImgLoad = (e: Event) => { + const img = e.target as HTMLImageElement + setImgSize({ w: img.naturalWidth, h: img.naturalHeight }) + } + const onHeifLoad = (w: number, h: number) => setImgSize({ w, h }) + + // ── keyboard ── + const onKey = (e: KeyboardEvent) => { + switch (e.key) { + case "ArrowLeft": + return prev() + case "ArrowRight": + return next() + case "+": + case "=": + return zoomIn() + case "-": + return zoomOut() + case "r": + return rotL() + case "R": + return rotR() + case "0": + return resetTransform() + case "i": + return setShowInfo((v) => !v) + case "c": + return fitPage() + case "h": + return fitHeight() + case "w": + return fitWidth() + case "f": + // TODO toggleFs() } } + + // ── fullscreen detection ── + const updateFullscreen = () => { + const native = !!document.fullscreenElement + setIsFullscreen(native) + } + onMount(() => { - window.addEventListener("keydown", onKeydown) + window.addEventListener("keydown", onKey) + areaRef?.addEventListener("wheel", onWheel, { passive: false }) + document.addEventListener("fullscreenchange", updateFullscreen) + updateFullscreen() }) onCleanup(() => { - window.removeEventListener("keydown", onKeydown) + window.removeEventListener("keydown", onKey) + areaRef?.removeEventListener("wheel", onWheel) + document.removeEventListener("fullscreenchange", updateFullscreen) }) + + const imgTransform = () => + `translate(${tx()}px,${ty()}px) scale(${scale()}) rotate(${rotation()}deg)` + const cursor = () => + scale() > 1 ? (dragging() ? "grabbing" : "grab") : "default" + + // ── render ────────────────────────────────────────────────────── return ( - } - fallbackErr={} - /> + + + {/* ── Toolbar ── */} + + + 0}> + + } + aria-label="Previous" + variant="ghost" + size="sm" + onClick={prev} + /> + + + + + } + aria-label="Next" + variant="ghost" + size="sm" + onClick={next} + /> + + + + {objStore.obj.name} + + 1}> + + {curIdx() + 1}/{images.length} + + + + + + + } + aria-label="Info" + variant={showInfo() ? "subtle" : "ghost"} + size="sm" + onClick={() => setShowInfo((v) => !v)} + /> + + + + } + aria-label="Zoom out" + variant="ghost" + size="sm" + onClick={zoomOut} + /> + + + } + aria-label="Zoom in" + variant="ghost" + size="sm" + onClick={zoomIn} + /> + + + } + aria-label="Fit page" + variant="ghost" + size="sm" + onClick={fitPage} + /> + + + } + aria-label="Fit height" + variant="ghost" + size="sm" + onClick={fitHeight} + /> + + + } + aria-label="Fit width" + variant="ghost" + size="sm" + onClick={fitWidth} + /> + + + } + aria-label="Rotate left" + variant="ghost" + size="sm" + onClick={rotL} + /> + + + } + aria-label="Rotate right" + variant="ghost" + size="sm" + onClick={rotR} + /> + + + + + {/* ── Image area ── */} +
+
+ } + fallbackErr={ + + } + onLoad={onImgLoad} + {...(fitMode() === "contain" + ? { w: "$full", h: "$full", objectFit: "contain" } + : { + css: + fitMode() === "height" + ? { + height: "100%", + width: "auto", + "max-width": "none", + } + : { + width: "100%", + height: "auto", + "max-height": "none", + }, + })} + /> + } + > + + + + +
+ + {/* ── Info overlay ── */} + + + + {objStore.obj.name} + + + {getFileSize(objStore.obj.size)} + + 0}> + + {imgSize().w} × {imgSize().h}px + + + + {formatDate(objStore.obj.modified)} + + + +
+
+
) } diff --git a/src/pages/home/previews/index.ts b/src/pages/home/previews/index.ts index 8fef26f15..687133f7b 100644 --- a/src/pages/home/previews/index.ts +++ b/src/pages/home/previews/index.ts @@ -92,6 +92,7 @@ const previews: Preview[] = [ { key: "image", type: ObjType.IMAGE, + exts: ["heic", "heif", "avif", "vvc", "avc"], // libheif component: lazy(() => import("./image")), prior: true, }, @@ -119,12 +120,7 @@ const previews: Preview[] = [ component: lazy(() => import("./plist")), prior: true, }, - { - key: "heic", - exts: ["heic", "heif", "avif", "vvc", "avc", "jpeg", "jpg"], - component: lazy(() => import("./heic")), - prior: true, - }, + ...(import.meta.env.VITE_LITE === "true" ? [] : [ @@ -143,13 +139,13 @@ const previews: Preview[] = [ }, { key: "xls", - exts: ["xlsx", "xls"], + exts: ["xlsx"], component: lazy(() => import("./xls")), prior: true, }, { key: "doc", - exts: ["docx", "doc"], + exts: ["docx"], component: lazy(() => import("./doc")), prior: true, }, diff --git a/src/pages/home/previews/pdf.tsx b/src/pages/home/previews/pdf.tsx index df88bd59d..b67aa9316 100644 --- a/src/pages/home/previews/pdf.tsx +++ b/src/pages/home/previews/pdf.tsx @@ -3,6 +3,7 @@ import pdfiumWasmUrl from "@embedpdf/snippet/dist/pdfium.wasm?url" import { Box, useColorMode } from "@hope-ui/solid" import { onMount } from "solid-js" import { currentLang } from "~/app/i18n" +import { BoxWithFullScreen } from "~/components" import { objStore } from "~/store" import { base_path } from "~/utils" @@ -30,7 +31,11 @@ const PDFViewer = () => { }) } }) - return (ref = el)} /> + return ( + + + + ) } export default PDFViewer diff --git a/src/pages/home/previews/ppt.tsx b/src/pages/home/previews/ppt.tsx index d0033803b..ed2f90db0 100644 --- a/src/pages/home/previews/ppt.tsx +++ b/src/pages/home/previews/ppt.tsx @@ -1,9 +1,12 @@ import { BoxWithFullScreen, Error as Erro, FullLoading } from "~/components" -import { objStore } from "~/store" -import { Box, IconButton, Tooltip } from "@hope-ui/solid" +import { Box, Button, IconButton, Tooltip } from "@hope-ui/solid" +import { loadScriptIIFE, loadCSS } from "~/utils" import { createSignal, onMount, onCleanup, Show } from "solid-js" -import { useT } from "~/hooks" -import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" +import { useLink, useT, useCDN } from "~/hooks" +import { + HiOutlineMagnifyingGlassPlus, + HiOutlineMagnifyingGlassMinus, +} from "solid-icons/hi" // 声明全局jQuery和pptxToHtml方法 declare global { @@ -15,47 +18,22 @@ declare global { const PPTViewerApp = () => { const t = useT() + const { currentObjLink } = useLink() + const { npm, pptBasePath } = useCDN() const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) - const [isFullscreen, setIsFullscreen] = createSignal(false) + // null = auto-fit, number = manual zoom level + const [zoom, setZoom] = createSignal(null) let containerRef: HTMLDivElement | undefined + let shadowHostRef: HTMLDivElement | undefined let resultRef: HTMLDivElement | undefined - // 加载外部脚本 - const loadScript = (src: string, id: string): Promise => { - return new Promise((resolve, reject) => { - // 检查脚本是否已加载 - if (document.getElementById(id)) { - resolve() - return + // 将已加载的CSS注入Shadow DOM + const injectStylesToShadow = (shadow: ShadowRoot) => { + document.querySelectorAll('link[id$="-css"]').forEach((el) => { + if (!shadow.getElementById(el.id)) { + shadow.appendChild(el.cloneNode(true)) } - - const script = document.createElement("script") - script.src = src - script.id = id - script.type = "text/javascript" - script.onload = () => resolve() - script.onerror = () => reject(new Error(`Failed to load script: ${src}`)) - document.head.appendChild(script) - }) - } - - // 加载CSS文件 - const loadCSS = (href: string, id: string): Promise => { - return new Promise((resolve, reject) => { - // 检查CSS是否已加载 - if (document.getElementById(id)) { - resolve() - return - } - - const link = document.createElement("link") - link.rel = "stylesheet" - link.href = href - link.id = id - link.onload = () => resolve() - link.onerror = () => reject(new Error(`Failed to load CSS: ${href}`)) - document.head.appendChild(link) }) } @@ -65,7 +43,7 @@ const PPTViewerApp = () => { setLoading(true) setError(false) - const baseUrl = "https://res.oplist.org.cn/ppt.js" + const baseUrl = pptBasePath() // 加载CSS文件 await Promise.all([ @@ -74,27 +52,37 @@ const PPTViewerApp = () => { ]) // 按顺序加载JS文件 - await loadScript(`${baseUrl}/js/jquery-1.11.3.min.js`, "jquery-script") - // 使用JSZip 3.x版本,与docx预览器保持一致 - await loadScript( - "https://unpkg.com/jszip@2.6.1/dist/jszip.min.js", - "jszip-script", + await loadScriptIIFE( + `${baseUrl}/js/jquery-1.11.3.min.js`, + "jquery-script", ) - await loadScript(`${baseUrl}/js/filereader.js`, "filereader-script") - await loadScript(`${baseUrl}/js/d3.min.js`, "d3-script") - await loadScript(`${baseUrl}/js/nv.d3.min.js`, "nv-d3-script") - await loadScript(`${baseUrl}/js/pptxjs.js`, "pptxjs-script") - await loadScript(`${baseUrl}/js/divs2slides.js`, "divs2slides-script") + // 使用JSZip 2.x版本,不支持3.x + // 加载前清理其他版本的 jszip,避免全局变量冲突 + document.getElementById("jszip-3.10.1-script")?.remove() + await loadScriptIIFE( + npm("jszip", "2.6.1", "dist/jszip.min.js"), + "jszip-2.6.1-script", + ) + await loadScriptIIFE(`${baseUrl}/js/filereader.js`, "filereader-script") + await loadScriptIIFE(`${baseUrl}/js/d3.min.js`, "d3-script") + await loadScriptIIFE(`${baseUrl}/js/nv.d3.min.js`, "nv-d3-script") + await loadScriptIIFE(`${baseUrl}/js/pptxjs.js`, "pptxjs-script") + await loadScriptIIFE(`${baseUrl}/js/divs2slides.js`, "divs2slides-script") // 等待jQuery加载完成 if (!window.$ || !window.jQuery) { throw new Error("jQuery not loaded") } - // 初始化pptxToHtml + // 将CSS注入Shadow DOM + if (shadowHostRef?.shadowRoot) { + injectStylesToShadow(shadowHostRef.shadowRoot) + } + + // 初始化pptxToHtml(渲染到Shadow DOM内的resultRef) if (resultRef) { window.$(resultRef).pptxToHtml({ - pptxFileUrl: objStore.raw_url, + pptxFileUrl: currentObjLink(), slideMode: false, keyBoardShortCut: false, slideModeConfig: { @@ -116,10 +104,31 @@ const PPTViewerApp = () => { }, }) - // 监听加载完成事件 - setTimeout(() => { + // 用 MutationObserver 检测 PPT 内容是否渲染完成 + const RENDER_TIMEOUT = 30000 // 30s fallback + let resolved = false + + const done = () => { + if (resolved) return + resolved = true setLoading(false) - }, 2000) + } + + const observer = new MutationObserver(() => { + // pptxjs 渲染完成后会生成子元素(slides) + if (resultRef!.children.length > 0) { + observer.disconnect() + done() + } + }) + observer.observe(resultRef, { childList: true, subtree: true }) + onCleanup(() => observer.disconnect()) + + // fallback: 若 30s 内 MutationObserver 未触发 + setTimeout(() => { + observer.disconnect() + done() + }, RENDER_TIMEOUT) } } catch (e) { console.error("PPT初始化失败:", e) @@ -128,92 +137,66 @@ const PPTViewerApp = () => { } } - // 全屏切换 - const toggleFullscreen = () => { - if (!containerRef) return - - if (!document.fullscreenElement) { - containerRef.requestFullscreen().then(() => { - setIsFullscreen(true) - // 调整幻灯片大小 - if (resultRef) { - const slides = resultRef.querySelectorAll(".slide") - slides.forEach((slide: any) => { - slide.style.width = "99%" - slide.style.margin = "0 auto" - }) - } - }) + // 应用缩放 + const applyScale = () => { + if (!resultRef || !containerRef) return + const z = zoom() + if (z === null) { + // auto-fit: 缩放到容器宽度 + const containerWidth = containerRef.clientWidth + const contentWidth = resultRef.scrollWidth + if (contentWidth > containerWidth) { + resultRef.style.zoom = `${containerWidth / contentWidth}` + } else { + resultRef.style.zoom = "" + } } else { - document.exitFullscreen().then(() => { - setIsFullscreen(false) - // 恢复幻灯片大小 - if (resultRef) { - const slides = resultRef.querySelectorAll(".slide") - slides.forEach((slide: any) => { - slide.style.width = "" - slide.style.margin = "" - }) - } - }) + resultRef.style.zoom = `${z}` } } - // 监听全屏变化 - const handleFullscreenChange = () => { - if (!document.fullscreenElement) { - setIsFullscreen(false) - // 恢复幻灯片大小 - if (resultRef) { - const slides = resultRef.querySelectorAll(".slide") - slides.forEach((slide: any) => { - slide.style.width = "" - slide.style.margin = "" - }) - } - } + // 缩放控制 + const zoomStep = 0.1 + const zoomIn = () => { + const current = zoom() ?? 1 + setZoom(Math.min(current + zoomStep, 3)) + applyScale() + } + const zoomOut = () => { + const current = zoom() ?? 1 + setZoom(Math.max(current - zoomStep, 0.3)) + applyScale() + } + const zoomReset = () => { + setZoom(null) + applyScale() + } + + const setupResponsiveScale = () => { + if (!resultRef || !containerRef) return + const container = containerRef + const observer = new ResizeObserver(() => { + if (zoom() === null) applyScale() + }) + observer.observe(container) + onCleanup(() => observer.disconnect()) } onMount(() => { + // 创建Shadow DOM,将PPT内容隔离在里面,防止pptxjs的高z-index影响外部 + if (shadowHostRef) { + const shadow = shadowHostRef.attachShadow({ mode: "open" }) + resultRef = document.createElement("div") + resultRef.id = "ppt-result" + resultRef.style.cssText = "width:100%;height:100%;" + shadow.appendChild(resultRef) + } initPPTViewer() - document.addEventListener("fullscreenchange", handleFullscreenChange) - }) - - onCleanup(() => { - document.removeEventListener("fullscreenchange", handleFullscreenChange) - // 清理加载的脚本和样式(可选) + setupResponsiveScale() }) return ( - {/* 全屏按钮 */} - - - : } - onClick={toggleFullscreen} - /> - - - {/* PPT容器 */}
{ background: "#f5f5f5", }} > + {/* Shadow DOM宿主 - 隔离pptxjs生成的高z-index元素 */}
{
+ + {/* 缩放控制 */} + + } + onClick={zoomOut} + /> + + + + } + onClick={zoomIn} + /> + ) } diff --git a/src/pages/home/previews/xls.tsx b/src/pages/home/previews/xls.tsx index d8c4d44b1..1a4d48176 100644 --- a/src/pages/home/previews/xls.tsx +++ b/src/pages/home/previews/xls.tsx @@ -1,9 +1,8 @@ import { BoxWithFullScreen, Error as Erro, FullLoading } from "~/components" -import { objStore } from "~/store" -import { Box, IconButton, Tooltip, Button, HStack } from "@hope-ui/solid" +import { Box, Button, HStack } from "@hope-ui/solid" +import { loadScriptIIFE } from "~/utils" import { createSignal, onMount, For, Show } from "solid-js" -import { useT } from "~/hooks" -import { VsScreenFull, VsScreenNormal } from "solid-icons/vs" +import { useLink, useT, useCDN } from "~/hooks" // 声明全局ExcelJS类型 declare global { @@ -30,43 +29,14 @@ interface SheetData { const ExcelViewerApp = () => { const t = useT() + const { currentObjLink } = useLink() + const { excelJSPath } = useCDN() const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(false) - const [isFullscreen, setIsFullscreen] = createSignal(false) const [sheets, setSheets] = createSignal([]) const [currentSheetIndex, setCurrentSheetIndex] = createSignal(0) let containerRef: HTMLDivElement | undefined - // 动态加载ExcelJS库 - const loadExcelJSScript = () => { - return new Promise((resolve, reject) => { - // 检查是否已经加载 - if (window.ExcelJS) { - resolve() - return - } - - // 检查脚本标签是否已存在 - const existingScript = document.getElementById("exceljs-script") - if (existingScript) { - // 脚本正在加载中,等待加载完成 - existingScript.addEventListener("load", () => resolve()) - existingScript.addEventListener("error", () => - reject(new Error("Failed to load ExcelJS library")), - ) - return - } - - const script = document.createElement("script") - script.src = "https://res.oplist.org.cn/exceljs/exceljs.min.js" - script.id = "exceljs-script" - script.async = true - script.onload = () => resolve() - script.onerror = () => reject(new Error("Failed to load ExcelJS library")) - document.head.appendChild(script) - }) - } - // 加载并解析Excel文件 const loadExcelFile = async () => { try { @@ -74,10 +44,10 @@ const ExcelViewerApp = () => { setError(false) // 先加载ExcelJS库 - await loadExcelJSScript() + await loadScriptIIFE(excelJSPath(), "exceljs-script") // 获取文件URL - const fileUrl = objStore.raw_url + const fileUrl = currentObjLink() // 下载文件 const response = await fetch(fileUrl) @@ -153,21 +123,6 @@ const ExcelViewerApp = () => { } } - // 全屏切换 - const toggleFullscreen = () => { - if (!containerRef) return - - if (!document.fullscreenElement) { - containerRef.requestFullscreen().then(() => { - setIsFullscreen(true) - }) - } else { - document.exitFullscreen().then(() => { - setIsFullscreen(false) - }) - } - } - onMount(() => { loadExcelFile() }) @@ -206,98 +161,78 @@ const ExcelViewerApp = () => { - {/* 全屏按钮 */} - - - : } - onClick={toggleFullscreen} - /> - - + {/* Excel表格容器 */} +
+ {/* 表格内容 */} + 0}> +
+ + + + {(row) => ( + + + {(cell) => ( + + )} + + + )} + + +
+ {cell.value} +
+
+
+ + {/* 加载状态 */} + + + + + {/* 错误状态 */} + + + +
- - {/* Excel表格容器 */} -
- {/* 表格内容 */} - 0}> -
- - - - {(row) => ( - - - {(cell) => ( - - )} - - - )} - - -
- {cell.value} -
-
-
- - {/* 加载状态 */} - - - - - {/* 错误状态 */} - - - -
) } diff --git a/src/pages/home/toolbar/Right.tsx b/src/pages/home/toolbar/Right.tsx index 9ae109fcd..f008309f8 100644 --- a/src/pages/home/toolbar/Right.tsx +++ b/src/pages/home/toolbar/Right.tsx @@ -30,6 +30,7 @@ export const Right = () => { pos="fixed" right={margin()} bottom={margin()} + zIndex="calc($modal - 1)" > { top="0" left="0" overflow="hidden" - zIndex="-1" + zIndex="$hide" w="100vw" h="100vh" > diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 6d22c284f..fec89c08c 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -225,7 +225,7 @@ const Login = () => { } return ( -
+
=> { + return new Promise((resolve, reject) => { + if (document.getElementById(id)) { + resolve() + return + } + const script = document.createElement("script") + script.src = src + script.id = id + script.type = "text/javascript" + script.onload = () => resolve() + script.onerror = () => reject(new Error(`Failed to load script: ${src}`)) + document.head.appendChild(script) + }) +} + +/** + * Load an external CSS file dynamically + * @returns Promise that resolves when the CSS is loaded + */ +export const loadCSS = (href: string, id: string): Promise => { + return new Promise((resolve, reject) => { + if (document.getElementById(id)) { + resolve() + return + } + const link = document.createElement("link") + link.rel = "stylesheet" + link.href = href + link.id = id + link.onload = () => resolve() + link.onerror = () => reject(new Error(`Failed to load CSS: ${href}`)) + document.head.appendChild(link) + }) +}