From 25c39b73c5bb8580d79648e430c2700f39810bd8 Mon Sep 17 00:00:00 2001 From: Dawn <2439646234@qq.com> Date: Sat, 8 Feb 2025 18:01:35 +0800 Subject: [PATCH] =?UTF-8?q?perf(common):=20:zap:=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=9F=A5=E7=9C=8B=E5=99=A8=E5=92=8C=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E9=87=8D=E8=AF=95=E6=8A=A5=E9=94=99=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加赞助者名单 --- .env.production | 2 +- README.en.md | 15 ++ README.md | 17 +- package.json | 2 +- src/common/exception.ts | 13 +- .../rightBox/renderMessage/Image.vue | 108 ++++++++-- src/hooks/useLogin.ts | 11 +- src/layout/left/config.tsx | 42 ++-- src/layout/left/style.scss | 4 + src/services/apis.ts | 15 +- src/services/http.ts | 17 +- src/services/request.ts | 12 +- src/stores/imageViewer.ts | 21 +- src/styles/scss/chat-main.scss | 2 +- src/styles/scss/theme/default.scss | 8 +- src/styles/scss/theme/simple.scss | 8 +- src/utils/Formatting.ts | 28 --- src/views/imageViewerWindow/index.vue | 193 ++++++++++-------- src/views/loginWindow/Login.vue | 2 +- src/views/onlineStatusWindow/index.vue | 6 +- 20 files changed, 341 insertions(+), 185 deletions(-) diff --git a/.env.production b/.env.production index a6a9b640..a7e44193 100644 --- a/.env.production +++ b/.env.production @@ -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" # 项目标题 diff --git a/README.en.md b/README.en.md index e1f453f3..f3698ae3 100644 --- a/README.en.md +++ b/README.en.md @@ -139,5 +139,20 @@ If you think HuLa is helpful to you, welcome to sponsor HuLa. Your support is ou ## HuLa Community discussion groups 微信群二维码 +## 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) diff --git a/README.md b/README.md index b17187af..4937016a 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ pnpm run tauri:build 网页上下载安装包会提示安装包已损坏,可能会遇到证书问题,这是因为 macOS 系统的安全机制导致的。请按照以下步骤解决: -#### 1. 打开 “系统设置” - “安全性与隐私”,如图勾选:允许 “任何来源” 下载的 App 运行: +#### 1. 打开 "系统设置" - "安全性与隐私",如图勾选:允许 "任何来源" 下载的 App 运行:
img_9.png @@ -139,5 +139,20 @@ sudo xattr -rd com.apple.quarantine 你的安装包路径/HuLa.app ## HuLa社区讨论群 微信群二维码 +## 赞助者名单 +感谢以下赞助者的支持! + +| 日期 | 赞助者 | 金额 | 平台 | +|------|--------|------|------| +| 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) diff --git a/package.json b/package.json index fa1c5289..c20a56a5 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "========= 校验代码规范 =========": "", "lint:staged": "lint-staged && vue-tsc --noEmit", "========= 安装husky =========": "", - "prepare": "husky install", + "prepare": "husky", "========= 发版 =========": "", "release": "release-it", "========= commit后再次添加修改到上一次的commit =========": "", diff --git a/src/common/exception.ts b/src/common/exception.ts index cfe386a7..9969ef77 100644 --- a/src/common/exception.ts +++ b/src/common/exception.ts @@ -11,12 +11,15 @@ export interface ErrorDetails { type: ErrorType code?: number details?: Record + showError?: boolean } export class AppException extends Error { public readonly type: ErrorType public readonly code?: number public readonly details?: Record + // 使用静态标志位来追踪是否已经显示过错误消息 + private static hasShownError = false constructor(message: string, errorDetails?: Partial) { 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) } } diff --git a/src/components/rightBox/renderMessage/Image.vue b/src/components/rightBox/renderMessage/Image.vue index 11c288c2..10646e05 100644 --- a/src/components/rightBox/renderMessage/Image.vue +++ b/src/components/rightBox/renderMessage/Image.vue @@ -2,7 +2,12 @@ + diff --git a/src/hooks/useLogin.ts b/src/hooks/useLogin.ts index 1f347515..9fa7cc74 100644 --- a/src/hooks/useLogin.ts +++ b/src/hooks/useLogin.ts @@ -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 { diff --git a/src/layout/left/config.tsx b/src/layout/left/config.tsx index 8ef8c136..91e3b941 100644 --- a/src/layout/left/config.tsx +++ b/src/layout/left/config.tsx @@ -93,25 +93,29 @@ const moreList = ref([ 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) + } } } ]) diff --git a/src/layout/left/style.scss b/src/layout/left/style.scss index cc7968a1..b170a0ef 100644 --- a/src/layout/left/style.scss +++ b/src/layout/left/style.scss @@ -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 { diff --git a/src/services/apis.ts b/src/services/apis.ts index 00c339b4..e6c67b37 100644 --- a/src/services/apis.ts +++ b/src/services/apis.ts @@ -125,15 +125,18 @@ export default { roomId }), /** 账号密码登录 */ - login: (user: LoginUserReq, abort?: AbortController) => POST(urls.login, user, abort), + login: (user: LoginUserReq, abort?: AbortController, noRetry = true) => + POST(urls.login, user, abort, noRetry), /** 移动端登录 */ - mobileLogin: (user: LoginUserReq, abort?: AbortController) => POST(urls.mobileLogin, user, abort), + mobileLogin: (user: LoginUserReq, abort?: AbortController, noRetry = true) => + POST(urls.mobileLogin, user, abort, noRetry), /** 退出登录 */ - logout: (abort?: AbortController) => POST(urls.logout, abort), + logout: () => POST(urls.logout, void 0, void 0, true), /** 注册 */ - register: (user: RegisterUserReq) => POST(urls.register, user), + register: (user: RegisterUserReq, abort?: AbortController, noRetry = true) => + POST(urls.register, user, abort, noRetry), /** 检查token是否有效 */ - checkToken: () => POST(urls.checkToken), + checkToken: () => POST(urls.checkToken, void 0, void 0, true), /** 下线 */ - offline: () => POST(urls.offline) + offline: () => POST(urls.offline, void 0, void 0, true) } diff --git a/src/services/http.ts b/src/services/http.ts index d34ac9d3..612a3d62 100644 --- a/src/services/http.ts +++ b/src/services/http.ts @@ -8,6 +8,7 @@ import { AppException, ErrorType } from '../common/exception' * @property {Record} [query] 请求参数 * @property {any} [body] 请求体 * @property {boolean} [isBlob] 是否为Blob + * @property {RetryOptions} [retry] 重试选项 * @property {boolean} [noRetry] 是否禁用重试 * @return HttpParams */ @@ -184,11 +185,11 @@ async function Http( // 若有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( 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) diff --git a/src/services/request.ts b/src/services/request.ts index af9e1303..5b75da14 100644 --- a/src/services/request.ts +++ b/src/services/request.ts @@ -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 ( 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) } } diff --git a/src/stores/imageViewer.ts b/src/stores/imageViewer.ts index 5dace076..05d388fd 100644 --- a/src/stores/imageViewer.ts +++ b/src/stores/imageViewer.ts @@ -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() + + 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 { diff --git a/src/styles/scss/chat-main.scss b/src/styles/scss/chat-main.scss index 9b920635..04822e02 100644 --- a/src/styles/scss/chat-main.scss +++ b/src/styles/scss/chat-main.scss @@ -24,7 +24,7 @@ } .bubble { &::selection { - background: red; /* 设置选中背景为透明 */ + background: transparent; /* 设置选中背景为透明 */ } @include bubble; } diff --git a/src/styles/scss/theme/default.scss b/src/styles/scss/theme/default.scss index 3f8963fd..75aba70b 100644 --- a/src/styles/scss/theme/default.scss +++ b/src/styles/scss/theme/default.scss @@ -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); } } diff --git a/src/styles/scss/theme/simple.scss b/src/styles/scss/theme/simple.scss index 76eefce0..370341a8 100644 --- a/src/styles/scss/theme/simple.scss +++ b/src/styles/scss/theme/simple.scss @@ -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; // 移动端样式 diff --git a/src/utils/Formatting.ts b/src/utils/Formatting.ts index 177ce168..57fe11f6 100644 --- a/src/utils/Formatting.ts +++ b/src/utils/Formatting.ts @@ -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 = { jpg: 'jpg', diff --git a/src/views/imageViewerWindow/index.vue b/src/views/imageViewerWindow/index.vue index 87cde410..e3e976b5 100644 --- a/src/views/imageViewerWindow/index.vue +++ b/src/views/imageViewerWindow/index.vue @@ -1,15 +1,12 @@ @@ -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; +} diff --git a/src/views/loginWindow/Login.vue b/src/views/loginWindow/Login.vue index 4f15732f..843441c9 100644 --- a/src/views/loginWindow/Login.vue +++ b/src/views/loginWindow/Login.vue @@ -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) { diff --git a/src/views/onlineStatusWindow/index.vue b/src/views/onlineStatusWindow/index.vue index 2694535c..6dd2591c 100644 --- a/src/views/onlineStatusWindow/index.vue +++ b/src/views/onlineStatusWindow/index.vue @@ -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; }