From c9fbc441bd33e2f450feb76531dbb212567de58c Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Sun, 26 Apr 2026 14:11:05 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=95=8C=E9=9D=A2=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=92=8C=E4=BD=BF=E7=94=A8sse=E6=9B=BF=E6=8D=A2websoc?= =?UTF-8?q?ket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 3 - package.json | 1 - src/api/user.ts | 84 +---- src/composables/useCountdown.ts | 39 +++ src/composables/useNavigation.ts | 38 ++ src/composables/useSse.ts | 214 ++++++++++++ src/composables/useStomp.ts | 368 -------------------- src/pages/index/index.vue | 64 +--- src/pages/login/index.vue | 86 ++--- src/pages/mine/account/index.vue | 39 +-- src/pages/mine/index.vue | 101 +++--- src/pages/mine/profile/complete-profile.vue | 10 +- src/pages/mine/profile/index.vue | 5 - src/pages/mine/settings/index.vue | 12 +- src/pages/mine/settings/theme/index.vue | 2 +- src/pages/work/config/index.vue | 10 +- src/pages/work/dept/index.vue | 8 +- src/pages/work/dict/index.vue | 8 +- src/pages/work/dict/item/index.vue | 8 +- src/pages/work/index.vue | 36 +- src/pages/work/menu/index.vue | 8 +- src/pages/work/notice/index.vue | 10 +- src/pages/work/role/assign-perm.vue | 8 +- src/pages/work/role/index.vue | 8 +- src/pages/work/user/index.vue | 18 +- src/types/auto-imports.d.ts | 19 +- src/types/env.d.ts | 5 - src/utils/auth.ts | 68 ++-- src/utils/format.ts | 18 + src/utils/permission.ts | 159 ++------- src/utils/request.ts | 5 +- 31 files changed, 546 insertions(+), 916 deletions(-) create mode 100644 src/composables/useCountdown.ts create mode 100644 src/composables/useNavigation.ts create mode 100644 src/composables/useSse.ts delete mode 100644 src/composables/useStomp.ts create mode 100644 src/utils/format.ts diff --git a/.env.development b/.env.development index 0f1526e..9157991 100644 --- a/.env.development +++ b/.env.development @@ -9,6 +9,3 @@ VITE_APP_BASE_API = '/dev-api' # API 服务器的 URL # VITE_APP_API_URL = http://localhost:8000 VITE_APP_API_URL = https://api.youlai.tech - -# WebSocket 服务器的 URL,不配置默认关闭 WebSocket -#VITE_APP_WS_ENDPOINT= ws://localhost:4096/ws diff --git a/package.json b/package.json index cde8483..8ef7742 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,6 @@ "@dcloudio/uni-mp-weixin": "3.0.0-4020420240722002", "@dcloudio/uni-mp-xhs": "3.0.0-4020420240722002", "@dcloudio/uni-quickapp-webview": "3.0.0-4020420240722002", - "@stomp/stompjs": "^7.2.0", "@uni-helper/uni-use": "^0.19.15", "@vueuse/core": "9.13.0", "pinia": "^2.3.1", diff --git a/src/api/user.ts b/src/api/user.ts index 761464b..aecbf02 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -160,38 +160,24 @@ const UserAPI = { }; export default UserAPI; +interface BaseUser { + username?: string; + nickname?: string; + avatar?: string; + gender?: number; + mobile?: string; + email?: string; + deptName?: string; +} + /** 登录用户信息 */ -export interface UserInfo { +export interface UserInfo extends BaseUser { /** 用户ID */ userId?: number; - - /** 用户名 */ - username?: string; - - /** 昵称 */ - nickname?: string; - - /** 头像URL */ - avatar?: string; - - /** 手机号 */ - mobile?: string; - - /** 邮箱 */ - email?: string; - - /** 性别 */ - gender?: number; - - /** 部门名称 */ - deptName?: string; - /** 角色编码集合 */ roles?: string[]; - /** 权限标识集合 */ perms?: string[]; - /** 角色名称(前端计算字段,取 roles[0] 的中文映射) */ roleName?: string; } @@ -246,60 +232,22 @@ export interface UserItem { } /** 个人中心用户信息 */ -export interface UserProfile { +export interface UserProfile extends BaseUser { /** 用户ID */ id?: number; - - /** 用户名 */ - username?: string; - - /** 昵称 */ - nickname?: string; - - /** 头像URL */ - avatar?: string; - - /** 性别 */ - gender?: number; - - /** 手机号 */ - mobile?: string; - - /** 邮箱 */ - email?: string; - - /** 部门名称 */ - deptName?: string; - /** 角色名称,多个使用英文逗号(,)分割 */ roleNames?: string; - /** 创建时间 */ createTime?: string; } /** 个人中心用户信息表单 */ -export interface UserProfileForm { +export interface UserProfileForm extends Pick< + BaseUser, + "username" | "nickname" | "avatar" | "gender" | "mobile" | "email" +> { /** 用户ID */ id?: number; - - /** 用户名 */ - username?: string; - - /** 昵称 */ - nickname?: string; - - /** 头像URL */ - avatar?: string; - - /** 性别 */ - gender?: number; - - /** 手机号 */ - mobile?: string; - - /** 邮箱 */ - email?: string; } /** 修改密码表单 */ diff --git a/src/composables/useCountdown.ts b/src/composables/useCountdown.ts new file mode 100644 index 0000000..17d304e --- /dev/null +++ b/src/composables/useCountdown.ts @@ -0,0 +1,39 @@ +import { ref, onUnmounted } from "vue"; + +export function useCountdown(duration = 60) { + const countdown = ref(0); + const isRunning = ref(false); + let timer: ReturnType | null = null; + + const start = () => { + if (isRunning.value) return; + + countdown.value = duration; + isRunning.value = true; + + timer = setInterval(() => { + countdown.value--; + if (countdown.value <= 0) { + stop(); + } + }, 1000); + }; + + const stop = () => { + if (timer) { + clearInterval(timer); + timer = null; + } + countdown.value = 0; + isRunning.value = false; + }; + + onUnmounted(() => stop()); + + return { + countdown, + isRunning, + start, + stop, + }; +} diff --git a/src/composables/useNavigation.ts b/src/composables/useNavigation.ts new file mode 100644 index 0000000..ba3652e --- /dev/null +++ b/src/composables/useNavigation.ts @@ -0,0 +1,38 @@ +import { checkLogin } from "@/utils/auth"; + +interface NavItem { + url?: string; + [key: string]: any; +} + +export function useNavigation() { + const handleNavClick = (item: NavItem) => { + if (!item.url) { + uni.showToast({ title: "功能开发中", icon: "none" }); + return; + } + + if (!checkLogin()) return; + + if (item.url.startsWith("http://") || item.url.startsWith("https://")) { + // #ifdef H5 + window.open(item.url, "_blank"); + // #endif + // #ifndef H5 + uni.navigateTo({ + url: `/pages/webview/index?url=${encodeURIComponent(item.url)}`, + }); + // #endif + return; + } + + uni.navigateTo({ + url: item.url, + fail: () => { + uni.showToast({ title: "页面不存在", icon: "none" }); + }, + }); + }; + + return { handleNavClick }; +} diff --git a/src/composables/useSse.ts b/src/composables/useSse.ts new file mode 100644 index 0000000..eddeade --- /dev/null +++ b/src/composables/useSse.ts @@ -0,0 +1,214 @@ +import { getAccessToken } from "@/utils/auth"; + +export interface UseSseOptions { + url?: string; + debug?: boolean; + connectionTimeout?: number; + reconnectInterval?: number; + maxReconnectInterval?: number; + maxReconnectAttempts?: number; +} + +type EventHandler = (data: any) => void; + +export enum SseConnectionState { + DISCONNECTED = "DISCONNECTED", + CONNECTING = "CONNECTING", + CONNECTED = "CONNECTED", +} + +let globalInstance: ReturnType | null = null; + +function createSseConnection(options: UseSseOptions = {}) { + const baseUrl = import.meta.env.VITE_APP_BASE_API; + const defaultUrl = `${baseUrl}/api/v1/sse/connect`; + + const config = { + url: options.url ?? defaultUrl, + debug: options.debug ?? false, + connectionTimeout: options.connectionTimeout ?? 10000, + reconnectInterval: options.reconnectInterval ?? 5000, + maxReconnectInterval: options.maxReconnectInterval ?? 120000, + maxReconnectAttempts: options.maxReconnectAttempts ?? 10, + }; + + const connectionState = ref(SseConnectionState.DISCONNECTED); + const isConnected = computed(() => connectionState.value === SseConnectionState.CONNECTED); + + let eventSource: EventSource | null = null; + let connectionTimeoutTimer: ReturnType | null = null; + let isManualDisconnect = false; + let reconnectTimer: ReturnType | null = null; + let reconnectAttempts = 0; + let currentReconnectInterval = config.reconnectInterval; + + const eventHandlers = new Map>(); + + const log = (...args: any[]) => config.debug && console.log("[SSE]", ...args); + const logError = (...args: any[]) => console.error("[SSE]", ...args); + + const clearTimer = (timer: typeof connectionTimeoutTimer) => { + if (timer) { + clearTimeout(timer); + return null; + } + return timer; + }; + + const scheduleReconnect = () => { + if (isManualDisconnect) return; + if (config.maxReconnectAttempts > 0 && reconnectAttempts >= config.maxReconnectAttempts) { + log(`已达到最大重试次数 ${config.maxReconnectAttempts},停止重连`); + return; + } + + reconnectAttempts++; + log(`将在 ${currentReconnectInterval}ms 后重试(${reconnectAttempts})`); + + reconnectTimer = setTimeout(() => { + connect(); + currentReconnectInterval = Math.min( + currentReconnectInterval * 2, + config.maxReconnectInterval + ); + }, currentReconnectInterval); + }; + + const connect = () => { + isManualDisconnect = false; + + if (connectionState.value !== SseConnectionState.DISCONNECTED) { + log("SSE 已连接或正在连接中,跳过重复连接"); + return; + } + + const token = getAccessToken(); + if (!token) { + log("未检测到有效令牌,跳过 SSE 连接"); + return; + } + + connectionState.value = SseConnectionState.CONNECTING; + + connectionTimeoutTimer = setTimeout(() => { + if (connectionState.value === SseConnectionState.CONNECTING) { + log("SSE 连接超时"); + disconnect(); + } + }, config.connectionTimeout); + + log("正在建立 SSE 连接..."); + + // SSE 连接地址附带 token 参数(EventSource 不支持自定义 Header) + const separator = config.url.includes("?") ? "&" : "?"; + const fullUrl = `${config.url}${separator}accessToken=${encodeURIComponent(token)}`; + + eventSource = new EventSource(fullUrl); + + eventSource.onopen = () => { + connectionTimeoutTimer = clearTimer(connectionTimeoutTimer); + connectionState.value = SseConnectionState.CONNECTED; + reconnectAttempts = 0; + currentReconnectInterval = config.reconnectInterval; + log("SSE 连接已建立"); + }; + + eventSource.onerror = () => { + logError("SSE 连接错误"); + connectionState.value = SseConnectionState.DISCONNECTED; + cleanupEventSource(); + scheduleReconnect(); + }; + + // 监听所有命名事件 + for (const [eventName] of eventHandlers) { + bindEvent(eventName); + } + + // 默认监听 message 事件 + bindEvent("message"); + }; + + const bindEvent = (eventName: string) => { + if (!eventSource) return; + eventSource.addEventListener(eventName, (event: MessageEvent) => { + const handlers = eventHandlers.get(eventName); + if (handlers) { + try { + const data = JSON.parse(event.data); + handlers.forEach((h) => h(data)); + } catch { + handlers.forEach((h) => h(event.data)); + } + } + log(`收到事件[${eventName}]:`, event.data); + }); + }; + + const on = (eventName: string, handler: EventHandler): (() => void) => { + if (!eventHandlers.has(eventName)) { + eventHandlers.set(eventName, new Set()); + // 如果已连接,动态绑定新事件 + if (eventSource) { + bindEvent(eventName); + } + } + eventHandlers.get(eventName)!.add(handler); + log(`已订阅事件: ${eventName}`); + + return () => { + const handlers = eventHandlers.get(eventName); + if (handlers) { + handlers.delete(handler); + if (handlers.size === 0) { + eventHandlers.delete(eventName); + } + } + }; + }; + + const cleanupEventSource = () => { + if (eventSource) { + eventSource.close(); + eventSource = null; + } + }; + + const disconnect = () => { + isManualDisconnect = true; + connectionTimeoutTimer = clearTimer(connectionTimeoutTimer); + reconnectTimer = clearTimer(reconnectTimer); + cleanupEventSource(); + connectionState.value = SseConnectionState.DISCONNECTED; + log("SSE 连接已断开"); + }; + + const cleanup = () => { + disconnect(); + eventHandlers.clear(); + log("SSE 资源已清理"); + }; + + return { + connectionState: readonly(connectionState), + isConnected, + connect, + disconnect, + cleanup, + on, + }; +} + +export function useSse(options: UseSseOptions = {}) { + if (!globalInstance) { + globalInstance = createSseConnection(options); + } + return globalInstance; +} + +export function cleanupSse() { + if (globalInstance) { + globalInstance.cleanup(); + globalInstance = null; + } +} diff --git a/src/composables/useStomp.ts b/src/composables/useStomp.ts deleted file mode 100644 index 6994013..0000000 --- a/src/composables/useStomp.ts +++ /dev/null @@ -1,368 +0,0 @@ -import { Client, type IMessage, type StompSubscription } from "@stomp/stompjs"; -import { getAccessToken } from "@/utils/auth"; - -export interface UseStompOptions { - /** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */ - brokerURL?: string; - /** 用于鉴权的 token,不传时使用 getAccessToken() 的返回值 */ - token?: string; - /** 重连延迟,单位毫秒,默认为 8000 */ - reconnectDelay?: number; - /** 连接超时时间,单位毫秒,默认为 10000 */ - connectionTimeout?: number; - /** 是否开启指数退避重连策略 */ - useExponentialBackoff?: boolean; - /** 最大重连次数,默认为 5 */ - maxReconnectAttempts?: number; - /** 最大重连延迟,单位毫秒,默认为 60000 */ - maxReconnectDelay?: number; - /** 是否开启调试日志 */ - debug?: boolean; -} - -/** - * STOMP WebSocket连接组合式函数 - * 用于管理WebSocket连接的建立、断开、重连和消息订阅 - */ -export function useStomp(options: UseStompOptions = {}) { - // 默认值:brokerURL 从环境变量中获取,token 从 getAccessToken() 获取 - const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || ""; - - const brokerURL = ref(options.brokerURL ?? defaultBrokerURL); - // 默认配置参数 - const reconnectDelay = options.reconnectDelay ?? 15000; // 默认15秒重连间隔 - const connectionTimeout = options.connectionTimeout ?? 10000; - const useExponentialBackoff = options.useExponentialBackoff ?? false; - const maxReconnectAttempts = options.maxReconnectAttempts ?? 3; // 最多重连3次 - const maxReconnectDelay = options.maxReconnectDelay ?? 60000; - - // 连接状态标记 - const isConnected = ref(false); - // 重连尝试次数 - const reconnectCount = ref(0); - // 重连计时器 - let reconnectTimer: any = null; - // 连接超时计时器 - let connectionTimeoutTimer: any = null; - // 存储所有订阅 - const subscriptions = new Map(); - - // 用于保存 STOMP 客户端的实例 - const client = ref(null); - // 防止重复连接的标志 - let isConnecting = false; - let isManualDisconnect = false; - - /** - * 初始化 STOMP 客户端 - */ - const initializeClient = () => { - // 如果客户端已存在且正在连接或已连接,直接返回 - if (client.value && (client.value.active || client.value.connected)) { - console.log("STOMP客户端已存在且处于活动状态,跳过初始化"); - return; - } - - // 检查WebSocket端点是否配置 - if (!brokerURL.value) { - console.error("WebSocket连接失败: 未配置WebSocket端点URL"); - return; - } - - // 每次连接前重新获取最新令牌,不依赖之前的token值 - const currentToken = getAccessToken(); - - // 检查令牌是否为空,如果为空则不进行连接 - if (!currentToken) { - console.error("WebSocket连接失败:授权令牌为空,请先登录"); - return; - } - - // 如果有旧的客户端,先清理 - if (client.value) { - try { - client.value.deactivate(); - } catch (error) { - console.warn("清理旧客户端时出错:", error); - } - client.value = null; - } - - // 创建 STOMP 客户端 - client.value = new Client({ - brokerURL: brokerURL.value, - connectHeaders: { - Authorization: `Bearer ${currentToken}`, - }, - debug: options.debug ? console.log : () => {}, - reconnectDelay: 0, // 禁用内置重连机制,使用自定义重连 - heartbeatIncoming: 4000, - heartbeatOutgoing: 4000, - }); - - // 设置连接监听器 - client.value.onConnect = () => { - isConnected.value = true; - isConnecting = false; - reconnectCount.value = 0; - clearTimeout(connectionTimeoutTimer); - clearTimeout(reconnectTimer); - console.log("WebSocket连接已建立"); - }; - - // 设置断开连接监听器 - client.value.onDisconnect = () => { - isConnected.value = false; - isConnecting = false; - console.log("WebSocket连接已断开"); - - // 如果不是手动断开且未达到最大重连次数,则尝试重连 - if (!isManualDisconnect && reconnectCount.value < maxReconnectAttempts) { - handleReconnect(); - } - }; - - // 设置 Web Socket 关闭监听器 - client.value.onWebSocketClose = (event: CloseEvent) => { - isConnected.value = false; - isConnecting = false; - console.log(`WebSocket已关闭: ${event?.code} ${event?.reason}`); - - // 如果是手动断开,不要重连 - if (isManualDisconnect) { - console.log("手动断开连接,不进行重连"); - return; - } - - // 如果是授权问题导致的关闭,尝试重连 - if ( - (event?.code === 1000 || event?.code === 1006 || event?.code === 1008) && - reconnectCount.value < maxReconnectAttempts - ) { - console.log("检测到连接异常关闭,将尝试重连"); - - // 通过 handleReconnect 统一处理重连,避免重复计数 - handleReconnect(); - } - }; - - // 设置错误监听器 - client.value.onStompError = (frame: any) => { - console.error("STOMP错误:", frame.headers, frame.body); - isConnecting = false; - - // 检查是否是授权错误 - if ( - frame.headers?.message?.includes("Unauthorized") || - frame.body?.includes("Unauthorized") || - frame.body?.includes("Token") - ) { - console.warn("WebSocket授权错误,请检查登录状态"); - // 授权错误不进行重连 - isManualDisconnect = true; - } - }; - }; - - /** - * 处理重连逻辑 - */ - const handleReconnect = () => { - // 如果已经在连接中或手动断开,不重连 - if (isConnecting || isManualDisconnect) { - return; - } - - if (reconnectCount.value >= maxReconnectAttempts) { - console.error(`已达到最大重连次数(${maxReconnectAttempts}),停止重连`); - return; - } - - reconnectCount.value++; - console.log(`准备重连(${reconnectCount.value}/${maxReconnectAttempts})...`); - - // 使用指数退避策略增加重连间隔 - const delay = useExponentialBackoff - ? Math.min(reconnectDelay * Math.pow(2, reconnectCount.value - 1), maxReconnectDelay) - : reconnectDelay; - - // 清除之前的计时器 - if (reconnectTimer) { - clearTimeout(reconnectTimer); - } - - // 设置重连计时器 - reconnectTimer = setTimeout(() => { - if (!isConnected.value && !isManualDisconnect && !isConnecting) { - console.log(`开始重连...`); - connect(); - } - }, delay); - }; - - // 监听 brokerURL 的变化,若地址改变则重新初始化 - watch(brokerURL, (newURL, oldURL) => { - if (newURL !== oldURL) { - console.log(`brokerURL changed from ${oldURL} to ${newURL}`); - // 断开当前连接,重新激活客户端 - if (client.value && client.value.connected) { - client.value.deactivate(); - } - brokerURL.value = newURL; - initializeClient(); // 重新初始化客户端 - } - }); - - // 初始化客户端 - initializeClient(); - - /** - * 激活连接(如果已经连接或正在激活则直接返回) - */ - const connect = () => { - // 重置手动断开标志 - isManualDisconnect = false; - - // 检查是否有配置WebSocket端点 - if (!brokerURL.value) { - console.error("WebSocket连接失败: 未配置WebSocket端点URL"); - return; - } - - // 防止重复连接 - if (isConnecting) { - console.log("WebSocket正在连接中,跳过重复连接请求"); - return; - } - - if (!client.value) { - initializeClient(); - } - - if (!client.value) { - console.error("STOMP客户端初始化失败"); - return; - } - - // 避免重复连接:检查是否已连接 - if (client.value.connected) { - console.log("WebSocket已经连接,跳过重复连接"); - isConnected.value = true; - return; - } - - // 设置连接标志 - isConnecting = true; - - // 设置连接超时 - clearTimeout(connectionTimeoutTimer); - connectionTimeoutTimer = setTimeout(() => { - if (!isConnected.value && isConnecting) { - console.warn("WebSocket连接超时"); - isConnecting = false; - if (!isManualDisconnect && reconnectCount.value < maxReconnectAttempts) { - handleReconnect(); - } - } - }, connectionTimeout); - - try { - client.value.activate(); - console.log("正在建立WebSocket连接..."); - } catch (error) { - console.error("激活WebSocket连接失败:", error); - isConnecting = false; - } - }; - - /** - * 订阅指定主题 - * @param destination 目标主题地址 - * @param callback 接收到消息时的回调函数 - * @returns 返回订阅 id,用于后续取消订阅 - */ - const subscribe = (destination: string, callback: (_message: IMessage) => void): string => { - if (!client.value || !client.value.connected) { - console.warn(`尝试订阅 ${destination} 失败: 客户端未连接`); - return ""; - } - - try { - const subscription = client.value.subscribe(destination, callback); - const subscriptionId = subscription.id; - subscriptions.set(subscriptionId, subscription); - console.log(`订阅成功: ${destination}, ID: ${subscriptionId}`); - return subscriptionId; - } catch (error) { - console.error(`订阅 ${destination} 失败:`, error); - return ""; - } - }; - - /** - * 取消订阅 - * @param subscriptionId 订阅 id - */ - const unsubscribe = (subscriptionId: string) => { - const subscription = subscriptions.get(subscriptionId); - if (subscription) { - subscription.unsubscribe(); - subscriptions.delete(subscriptionId); - console.log(`已取消订阅: ${subscriptionId}`); - } - }; - - /** - * 断开WebSocket连接 - */ - const disconnect = () => { - // 设置手动断开标志 - isManualDisconnect = true; - - // 清除所有计时器 - if (reconnectTimer) { - clearTimeout(reconnectTimer); - reconnectTimer = null; - } - - if (connectionTimeoutTimer) { - clearTimeout(connectionTimeoutTimer); - connectionTimeoutTimer = null; - } - - // 清除所有订阅 - for (const [id, subscription] of subscriptions.entries()) { - try { - subscription.unsubscribe(); - } catch (error) { - console.warn(`取消订阅 ${id} 时出错:`, error); - } - } - subscriptions.clear(); - - // 断开连接 - if (client.value) { - try { - if (client.value.connected || client.value.active) { - client.value.deactivate(); - console.log("WebSocket连接已主动断开"); - } - } catch (error) { - console.error("断开WebSocket连接时出错:", error); - } - client.value = null; - } - - isConnected.value = false; - isConnecting = false; - reconnectCount.value = 0; - }; - - return { - isConnected, - connect, - subscribe, - unsubscribe, - disconnect, - }; -} diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue index 564edad..6cffddb 100644 --- a/src/pages/index/index.vue +++ b/src/pages/index/index.vue @@ -2,14 +2,7 @@ - + @@ -20,7 +13,7 @@ v-for="(item, index) in quickNavList" :key="index" use-slot - @itemclick="handleNavClick(item)" + @itemclick="handleNavClickWithGuard(item)" > @@ -100,8 +93,9 @@ import { onReady, onShow } from "@dcloudio/uni-app"; import { dayjs } from "wot-design-uni"; import { useRouter } from "uni-mini-router"; import { useUserStore } from "@/store"; +import { useNavigation } from "@/composables/useNavigation"; import { menuConfig } from "@/config/menu"; -import { checkLogin, isLoggedIn } from "@/utils/auth"; +import { isLoggedIn } from "@/utils/auth"; import { hasPermission } from "@/utils/permission"; import LogAPI, { type VisitOverview as ApiVisitOverview, type VisitTrend } from "@/api/log"; import NoticeAPI, { type NoticeItem } from "@/api/notice"; @@ -117,6 +111,7 @@ interface NavItem { const router = useRouter(); const userStore = useUserStore(); +const { handleNavClick } = useNavigation(); // custom-navbar 组件内部已处理导航栏高度与胶囊避让 const current = ref(0); @@ -136,17 +131,12 @@ const visitOverviewData = ref({ totalPvCount: 0, }); -const appVersion = ref(""); - const noticeList = ref([]); const noticeText = computed(() => { - if (!noticeList.value.length) { - return "暂无通知"; - } const titles = noticeList.value .map((n: NoticeItem) => n.title) .filter(Boolean) - .slice(0, 2) as string[]; + .slice(0, 2); return titles.length ? titles.join(" ") : "暂无通知"; }); @@ -214,17 +204,6 @@ const chartOpts = ref({ }, }); -function loadAppVersion() { - try { - const p: any = (globalThis as any).plus; - if (p?.runtime?.version) { - appVersion.value = `v${p.runtime.version}`; - } - } catch { - appVersion.value = ""; - } -} - async function loadNoticeData() { // 未登录时不调用通知接口 if (!isLogged.value) { @@ -269,49 +248,24 @@ async function loadVisitTrendData() { } } -function handleNavClick(item: NavItem) { - // 未登录 / 无权限时:展示默认导航,但点击统一跳登录 +function handleNavClickWithGuard(item: NavItem) { if (!isLogged.value || !hasAnyPerm.value) { uni.navigateTo({ url: "/pages/login/index" }); return; } - // 已登录但访问受限时,仍做一次登录校验(防 token 过期) - if (!checkLogin()) return; - - // 外部链接处理 - if (item.url.startsWith("http://") || item.url.startsWith("https://")) { - const isH5 = typeof window !== "undefined"; - if (isH5) { - uni.navigateTo({ url: `/pages/webview/index?url=${encodeURIComponent(item.url)}` }); - } else { - try { - const p = (globalThis as any).plus; - p?.runtime?.openURL(item.url); - } catch { - uni.navigateTo({ url: `/pages/webview/index?url=${encodeURIComponent(item.url)}` }); - } - } - return; - } - - router.push({ path: item.url }); + handleNavClick(item); } function handleNoticeClick() { router.push({ path: "/pages/work/notice/index" }); } -function handleSwiperClick(_e: any) {} - -function handleSwiperChange(_e: any) {} - function handleDataRangeChange({ value }: { value: number }) { recentDaysRange.value = value; loadVisitTrendData(); } onReady(() => { - loadAppVersion(); loadNoticeData(); loadVisitOverviewData(); loadVisitTrendData(); @@ -436,7 +390,6 @@ onShow(() => { &__header { position: relative; - z-index: var(--z-sticky); display: flex; align-items: center; justify-content: space-between; @@ -478,7 +431,6 @@ onShow(() => { &__num { position: relative; - z-index: var(--z-sticky); font-size: 48rpx; font-weight: 700; line-height: 1; diff --git a/src/pages/login/index.vue b/src/pages/login/index.vue index f148cc6..c4d71f8 100644 --- a/src/pages/login/index.vue +++ b/src/pages/login/index.vue @@ -105,7 +105,7 @@ @@ -228,7 +228,7 @@ - + 确认绑定 @@ -260,9 +260,10 @@