mirror of
https://github.com/HuLaSpark/HuLa.git
synced 2026-09-21 12:43:20 +08:00
perf(common): ⚡ 优化图片查看器和请求重试报错机制
增加赞助者名单
This commit is contained in:
+1
-1
@@ -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"
|
||||
# 项目标题
|
||||
|
||||
@@ -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
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FHuLaSpark%2FHuLa?ref=badge_large)
|
||||
|
||||
@@ -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
|
||||
|
||||
## 许可证
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FHuLaSpark%2FHuLa?ref=badge_large)
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@
|
||||
"========= 校验代码规范 =========": "",
|
||||
"lint:staged": "lint-staged && vue-tsc --noEmit",
|
||||
"========= 安装husky =========": "",
|
||||
"prepare": "husky install",
|
||||
"prepare": "husky",
|
||||
"========= 发版 =========": "",
|
||||
"release": "release-it",
|
||||
"========= commit后再次添加修改到上一次的commit =========": "",
|
||||
|
||||
+11
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
}
|
||||
.bubble {
|
||||
&::selection {
|
||||
background: red; /* 设置选中背景为透明 */
|
||||
background: transparent; /* 设置选中背景为透明 */
|
||||
}
|
||||
@include bubble;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
// 移动端样式
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user