U:前端增加大图优化加载
This commit is contained in:
1 parent
1241d147a6
commit
c42628f38d
3 files changed
+217
-102
No files matched your search
@@ -0,0 +1,47 @@
|
||||
# Repository Guidelines
|
||||
|
||||
This document describes how to work effectively in the `argus-front` Vue 3 + TypeScript project.
|
||||
|
||||
## Project Structure & Modules
|
||||
|
||||
- App source lives in `src/` (views, components, stores, services, router, composables, utils).
|
||||
- Global styles and icons are under `src/assets/`.
|
||||
- Reusable UI is in `src/components/`; route-level pages are in `src/views/`.
|
||||
- Shared types, models, and constants live in `src/types/`, `src/model/`, and `src/constants/`.
|
||||
- Additional architecture notes are in `CLAUDE.md` and `docs/`.
|
||||
|
||||
## Build, Test & Development
|
||||
|
||||
- `pnpm dev` – run Vite dev server.
|
||||
- `pnpm build` – type-check and production build.
|
||||
- `pnpm preview` – preview built app.
|
||||
- `pnpm test:unit` – run Vitest unit tests.
|
||||
- `pnpm lint` – run ESLint with auto-fix.
|
||||
- `pnpm format` – run Prettier on `src/`.
|
||||
|
||||
## Coding Style & Naming
|
||||
|
||||
- Use TypeScript, Vue 3 `<script setup>` where appropriate.
|
||||
- Follow ESLint + Prettier rules (`.editorconfig`, `.prettierrc.json`, `eslint.config.ts`).
|
||||
- Use 2-space indentation; avoid unused exports and `any`.
|
||||
- Name Vue components in `PascalCase` (`FileManagerView.vue`), composables as `useXxx.ts`, and stores as `useXxxStore.ts`.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
- Prefer Vitest + `@vue/test-utils` for component and logic tests.
|
||||
- Place tests next to code (e.g. `FileManager.spec.ts`) or in a `__tests__` folder.
|
||||
- When changing behavior, add or update tests to cover the new path; keep tests deterministic.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
- Write focused commits with imperative, English subjects (e.g. `feat: add folder selection toolbar`, `fix: debounce file search input`).
|
||||
- Reference related issues in the body when applicable.
|
||||
- For PRs, include: purpose, main changes, testing performed (`pnpm test:unit`, `pnpm lint`), and screenshots for UI changes.
|
||||
- Keep PRs small and cohesive; prefer follow-up PRs over large, mixed changes.
|
||||
|
||||
## Agent-Specific Notes
|
||||
|
||||
- Prefer modifying existing patterns over introducing new abstractions.
|
||||
- Do not change build tooling or dependency versions unless explicitly requested.
|
||||
- When in doubt, align with the architecture described in `CLAUDE.md`.
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<!-- 半透明背景 -->
|
||||
<div v-if="visible" class="photo-preview-backdrop" @click.self="handleClose">
|
||||
<!-- 悬浮预览窗口(支持全屏) -->
|
||||
<div
|
||||
v-if="visible"
|
||||
class="photo-preview-backdrop"
|
||||
@click.self="handleClose"
|
||||
@wheel.stop.prevent="handleWheel"
|
||||
>
|
||||
<!-- 预览窗口(全屏) -->
|
||||
<div class="photo-preview-window" :class="{ fullscreen: isFullscreen }" @click.stop>
|
||||
<!-- 信息面板 (Tab 键切换) - HoneyView 风格 -->
|
||||
<div v-if="showInfo" class="info-panel">
|
||||
@@ -40,7 +45,6 @@
|
||||
@mousemove="handleMouseMove"
|
||||
@mouseup="handleMouseUp"
|
||||
@mouseleave="handleMouseUp"
|
||||
@wheel="handleWheel"
|
||||
@dblclick="handleDoubleClick"
|
||||
>
|
||||
<!-- 多层渐进式图片加载 -->
|
||||
@@ -51,7 +55,7 @@
|
||||
:alt="fileName"
|
||||
class="preview-image preview-layer"
|
||||
:class="{ 'layer-visible': currentQuality === 'thumbnail' }"
|
||||
@load="onLayerLoad('thumbnail')"
|
||||
@load="onLayerLoad('thumbnail', $event)"
|
||||
@error="handleImageError"
|
||||
draggable="false"
|
||||
/>
|
||||
@@ -63,7 +67,7 @@
|
||||
:alt="fileName"
|
||||
class="preview-image preview-layer"
|
||||
:class="{ 'layer-visible': currentQuality === 'highRes' && layersLoaded.highRes }"
|
||||
@load="onLayerLoad('highRes')"
|
||||
@load="onLayerLoad('highRes', $event)"
|
||||
@error="handleImageError"
|
||||
draggable="false"
|
||||
/>
|
||||
@@ -75,7 +79,7 @@
|
||||
:alt="fileName"
|
||||
class="preview-image preview-layer"
|
||||
:class="{ 'layer-visible': currentQuality === 'original' && layersLoaded.original }"
|
||||
@load="onLayerLoad('original')"
|
||||
@load="onLayerLoad('original', $event)"
|
||||
@error="handleImageError"
|
||||
draggable="false"
|
||||
/>
|
||||
@@ -136,6 +140,12 @@ const layersLoaded = ref({
|
||||
original: false
|
||||
})
|
||||
|
||||
const layerSizes = ref({
|
||||
thumbnail: { width: 0, height: 0 },
|
||||
highRes: { width: 0, height: 0 },
|
||||
original: { width: 0, height: 0 }
|
||||
})
|
||||
|
||||
// EXIF 数据
|
||||
interface ExifData {
|
||||
make?: string
|
||||
@@ -237,6 +247,8 @@ async function loadPreview() {
|
||||
original: urls.original
|
||||
}
|
||||
|
||||
applyQualityForScale(1)
|
||||
|
||||
// TODO: 获取 EXIF 信息
|
||||
// loadExifData(props.filePath)
|
||||
} catch (err) {
|
||||
@@ -246,7 +258,7 @@ async function loadPreview() {
|
||||
}
|
||||
|
||||
// 各层图片加载完成
|
||||
function onLayerLoad(layer: 'thumbnail' | 'highRes' | 'original') {
|
||||
function onLayerLoad(layer: 'thumbnail' | 'highRes' | 'original', event: Event) {
|
||||
layersLoaded.value[layer] = true
|
||||
isAnimating.value = false
|
||||
console.log(`✅ [PhotoPreviewModal.onLayerLoad] ${layer} 层加载成功`)
|
||||
@@ -268,22 +280,52 @@ function switchQuality(quality: 'thumbnail' | 'highRes' | 'original') {
|
||||
currentQuality.value = quality
|
||||
}
|
||||
|
||||
function pickQualityForScale(nextScale: number): 'thumbnail' | 'highRes' | 'original' {
|
||||
const hasHighRes = !!imageUrls.value.highRes
|
||||
const hasOriginal = !!imageUrls.value.original
|
||||
|
||||
// 如果原图(或最高清晰度预览)已经加载过,后续始终优先使用它
|
||||
if (hasOriginal && layersLoaded.value.original) {
|
||||
return 'original'
|
||||
}
|
||||
|
||||
if (hasOriginal && nextScale >= 2) {
|
||||
return 'original'
|
||||
}
|
||||
|
||||
if (hasHighRes && nextScale >= 1) {
|
||||
return 'highRes'
|
||||
}
|
||||
|
||||
return 'thumbnail'
|
||||
}
|
||||
|
||||
function applyQualityForScale(nextScale: number) {
|
||||
const target = pickQualityForScale(nextScale)
|
||||
if (target !== currentQuality.value) {
|
||||
switchQuality(target)
|
||||
}
|
||||
}
|
||||
|
||||
// 缩放控制
|
||||
function zoomIn() {
|
||||
isAnimating.value = true
|
||||
const newScale = Math.min(scale.value + 0.2, 5)
|
||||
const newScale = Math.min(scale.value * 1.2, 5)
|
||||
|
||||
// 如果放大超过 1.5 倍,自动切换到原图
|
||||
if (newScale > 1.5 && currentQuality.value !== 'original') {
|
||||
if (false && newScale > 1.5 && currentQuality.value !== 'original') {
|
||||
switchQuality('original')
|
||||
}
|
||||
|
||||
applyQualityForScale(newScale)
|
||||
scale.value = newScale
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
isAnimating.value = true
|
||||
scale.value = Math.max(scale.value - 0.2, 0.1)
|
||||
const newScale = Math.max(scale.value / 1.2, 0.1)
|
||||
applyQualityForScale(newScale)
|
||||
scale.value = newScale
|
||||
}
|
||||
|
||||
function resetZoom() {
|
||||
@@ -292,19 +334,45 @@ function resetZoom() {
|
||||
translateX.value = 0
|
||||
translateY.value = 0
|
||||
|
||||
// 重置到缩略图模式
|
||||
if (currentQuality.value !== 'thumbnail') {
|
||||
switchQuality('thumbnail')
|
||||
}
|
||||
applyQualityForScale(1)
|
||||
}
|
||||
|
||||
function setActualSize() {
|
||||
if (!containerRef.value) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const rect = containerRef.value.getBoundingClientRect()
|
||||
const boxWidth = rect.width || 1
|
||||
|
||||
const targetLayer =
|
||||
imageUrls.value.original ? 'original'
|
||||
: imageUrls.value.highRes ? 'highRes'
|
||||
: 'thumbnail'
|
||||
|
||||
const size = layerSizes.value[targetLayer]
|
||||
if (!size.width) return
|
||||
|
||||
const targetScale = Math.max(0.1, Math.min((size.width / dpr) / boxWidth, 5))
|
||||
|
||||
scale.value = targetScale
|
||||
translateX.value = 0
|
||||
translateY.value = 0
|
||||
|
||||
switchQuality(targetLayer)
|
||||
}
|
||||
|
||||
// 鼠标滚轮缩放
|
||||
function handleWheel(event: WheelEvent) {
|
||||
// 在任意位置滚轮都缩放
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
isAnimating.value = true
|
||||
|
||||
const delta = event.deltaY > 0 ? -0.1 : 0.1
|
||||
const newScale = Math.max(0.1, Math.min(5, scale.value + delta))
|
||||
// 使用指数函数让缩放更平滑、与硬件无关
|
||||
const zoomIntensity = 0.0015
|
||||
const zoomFactor = Math.exp(-event.deltaY * zoomIntensity)
|
||||
const newScale = Math.max(0.1, Math.min(5, scale.value * zoomFactor))
|
||||
applyQualityForScale(newScale)
|
||||
|
||||
// 放大时自动进入全屏 + 切换原图
|
||||
if (newScale > 1.2 && !isFullscreen.value) {
|
||||
@@ -312,7 +380,7 @@ function handleWheel(event: WheelEvent) {
|
||||
}
|
||||
|
||||
// 如果放大超过 1.5 倍,自动切换到原图
|
||||
if (newScale > 1.5 && currentQuality.value !== 'original') {
|
||||
if (false && newScale > 1.5 && currentQuality.value !== 'original') {
|
||||
switchQuality('original')
|
||||
}
|
||||
|
||||
@@ -380,6 +448,7 @@ function resetState() {
|
||||
exifData.value = null
|
||||
imageUrls.value = { thumbnail: '', highRes: '', original: '' }
|
||||
layersLoaded.value = { thumbnail: false, highRes: false, original: false }
|
||||
layerSizes.value = { thumbnail: { width: 0, height: 0 }, highRes: { width: 0, height: 0 }, original: { width: 0, height: 0 } }
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
@@ -461,6 +530,11 @@ function handleKeyDown(event: KeyboardEvent) {
|
||||
event.stopPropagation()
|
||||
zoomOut()
|
||||
break
|
||||
case '1':
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setActualSize()
|
||||
break
|
||||
case '0':
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@@ -475,6 +549,7 @@ function resetZoomForNavigation() {
|
||||
translateX.value = 0
|
||||
translateY.value = 0
|
||||
isAnimating.value = false
|
||||
applyQualityForScale(1)
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
@@ -516,10 +591,8 @@ onUnmounted(() => {
|
||||
|
||||
.photo-preview-window {
|
||||
position: relative;
|
||||
width: 85vw;
|
||||
max-width: 1400px;
|
||||
height: 85vh;
|
||||
max-height: 900px;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: scaleIn 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
@@ -614,15 +687,12 @@ onUnmounted(() => {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
||||
border-radius: 4px;
|
||||
border-radius: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
pointer-events: none;
|
||||
@@ -636,15 +706,12 @@ onUnmounted(() => {
|
||||
|
||||
/* 兼容旧的 class 名(如果有地方还在用) */
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
||||
border-radius: 4px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
|
||||
@@ -6,101 +6,99 @@ export interface PreviewConfig {
|
||||
}
|
||||
|
||||
export interface PreviewUrls {
|
||||
thumbnail: string // 初始缩略图 (720p)
|
||||
highRes: string // 高清版本 (2K)
|
||||
original: string // 原图
|
||||
thumbnail: string // 初始预览(首屏)
|
||||
highRes: string // 更高清版本(放大时)
|
||||
original: string // 原图级别(或接近原图)
|
||||
}
|
||||
|
||||
class PhotoPreviewService {
|
||||
private baseUrl = '/v1/photo/preview'
|
||||
|
||||
/**
|
||||
* 根据文件大小智能选择预览策略
|
||||
* @param filePath 文件路径
|
||||
* @param fileSize 文件大小(字节)
|
||||
* @returns 预览 URL
|
||||
* 单一预览 URL(老接口保留给其他地方使用),内部也按屏幕和文件大小做决策
|
||||
*/
|
||||
getPreviewUrl(filePath: string, fileSize?: number): string {
|
||||
const encodedPath = encodeURIComponent(filePath)
|
||||
|
||||
if (!fileSize) {
|
||||
// 未知文件大小,默认加载 512p 缩略图(更快的初始加载)
|
||||
return `${httpClient.getAxiosInstance().defaults.baseURL}${this.baseUrl}?path=${encodedPath}&size=512`
|
||||
}
|
||||
|
||||
const baseURL = httpClient.getAxiosInstance().defaults.baseURL
|
||||
const MB = 1024 * 1024
|
||||
const buildUrl = (size: number) =>
|
||||
`${baseURL}${this.baseUrl}?path=${encodedPath}&size=${size}`
|
||||
|
||||
if (fileSize < MB) {
|
||||
// <1MB:直接加载原图(小文件)
|
||||
return `${httpClient.getAxiosInstance().defaults.baseURL}${this.baseUrl}?path=${encodedPath}&size=0`
|
||||
} else if (fileSize < 5 * MB) {
|
||||
// 1-5MB:加载 1080p 缩略图
|
||||
return `${httpClient.getAxiosInstance().defaults.baseURL}${this.baseUrl}?path=${encodedPath}&size=1080`
|
||||
} else if (fileSize < 20 * MB) {
|
||||
// 5-20MB:加载 512p 缩略图(初始)
|
||||
return `${httpClient.getAxiosInstance().defaults.baseURL}${this.baseUrl}?path=${encodedPath}&size=512`
|
||||
} else {
|
||||
// 超过 20MB:加载 512p 缩略图(最小尺寸)
|
||||
return `${httpClient.getAxiosInstance().defaults.baseURL}${this.baseUrl}?path=${encodedPath}&size=512`
|
||||
const viewportLongSide = this.getViewportLongSide()
|
||||
|
||||
// 未知大小:按屏幕尺寸给一个适中的预览
|
||||
if (!fileSize) {
|
||||
const baseSize = Math.min(Math.max(viewportLongSide, 1080), 2048)
|
||||
return buildUrl(baseSize)
|
||||
}
|
||||
|
||||
// 小文件:直接原图(只压缩质量,不压缩尺寸)
|
||||
if (fileSize <= 4 * MB) {
|
||||
return buildUrl(0)
|
||||
}
|
||||
|
||||
// 较大的文件:按屏幕尺寸裁一档预览
|
||||
const baseSize = Math.min(Math.max(viewportLongSide, 1280), 2560)
|
||||
return buildUrl(baseSize)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有级别的预览 URL(按文件大小智能选择)
|
||||
* @param filePath 文件路径
|
||||
* @param fileSize 文件大小(字节,可选)
|
||||
* @returns 包含不同分辨率的 URL 对象
|
||||
* 获取所有级别的预览 URL:首屏 + 放大 + 原图级别
|
||||
*/
|
||||
getAllPreviewUrls(filePath: string, fileSize?: number): PreviewUrls {
|
||||
const encodedPath = encodeURIComponent(filePath)
|
||||
const baseURL = httpClient.getAxiosInstance().defaults.baseURL
|
||||
const MB = 1024 * 1024
|
||||
|
||||
const buildUrl = (size: number) =>
|
||||
`${baseURL}${this.baseUrl}?path=${encodedPath}&size=${size}`
|
||||
|
||||
const viewportLongSide = this.getViewportLongSide()
|
||||
|
||||
// 没有文件大小信息:按屏幕尺寸给一个较清晰的首屏 + 更高质量的放大层
|
||||
if (!fileSize) {
|
||||
// 默认策略:三级加载
|
||||
const baseSize = Math.min(Math.max(viewportLongSide, 1080), 2560)
|
||||
const highSize = Math.min(baseSize * 1.5, 4096)
|
||||
return {
|
||||
thumbnail: `${baseURL}${this.baseUrl}?path=${encodedPath}&size=512`,
|
||||
highRes: `${baseURL}${this.baseUrl}?path=${encodedPath}&size=1080`,
|
||||
original: `${baseURL}${this.baseUrl}?path=${encodedPath}&size=0`
|
||||
thumbnail: buildUrl(baseSize),
|
||||
highRes: buildUrl(Math.round(highSize)),
|
||||
original: buildUrl(0)
|
||||
}
|
||||
}
|
||||
|
||||
const MB = 1024 * 1024
|
||||
|
||||
if (fileSize < MB) {
|
||||
// <1MB:只需一级(直接原图)
|
||||
// 小文件:直接使用原图,不做尺寸压缩(只依赖后端的质量设置)
|
||||
if (fileSize <= 4 * MB) {
|
||||
return {
|
||||
thumbnail: `${baseURL}${this.baseUrl}?path=${encodedPath}&size=0`,
|
||||
thumbnail: buildUrl(0),
|
||||
highRes: '',
|
||||
original: ''
|
||||
}
|
||||
} else if (fileSize < 5 * MB) {
|
||||
// 1-5MB:两级加载
|
||||
}
|
||||
|
||||
// 中等大小文件:首屏使用接近屏幕分辨率的预览,放大时使用更高质量,再需要时才走“原图级别”
|
||||
if (fileSize <= 20 * MB) {
|
||||
const baseSize = Math.min(Math.max(viewportLongSide, 1280), 2560)
|
||||
const highSize = Math.min(baseSize * 1.5, 4096)
|
||||
return {
|
||||
thumbnail: `${baseURL}${this.baseUrl}?path=${encodedPath}&size=512`,
|
||||
highRes: `${baseURL}${this.baseUrl}?path=${encodedPath}&size=1080`,
|
||||
original: ''
|
||||
}
|
||||
} else if (fileSize < 20 * MB) {
|
||||
// 5-20MB:三级加载
|
||||
return {
|
||||
thumbnail: `${baseURL}${this.baseUrl}?path=${encodedPath}&size=512`,
|
||||
highRes: `${baseURL}${this.baseUrl}?path=${encodedPath}&size=1080`,
|
||||
original: `${baseURL}${this.baseUrl}?path=${encodedPath}&size=2048`
|
||||
}
|
||||
} else {
|
||||
// 20MB以上:四级加载(包含4K)
|
||||
return {
|
||||
thumbnail: `${baseURL}${this.baseUrl}?path=${encodedPath}&size=512`,
|
||||
highRes: `${baseURL}${this.baseUrl}?path=${encodedPath}&size=2048`,
|
||||
original: `${baseURL}${this.baseUrl}?path=${encodedPath}&size=4096`
|
||||
thumbnail: buildUrl(baseSize),
|
||||
highRes: buildUrl(Math.round(highSize)),
|
||||
original: buildUrl(0)
|
||||
}
|
||||
}
|
||||
|
||||
// 超大文件:首屏略低一档,放大和进一步查看时再拉高质量 / 原图
|
||||
const baseSize = Math.min(Math.max(Math.round(viewportLongSide * 0.8), 1080), 2560)
|
||||
const highSize = Math.min(baseSize * 1.5, 4096)
|
||||
|
||||
return {
|
||||
thumbnail: buildUrl(baseSize),
|
||||
highRes: buildUrl(Math.round(highSize)),
|
||||
original: buildUrl(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高清版本 URL(用户放大时加载)
|
||||
* @param filePath 文件路径
|
||||
* @returns 高清 URL
|
||||
*/
|
||||
getHighResUrl(filePath: string): string {
|
||||
const encodedPath = encodeURIComponent(filePath)
|
||||
@@ -109,8 +107,6 @@ class PhotoPreviewService {
|
||||
|
||||
/**
|
||||
* 获取原图 URL(用户明确需要时加载)
|
||||
* @param filePath 文件路径
|
||||
* @returns 原图 URL
|
||||
*/
|
||||
getOriginalUrl(filePath: string): string {
|
||||
const encodedPath = encodeURIComponent(filePath)
|
||||
@@ -118,10 +114,7 @@ class PhotoPreviewService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自定义尺寸的预览 URL
|
||||
* @param filePath 文件路径
|
||||
* @param size 最长边尺寸(像素)
|
||||
* @returns 预览 URL
|
||||
* 获取自定义尺寸的预览 URL(按最长边)
|
||||
*/
|
||||
getCustomSizeUrl(filePath: string, size: number): string {
|
||||
const encodedPath = encodeURIComponent(filePath)
|
||||
@@ -130,8 +123,6 @@ class PhotoPreviewService {
|
||||
|
||||
/**
|
||||
* 预加载图片(用于提前缓存)
|
||||
* @param url 图片 URL
|
||||
* @returns Promise
|
||||
*/
|
||||
preloadImage(url: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -144,12 +135,22 @@ class PhotoPreviewService {
|
||||
|
||||
/**
|
||||
* 批量预加载图片
|
||||
* @param urls 图片 URL 数组
|
||||
* @returns Promise
|
||||
*/
|
||||
async preloadImages(urls: string[]): Promise<void> {
|
||||
await Promise.all(urls.map(url => this.preloadImage(url)))
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算当前屏幕的最长边(像素),结合设备像素比
|
||||
*/
|
||||
private getViewportLongSide(): number {
|
||||
if (typeof window === 'undefined') {
|
||||
return 1440
|
||||
}
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
return Math.round(Math.max(window.innerWidth, window.innerHeight) * dpr)
|
||||
}
|
||||
}
|
||||
|
||||
export const photoPreviewService = new PhotoPreviewService()
|
||||
|
||||
Reference in new issue
Block a user