perf(common): 优化图片查看器和请求重试报错机制

增加赞助者名单
This commit is contained in:
Dawn
2025-02-08 18:01:35 +08:00
parent 223b9f2587
commit 25c39b73c5
20 changed files with 341 additions and 185 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# 后端服务地址
VITE_SERVICE_URL="https://hulaspark.com/api/"
VITE_SERVICE_URL="https://hulaspark.com/api"
# websocket服务地址
VITE_WEBSOCKET_URL="wss://hulaspark.com/websocket"
# 项目标题
+15
View File
@@ -139,5 +139,20 @@ If you think HuLa is helpful to you, welcome to sponsor HuLa. Your support is ou
## HuLa Community discussion groups
<img src="preview/wx.png" width="260" height="300" alt="微信群二维码" style="border-radius: 12px;" />
## List of sponsors
Thanks to the following sponsors for their support!
| Date | Sponsor | Sum | Platform |
|------|--------|------|------|
| 2025-02-8 | 邓伟 | ¥88 | 微信赞赏码 |
| 2025-02-8 | Boom.... | ¥100 | 微信赞赏码 |
| 2025-02-7 | dennis | ¥80 | gitee码云赞赏 |
| 2025-02-6 | 小二 | ¥62 | 微信转账 |
> Note: This list is manually updated. If you have sponsored but are not displayed in the list, please contact us by:
1. Submit Issue on GitHub
2. Send an email to: 2439646234@qq.com
3. Contact via WeChat: cy2439646234
## License
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FHuLaSpark%2FHuLa.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FHuLaSpark%2FHuLa?ref=badge_large)
+16 -1
View File
@@ -104,7 +104,7 @@ pnpm run tauri:build
网页上下载安装包会提示安装包已损坏,可能会遇到证书问题,这是因为 macOS 系统的安全机制导致的。请按照以下步骤解决:
#### 1. 打开 系统设置 - 安全性与隐私,如图勾选:允许 任何来源 下载的 App 运行:
#### 1. 打开 "系统设置" - "安全性与隐私",如图勾选:允许 "任何来源" 下载的 App 运行:
<div style="padding: 28px; display: inline-block;">
<img src="preview/img_9.png" alt="img_9.png" style="border-radius: 8px; display: block;" />
@@ -139,5 +139,20 @@ sudo xattr -rd com.apple.quarantine 你的安装包路径/HuLa.app
## HuLa社区讨论群
<img src="preview/wx.png" width="260" height="300" alt="微信群二维码" style="border-radius: 12px;" />
## 赞助者名单
感谢以下赞助者的支持!
| 日期 | 赞助者 | 金额 | 平台 |
|------|--------|------|------|
| 2025-02-8 | 邓伟 | ¥88 | 微信赞赏码 |
| 2025-02-8 | Boom.... | ¥100 | 微信赞赏码 |
| 2025-02-7 | dennis | ¥80 | gitee码云赞赏 |
| 2025-02-6 | 小二 | ¥62 | 微信转账 |
> 注:该名单为手动更新。如果您已赞助但未显示在列表中,请通过以下方式联系我们:
1. 在GitHub上提交Issue
2. 发送邮件至: 2439646234@qq.com
3. 通过微信联系: cy2439646234
## 许可证
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FHuLaSpark%2FHuLa.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FHuLaSpark%2FHuLa?ref=badge_large)
+1 -1
View File
@@ -43,7 +43,7 @@
"========= 校验代码规范 =========": "",
"lint:staged": "lint-staged && vue-tsc --noEmit",
"========= 安装husky =========": "",
"prepare": "husky install",
"prepare": "husky",
"========= 发版 =========": "",
"release": "release-it",
"========= commit后再次添加修改到上一次的commit =========": "",
+11 -2
View File
@@ -11,12 +11,15 @@ export interface ErrorDetails {
type: ErrorType
code?: number
details?: Record<string, any>
showError?: boolean
}
export class AppException extends Error {
public readonly type: ErrorType
public readonly code?: number
public readonly details?: Record<string, any>
// 使用静态标志位来追踪是否已经显示过错误消息
private static hasShownError = false
constructor(message: string, errorDetails?: Partial<ErrorDetails>) {
super(message)
@@ -25,9 +28,15 @@ export class AppException extends Error {
this.code = errorDetails?.code
this.details = errorDetails?.details
// Show error message to user if window.$message is available
if (window.$message) {
// 只有在明确指定显示错误时才显示
if (errorDetails?.showError && window.$message && !AppException.hasShownError) {
window.$message.error(message)
AppException.hasShownError = true
// 只有在 2 秒内没有显示过错误消息时才会显示
setTimeout(() => {
AppException.hasShownError = false
}, 2000)
}
}
+93 -15
View File
@@ -2,7 +2,12 @@
<n-image
v-if="body?.url"
class="select-none cursor-pointer"
:img-props="{ style: { height: getImageHeight + 'px' } }"
:img-props="{
style: {
...imageStyle
}
}"
object-fit="cover"
show-toolbar-tooltip
preview-disabled
style="border-radius: 8px"
@@ -15,9 +20,9 @@
</template>
</n-image>
</template>
<script setup lang="ts">
import type { ImageBody } from '@/services/types'
import { formatImage } from '@/utils/Formatting.ts'
import { useWindow } from '@/hooks/useWindow.ts'
import { useChatStore } from '@/stores/chat'
import { MsgEnum } from '@/enums/index'
@@ -27,6 +32,11 @@ const props = defineProps<{ body: ImageBody }>()
const chatStore = useChatStore()
const { createWebviewWindow } = useWindow()
const imageViewerStore = useImageViewer()
// 图片显示相关常量
const MAX_WIDTH = 320
const MAX_HEIGHT = 240
const MIN_WIDTH = 60
const MIN_HEIGHT = 60
// 获取当前聊天中的所有图片URL
const getAllImagesFromChat = () => {
@@ -59,25 +69,93 @@ const openImageViewer = async () => {
// 使用新的重置方法,自动去重并保持顺序
imageViewerStore.resetImageList(list, index)
await createWebviewWindow('图片查看', 'imageViewer', 630, 660, '', true, 630, 660)
// 获取当前图片的宽高
const img = new Image()
img.src = props.body.url
await new Promise((resolve, reject) => {
img.onload = resolve
img.onerror = reject
})
// 默认窗口尺寸(最小尺寸)
const MIN_WINDOW_WIDTH = 630
const MIN_WINDOW_HEIGHT = 660
// 计算实际窗口尺寸(保留一定边距)
const MARGIN = 100 // 窗口边距
let windowWidth = MIN_WINDOW_WIDTH
let windowHeight = MIN_WINDOW_HEIGHT
// 获取屏幕尺寸
const { width: screenWidth, height: screenHeight } = window.screen
// 计算最大可用尺寸(考虑边距)
const maxWidth = screenWidth - MARGIN * 2
const maxHeight = screenHeight - MARGIN * 2
// 保持图片比例计算窗口尺寸
const imageRatio = img.width / img.height
// 计算实际窗口尺寸
if (img.width > MIN_WINDOW_WIDTH || img.height > MIN_WINDOW_HEIGHT) {
if (imageRatio > maxWidth / maxHeight) {
// 以宽度为基准
windowWidth = Math.min(img.width + MARGIN, maxWidth)
windowHeight = Math.max(windowWidth / imageRatio + MARGIN, MIN_WINDOW_HEIGHT)
} else {
// 以高度为基准
windowHeight = Math.min(img.height + MARGIN, maxHeight)
windowWidth = Math.max(windowHeight * imageRatio + MARGIN, MIN_WINDOW_WIDTH)
}
}
// 创建窗口,使用计算后的尺寸
await createWebviewWindow(
'图片查看',
'imageViewer',
Math.round(windowWidth),
Math.round(windowHeight),
'',
true,
Math.round(windowWidth),
Math.round(windowHeight)
)
} catch (error) {
console.error('打开图片查看器失败:', error)
}
}
// 默认宽高
const DEFAULT_WIDTH = 200
const DEFAULT_HEIGHT = 150
/**
* 核心就是的到高度,产生明确占位防止图片加载时页面抖动
* @param width 宽度
* @param height 高度
* 计算图片样式
*/
const getImageHeight = computed(() => {
// 使用可选链和空值合并运算符设置默认值
const width = props.body?.width ?? DEFAULT_WIDTH
const height = props.body?.height ?? DEFAULT_HEIGHT
return formatImage(width, height)
const imageStyle = computed(() => {
const width = props.body?.width ?? MAX_WIDTH
const height = props.body?.height ?? MAX_HEIGHT
const aspectRatio = width / height
let finalWidth = width
let finalHeight = height
// 如果图片太大,需要等比缩放
if (width > MAX_WIDTH || height > MAX_HEIGHT) {
if (width / height > MAX_WIDTH / MAX_HEIGHT) {
// 宽度超出更多,以最大宽度为基准缩放
finalWidth = MAX_WIDTH
finalHeight = MAX_WIDTH / aspectRatio
} else {
// 高度超出更多,以最大高度为基准缩放
finalHeight = MAX_HEIGHT
finalWidth = MAX_HEIGHT * aspectRatio
}
}
// 确保不小于最小尺寸
finalWidth = Math.max(finalWidth, MIN_WIDTH)
finalHeight = Math.max(finalHeight, MIN_HEIGHT)
return {
width: `${finalWidth}px`,
height: `${finalHeight}px`
}
})
</script>
+8 -3
View File
@@ -29,11 +29,16 @@ export const useLogin = () => {
const logout = async () => {
const { createWebviewWindow } = useWindow()
isTrayMenuShow.value = false
// todo 退出账号 需要关闭其他的全部窗口
await createWebviewWindow('登录', 'login', 320, 448, 'home', false, 320, 448).then(async () => {
try {
// 创建登录窗口
await createWebviewWindow('登录', 'login', 320, 448, 'home', false, 320, 448)
// 调整托盘大小
await resizeWindow('tray', 130, 44)
// 发送登出事件
await emit(EventEnum.LOGOUT)
})
} catch (error) {
console.error('创建登录窗口失败:', error)
}
}
return {
+23 -19
View File
@@ -93,25 +93,29 @@ const moreList = ref<OPT.L.MoreList[]>([
label: '退出账号',
icon: 'power',
click: async () => {
// rust保存用户信息
await invoke('save_user_info', {
userId: -1,
username: '',
token: '',
portrait: '',
isSign: false
})
// 后端发布下线通知同时清除token
await apis.logout()
await logout()
// 如果没有设置自动登录,则清除用户信息
userStore.userInfo = {}
localStorage.removeItem('user')
localStorage.removeItem('TOKEN')
const headers = new Headers()
headers.append('Authorization', '')
userStore.isSign = false
loginStore.loginStatus = LoginStatus.Init
try {
// 1. 先调用后端退出接口
await apis.logout()
// 2. 保存 rust 端用户信息
await invoke('save_user_info', {
userId: -1,
username: '',
token: '',
portrait: '',
isSign: false
})
// 3. 清理本地存储
localStorage.removeItem('user')
localStorage.removeItem('TOKEN')
// 4. 重置用户状态
userStore.isSign = false
userStore.userInfo = {}
loginStore.loginStatus = LoginStatus.Init
// 5. 最后调用登出方法(这会创建登录窗口)
await logout()
} catch (error) {
console.error('退出登录失败:', error)
}
}
}
])
+4
View File
@@ -20,10 +20,14 @@
@include action;
}
/** 通用的 active 样式 */
.active {
background: var(--left-active-bg-color);
border-radius: 8px;
color: var(--left-active-icon-color);
img {
background: var(--icon-hover-color);
}
}
.setting-item {
+9 -6
View File
@@ -125,15 +125,18 @@ export default {
roomId
}),
/** 账号密码登录 */
login: (user: LoginUserReq, abort?: AbortController) => POST<string>(urls.login, user, abort),
login: (user: LoginUserReq, abort?: AbortController, noRetry = true) =>
POST<string>(urls.login, user, abort, noRetry),
/** 移动端登录 */
mobileLogin: (user: LoginUserReq, abort?: AbortController) => POST<string>(urls.mobileLogin, user, abort),
mobileLogin: (user: LoginUserReq, abort?: AbortController, noRetry = true) =>
POST<string>(urls.mobileLogin, user, abort, noRetry),
/** 退出登录 */
logout: (abort?: AbortController) => POST<string>(urls.logout, abort),
logout: () => POST<string>(urls.logout, void 0, void 0, true),
/** 注册 */
register: (user: RegisterUserReq) => POST<string>(urls.register, user),
register: (user: RegisterUserReq, abort?: AbortController, noRetry = true) =>
POST<string>(urls.register, user, abort, noRetry),
/** 检查token是否有效 */
checkToken: () => POST<string>(urls.checkToken),
checkToken: () => POST<string>(urls.checkToken, void 0, void 0, true),
/** 下线 */
offline: () => POST<string>(urls.offline)
offline: () => POST<string>(urls.offline, void 0, void 0, true)
}
+9 -8
View File
@@ -8,6 +8,7 @@ import { AppException, ErrorType } from '../common/exception'
* @property {Record<string, any>} [query] 请求参数
* @property {any} [body] 请求体
* @property {boolean} [isBlob] 是否为Blob
* @property {RetryOptions} [retry] 重试选项
* @property {boolean} [noRetry] 是否禁用重试
* @return HttpParams
*/
@@ -184,11 +185,11 @@ async function Http<T = any>(
// 若有success === false,需要重试
if (responseData && responseData.success === false) {
const errorMessage = responseData.errMsg || '服务器返回错误'
window.$message?.error?.(errorMessage)
throw new AppException(errorMessage, {
type: ErrorType.Server,
code: response.status,
details: responseData
details: responseData,
showError: !shouldRetry(currentAttempt, retries, abort) // 只在最后一次重试失败时显示错误
})
}
@@ -204,26 +205,26 @@ async function Http<T = any>(
if (!shouldRetry(currentAttempt, retries, abort)) {
console.error(`Max retries reached or aborted. Request failed → ${url}`)
if (error instanceof FetchRetryError) {
window.$message?.error?.(error.message || '网络请求失败')
throw new AppException(error.message, {
type: error.type,
code: error.status,
details: { url, attempts: currentAttempt + 1 }
details: { url, attempts: currentAttempt + 1 },
showError: true // 重试次数用完后显示错误
})
}
if (error instanceof AppException) {
window.$message?.error?.(error.message || '请求出错')
// 如果是 AppException,保持原有的 showError 设置
throw error
}
const errorMessage = String(error) || '未知错误'
window.$message?.error?.(errorMessage)
throw new AppException(errorMessage, {
type: ErrorType.Unknown,
details: { url, attempts: currentAttempt + 1 }
details: { url, attempts: currentAttempt + 1 },
showError: true // 重试次数用完后显示错误
})
}
// 若需继续重试
// 若需继续重试,不显示错误消息
const delayMs = retryDelay ? retryDelay(currentAttempt) : 1000
console.warn(`Retrying request → ${url} (next attempt: ${currentAttempt + 2}, waiting ${delayMs}ms)`)
await wait(delayMs)
+9 -3
View File
@@ -1,5 +1,7 @@
import Http, { HttpParams } from './http.ts'
import { ServiceResponse } from '@/services/types.ts'
import { AppException } from '@/common/exception'
import { ErrorType } from '@/common/exception'
function getToken() {
let tempToken = ''
@@ -52,12 +54,16 @@ const responseInterceptor = async <T>(
const serviceData = (await data.data) as ServiceResponse
//检查服务端返回是否成功,并且中断请求
if (!serviceData.success) {
window.$message.error(serviceData.errMsg)
return Promise.reject(`http error: ${serviceData.errMsg}`)
return Promise.reject(
new AppException(serviceData.errMsg, {
type: ErrorType.Server,
showError: true
})
)
}
return Promise.resolve(serviceData.data)
} catch (err) {
return Promise.reject(`http error: ${err}`)
return Promise.reject(err)
}
}
+17 -4
View File
@@ -8,10 +8,23 @@ export const useImageViewer = defineStore(
const currentIndex = ref(0)
// 添加一个重置方法,用于设置新的图片列表
const resetImageList = (list: string[], index: number) => {
// 去重但保持原有顺序
imageList.value = Array.from(new Set(list))
currentIndex.value = index
const resetImageList = (list: string[], originalIndex: number) => {
// 创建一个去重后的新数组,同时保持原有顺序
const uniqueList: string[] = []
const seenUrls = new Set<string>()
for (const url of list) {
if (!seenUrls.has(url)) {
uniqueList.push(url)
seenUrls.add(url)
}
}
// 找到原始图片在去重后的新列表中的索引
const newIndex = uniqueList.indexOf(list[originalIndex])
imageList.value = uniqueList
currentIndex.value = newIndex !== -1 ? newIndex : 0
}
return {
+1 -1
View File
@@ -24,7 +24,7 @@
}
.bubble {
&::selection {
background: red; /* 设置选中背景为透明 */
background: transparent; /* 设置选中背景为透明 */
}
@include bubble;
}
+4 -4
View File
@@ -29,10 +29,10 @@
// 消息列表样式
--msg-active-color: #13987f;
//列表通用hover
// 列表通用hover
--list-hover-color: rgba(99, 99, 99, 0.1);
//整体hover图标的样式
// 整体hover图标的样式
--icon-hover-color: rgba(166, 166, 166, 0.2);
}
}
@@ -67,10 +67,10 @@ html[data-theme='dark'] {
// 消息列表样式
--msg-active-color: rgba(19, 152, 127, 0.4);
//列表通用hover
// 列表通用hover
--list-hover-color: rgba(244, 244, 244, 0.1);
//整体hover图标的样式
// 整体hover图标的样式
--icon-hover-color: rgba(90, 90, 90, 0.4);
}
}
+4 -4
View File
@@ -25,10 +25,10 @@
// 消息列表样式
--msg-active-color: #13987f;
//列表通用hover
// 列表通用hover
--list-hover-color: rgba(99, 99, 99, 0.1);
//整体hover图标的样式
// 整体hover图标的样式
--icon-hover-color: #e7e7e7;
// 移动端样式
@@ -62,10 +62,10 @@ html[data-theme='dark'] {
// 消息列表样式
--msg-active-color: rgba(19, 152, 127, 0.4);
//列表通用hover
// 列表通用hover
--list-hover-color: rgba(244, 244, 244, 0.1);
//整体hover图标的样式
// 整体hover图标的样式
--icon-hover-color: #242424;
// 移动端样式
-28
View File
@@ -16,34 +16,6 @@ export const formatBytes = (bytes: number): string => {
return size + ' ' + units[unitIndex]
}
/**
* 图片尺寸格式化
* @param width 图片宽度
* @param height 图片高度
* @param option 选项: { maxWidth: number, maxHeight: number }
*/
export const formatImage = (
width: number,
height: number,
option = {
maxWidth: 200,
maxHeight: 150
}
): number => {
const { maxWidth, maxHeight } = option
// 小: 如果图片宽高都小于最大宽高,直接返回原高度
if (width < maxWidth && height < maxHeight) {
return height
// 宽: 根据宽度等比缩放
} else if (width > height) {
return (maxWidth / width) * height
// 窄:返回最大高度
} else if (width === height || width < height) {
return maxHeight
}
return maxHeight
}
/** 注意!这是文件图标映射关系表,如有修改需求-请联系前端管理同学 */
const fileSuffixMap: Record<string, string> = {
jpg: 'jpg',
+112 -81
View File
@@ -1,15 +1,12 @@
<template>
<div class="size-full bg-#222">
<div class="size-full bg-#222 relative flex flex-col" @mousemove="handleMouseMove" @mouseleave="handleMouseLeave">
<!-- 顶部操作栏 -->
<ActionBar class="bg-#000" :shrink="false" :current-label="currentLabel" />
<ActionBar class="bg-#000 z-9999" :shrink="false" :current-label="currentLabel" />
<!-- 主体内容区域 -->
<div style="height: calc(100% - 24px)" class="relative w-full flex flex-col">
<div class="flex-1 overflow-auto">
<!-- 图片展示区域 -->
<div
class="flex-1 relative flex-center"
@mousemove="handleMouseMove"
@mouseleave="showArrows.left = showArrows.right = false">
<div class="min-h-[calc(100vh-124px)] flex-center">
<img
ref="imageRef"
:src="currentImage"
@@ -20,84 +17,87 @@
class="max-w-90% max-h-90% select-none"
:class="{ 'transition-transform duration-200': !isDragging }"
@mousedown="startDrag"
@wheel.passive="handleWheel"
alt="preview" />
<!-- 提示文本 -->
<transition name="viewer-tip">
<div
v-if="showTip"
class="absolute z-10 bg-black/60 px-24px py-12px rounded-8px text-(white 14px) transition-all duration-300 backdrop-blur-sm select-none flex items-center gap-8px">
class="fixed z-10 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-black/60 px-24px py-12px rounded-8px text-(white 14px) transition-all duration-300 backdrop-blur-sm select-none flex items-center gap-8px">
<svg class="size-16px"><use href="#info"></use></svg>
{{ tipText }}
</div>
</transition>
<!-- 左右箭头 -->
<div
v-show="imageList.length > 1 && showArrows.left"
@click="prevImage"
class="absolute left-20px top-1/2 -translate-y-1/2 size-40px rounded-full bg-black/30 flex-center cursor-pointer hover:bg-black/50 transition-all duration-200 opacity-0"
:class="{ 'opacity-100': showArrows.left }">
<svg class="size-24px color-white rotate-180"><use href="#arrow-right"></use></svg>
</div>
<div
v-show="imageList.length > 1 && showArrows.right"
@click="nextImage"
class="absolute right-20px top-1/2 -translate-y-1/2 size-40px rounded-full bg-black/30 flex-center cursor-pointer hover:bg-black/50 transition-all duration-200 opacity-0"
:class="{ 'opacity-100': showArrows.right }">
<svg class="size-24px color-white"><use href="#arrow-right"></use></svg>
</div>
</div>
</div>
<!-- 底部工具栏 -->
<div data-tauri-drag-region class="z-9999 h-50px bg-#000 flex justify-center items-center gap-30px">
<n-tooltip placement="top">
<template #trigger>
<svg @click="zoomOut" class="size-24px cursor-pointer color-white"><use href="#zoom-out"></use></svg>
</template>
缩小
</n-tooltip>
<!-- 左右箭头 -->
<div
v-show="imageList.length > 1 && showArrows.left"
@click="prevImage"
@mouseenter="handleArrowEnter('left')"
@mouseleave="handleArrowLeave('left')"
class="fixed left-20px top-1/2 -translate-y-1/2 size-40px rounded-full bg-black/30 flex-center cursor-pointer hover:bg-black/50 transition-all duration-200 opacity-0 z-10"
:class="{ 'opacity-100': showArrows.left }">
<svg class="size-24px color-white rotate-180"><use href="#arrow-right"></use></svg>
</div>
<div
v-show="imageList.length > 1 && showArrows.right"
@click="nextImage"
@mouseenter="handleArrowEnter('right')"
@mouseleave="handleArrowLeave('right')"
class="fixed right-20px top-1/2 -translate-y-1/2 size-40px rounded-full bg-black/30 flex-center cursor-pointer hover:bg-black/50 transition-all duration-200 opacity-0 z-10"
:class="{ 'opacity-100': showArrows.right }">
<svg class="size-24px color-white"><use href="#arrow-right"></use></svg>
</div>
<span class="color-white text-14px min-w-50px text-center select-none">{{ scaleText }}</span>
<!-- 底部工具栏 -->
<div data-tauri-drag-region class="z-9999 h-50px bg-#000 flex justify-center items-center gap-30px">
<n-tooltip placement="top">
<template #trigger>
<svg @click="zoomOut" class="size-24px cursor-pointer color-white"><use href="#zoom-out"></use></svg>
</template>
缩小
</n-tooltip>
<n-tooltip placement="top">
<template #trigger>
<svg @click="zoomIn" class="size-24px cursor-pointer color-white"><use href="#zoom-in"></use></svg>
</template>
放大
</n-tooltip>
<span class="color-white text-14px min-w-50px text-center select-none">{{ scaleText }}</span>
<n-tooltip placement="top">
<template #trigger>
<svg @click="rotateLeft" class="size-24px cursor-pointer scale-x--100 color-white">
<use href="#RotateRight"></use>
</svg>
</template>
向左旋转
</n-tooltip>
<n-tooltip placement="top">
<template #trigger>
<svg @click="zoomIn" class="size-24px cursor-pointer color-white"><use href="#zoom-in"></use></svg>
</template>
放大
</n-tooltip>
<n-tooltip placement="top">
<template #trigger>
<svg @click="rotateRight" class="size-24px cursor-pointer color-white"><use href="#RotateRight"></use></svg>
</template>
向右旋转
</n-tooltip>
<n-tooltip placement="top">
<template #trigger>
<svg @click="rotateLeft" class="size-24px cursor-pointer scale-x--100 color-white">
<use href="#RotateRight"></use>
</svg>
</template>
向左旋转
</n-tooltip>
<n-tooltip placement="top">
<template #trigger>
<svg @click="resetImage()" class="size-24px cursor-pointer color-white"><use href="#refresh"></use></svg>
</template>
重置
</n-tooltip>
<n-tooltip placement="top">
<template #trigger>
<svg @click="rotateRight" class="size-24px cursor-pointer color-white"><use href="#RotateRight"></use></svg>
</template>
向右旋转
</n-tooltip>
<n-tooltip placement="top">
<template #trigger>
<svg @click="saveImage" class="size-24px cursor-pointer color-white"><use href="#Importing"></use></svg>
</template>
另存为
</n-tooltip>
</div>
<n-tooltip placement="top">
<template #trigger>
<svg @click="resetImage()" class="size-24px cursor-pointer color-white"><use href="#refresh"></use></svg>
</template>
重置
</n-tooltip>
<n-tooltip placement="top">
<template #trigger>
<svg @click="saveImage" class="size-24px cursor-pointer color-white"><use href="#Importing"></use></svg>
</template>
另存为
</n-tooltip>
</div>
</div>
</template>
@@ -131,7 +131,9 @@ const tipText = ref('')
// 左右箭头显示
const showArrows = reactive({
left: false,
right: false
right: false,
leftHover: false,
rightHover: false
})
// 添加缩放倍数显示的计算属性
@@ -147,10 +149,35 @@ const handleMouseMove = (e: MouseEvent) => {
const { innerWidth } = window
// 左侧箭头显示逻辑
showArrows.left = clientX <= 78
if (!showArrows.leftHover) {
showArrows.left = clientX <= 78
}
// 右侧箭头显示逻辑
showArrows.right = innerWidth - clientX <= 78
if (!showArrows.rightHover) {
showArrows.right = innerWidth - clientX <= 78
}
}
// 添加鼠标离开整个容器的处理
const handleMouseLeave = () => {
if (!showArrows.leftHover) {
showArrows.left = false
}
if (!showArrows.rightHover) {
showArrows.right = false
}
}
// 添加箭头hover状态处理
const handleArrowEnter = (direction: 'left' | 'right') => {
showArrows[`${direction}Hover`] = true
showArrows[direction] = true
}
const handleArrowLeave = (direction: 'left' | 'right') => {
showArrows[`${direction}Hover`] = false
showArrows[direction] = false
}
// 图片拖动相关
@@ -188,17 +215,6 @@ const handleDrag = (e: MouseEvent) => {
updateTransform()
}
// 修改滚轮缩放
const handleWheel = (e: WheelEvent) => {
e.preventDefault()
const delta = e.deltaY > 0 ? -0.1 : 0.1
const newScale = Math.max(0.1, Math.min(5, scale.value + delta))
if (newScale !== scale.value) {
scale.value = newScale
updateTransform()
}
}
// 工具栏操作
const zoomIn = () => {
scale.value = Math.min(5, scale.value + 0.1)
@@ -348,4 +364,19 @@ onUnmounted(() => {
.viewer-tip-leave-to {
opacity: 0;
}
/* 自定义滚动条样式 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
::-webkit-scrollbar-track {
background: transparent;
}
</style>
+1 -1
View File
@@ -371,7 +371,7 @@ onMounted(async () => {
await getCurrentWebviewWindow().show()
useMitt.on(WsResponseMessageType.NO_INTERNET, () => {
loginDisabled.value = true
loginText.value = '网络已断开'
loginText.value = '服务异常断开'
})
// 自动登录
if (login.value.autoLogin && TOKEN.value) {
+3 -3
View File
@@ -105,9 +105,9 @@ onMounted(async () => {
}
/** 隐藏naive UI的滚动条 */
:deep(
.n-scrollbar > .n-scrollbar-rail.n-scrollbar-rail--vertical > .n-scrollbar-rail__scrollbar,
.n-scrollbar + .n-scrollbar-rail.n-scrollbar-rail--vertical > .n-scrollbar-rail__scrollbar
) {
.n-scrollbar > .n-scrollbar-rail.n-scrollbar-rail--vertical > .n-scrollbar-rail__scrollbar,
.n-scrollbar + .n-scrollbar-rail.n-scrollbar-rail--vertical > .n-scrollbar-rail__scrollbar
) {
display: none;
}
</style>