mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 14:06:28 +00:00
feat: improve error handling and code quality
后端改进: - 添加统一异常处理系统 (exceptions.py, response.py) - 实现自定义异常类 (ValidationError, AuthorizationError, ResourceNotFoundError, BusinessLogicError) - 配置全局异常处理器,统一 API 错误响应格式 - 迁移业务逻辑错误到自定义异常 (users.py, auth.py) - 添加 SQL LIKE 通配符转义,防止通配符滥用 - 使用 EmailStr 进行邮箱格式验证 - 移除敏感字段暴露 (jwt_sub) 前端改进: - 配置 ESLint 9 (flat config) 和 Prettier - 修复所有 ESLint 错误和警告 - 移除未使用的变量和导入 - 为组件添加 PropTypes 默认值 - 统一代码格式和风格
This commit is contained in:
@@ -13,12 +13,12 @@
|
||||
* }
|
||||
*/
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
export function useAsyncAction(options = {}) {
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
/**
|
||||
* 执行异步操作
|
||||
@@ -35,50 +35,50 @@ export function useAsyncAction(options = {}) {
|
||||
successMsg = options.successMsg,
|
||||
errorMsg = options.errorMsg,
|
||||
throwOnError = false,
|
||||
silent = false
|
||||
} = config
|
||||
silent = false,
|
||||
} = config;
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const result = await asyncFn()
|
||||
const result = await asyncFn();
|
||||
|
||||
if (!silent && successMsg) {
|
||||
message.success(successMsg)
|
||||
message.success(successMsg);
|
||||
}
|
||||
|
||||
return result
|
||||
return result;
|
||||
} catch (err) {
|
||||
error.value = err
|
||||
error.value = err;
|
||||
|
||||
if (!silent) {
|
||||
const msg = err.message || err.detail || errorMsg || '操作失败'
|
||||
message.error(msg)
|
||||
const msg = err.message || err.detail || errorMsg || '操作失败';
|
||||
message.error(msg);
|
||||
}
|
||||
|
||||
if (throwOnError) {
|
||||
throw err
|
||||
throw err;
|
||||
}
|
||||
|
||||
return null
|
||||
return null;
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置状态
|
||||
*/
|
||||
const reset = () => {
|
||||
loading.value = false
|
||||
error.value = null
|
||||
}
|
||||
loading.value = false;
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
execute,
|
||||
reset
|
||||
}
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
/**
|
||||
* 响应式断点检测 Composable
|
||||
@@ -11,42 +11,42 @@ import { ref, onMounted, onUnmounted } from 'vue'
|
||||
* - xxl: ≥1600px (超大屏)
|
||||
*/
|
||||
export function useBreakpoint() {
|
||||
const isMobile = ref(window.innerWidth < 768)
|
||||
const isTablet = ref(window.innerWidth >= 768 && window.innerWidth < 992)
|
||||
const isDesktop = ref(window.innerWidth >= 992)
|
||||
const isMobile = ref(window.innerWidth < 768);
|
||||
const isTablet = ref(window.innerWidth >= 768 && window.innerWidth < 992);
|
||||
const isDesktop = ref(window.innerWidth >= 992);
|
||||
|
||||
// Ant Design 断点
|
||||
const isXs = ref(window.innerWidth < 576)
|
||||
const isSm = ref(window.innerWidth >= 576 && window.innerWidth < 768)
|
||||
const isMd = ref(window.innerWidth >= 768 && window.innerWidth < 992)
|
||||
const isLg = ref(window.innerWidth >= 992 && window.innerWidth < 1200)
|
||||
const isXl = ref(window.innerWidth >= 1200 && window.innerWidth < 1600)
|
||||
const isXxl = ref(window.innerWidth >= 1600)
|
||||
const isXs = ref(window.innerWidth < 576);
|
||||
const isSm = ref(window.innerWidth >= 576 && window.innerWidth < 768);
|
||||
const isMd = ref(window.innerWidth >= 768 && window.innerWidth < 992);
|
||||
const isLg = ref(window.innerWidth >= 992 && window.innerWidth < 1200);
|
||||
const isXl = ref(window.innerWidth >= 1200 && window.innerWidth < 1600);
|
||||
const isXxl = ref(window.innerWidth >= 1600);
|
||||
|
||||
const updateBreakpoints = () => {
|
||||
const width = window.innerWidth
|
||||
const width = window.innerWidth;
|
||||
|
||||
// 简化断点
|
||||
isMobile.value = width < 768
|
||||
isTablet.value = width >= 768 && width < 992
|
||||
isDesktop.value = width >= 992
|
||||
isMobile.value = width < 768;
|
||||
isTablet.value = width >= 768 && width < 992;
|
||||
isDesktop.value = width >= 992;
|
||||
|
||||
// Ant Design 断点
|
||||
isXs.value = width < 576
|
||||
isSm.value = width >= 576 && width < 768
|
||||
isMd.value = width >= 768 && width < 992
|
||||
isLg.value = width >= 992 && width < 1200
|
||||
isXl.value = width >= 1200 && width < 1600
|
||||
isXxl.value = width >= 1600
|
||||
}
|
||||
isXs.value = width < 576;
|
||||
isSm.value = width >= 576 && width < 768;
|
||||
isMd.value = width >= 768 && width < 992;
|
||||
isLg.value = width >= 992 && width < 1200;
|
||||
isXl.value = width >= 1200 && width < 1600;
|
||||
isXxl.value = width >= 1600;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', updateBreakpoints)
|
||||
})
|
||||
window.addEventListener('resize', updateBreakpoints);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateBreakpoints)
|
||||
})
|
||||
window.removeEventListener('resize', updateBreakpoints);
|
||||
});
|
||||
|
||||
return {
|
||||
// 简化断点(常用)
|
||||
@@ -61,5 +61,5 @@ export function useBreakpoint() {
|
||||
isLg,
|
||||
isXl,
|
||||
isXxl,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,19 +26,19 @@
|
||||
* )
|
||||
*/
|
||||
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { ref, onUnmounted } from 'vue';
|
||||
|
||||
export function usePollStatus(options = {}) {
|
||||
const {
|
||||
interval = 2000, // 初始轮询间隔(毫秒)
|
||||
maxRetries = 15, // 最大重试次数
|
||||
backoff = false, // 是否使用指数退避
|
||||
maxBackoffInterval = 10000 // 最大退避间隔(毫秒)
|
||||
} = options
|
||||
interval = 2000, // 初始轮询间隔(毫秒)
|
||||
maxRetries = 15, // 最大重试次数
|
||||
backoff = false, // 是否使用指数退避
|
||||
maxBackoffInterval = 10000, // 最大退避间隔(毫秒)
|
||||
} = options;
|
||||
|
||||
const polling = ref(false)
|
||||
let pollTimer = null
|
||||
let retryCount = 0
|
||||
const polling = ref(false);
|
||||
let pollTimer = null;
|
||||
let retryCount = 0;
|
||||
|
||||
/**
|
||||
* 开始轮询
|
||||
@@ -49,80 +49,76 @@ export function usePollStatus(options = {}) {
|
||||
* @param {Function} callbacks.onTimeout - 超时回调
|
||||
*/
|
||||
const startPolling = async (checkFn, callbacks = {}) => {
|
||||
const { onSuccess, onFailure, onTimeout } = callbacks
|
||||
const { onSuccess, onFailure, onTimeout } = callbacks;
|
||||
|
||||
// 重置状态
|
||||
stopPolling()
|
||||
polling.value = true
|
||||
retryCount = 0
|
||||
stopPolling();
|
||||
polling.value = true;
|
||||
retryCount = 0;
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const result = await checkFn()
|
||||
const result = await checkFn();
|
||||
|
||||
// 检查是否完成
|
||||
if (result.completed) {
|
||||
stopPolling()
|
||||
stopPolling();
|
||||
|
||||
if (result.success) {
|
||||
onSuccess?.(result.data || result)
|
||||
onSuccess?.(result.data || result);
|
||||
} else {
|
||||
onFailure?.(result.data || result)
|
||||
onFailure?.(result.data || result);
|
||||
}
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否超时
|
||||
retryCount++
|
||||
retryCount++;
|
||||
if (retryCount >= maxRetries) {
|
||||
stopPolling()
|
||||
onTimeout?.()
|
||||
return
|
||||
stopPolling();
|
||||
onTimeout?.();
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算下次轮询间隔(支持指数退避)
|
||||
let nextInterval = interval
|
||||
let nextInterval = interval;
|
||||
if (backoff) {
|
||||
// 指数退避:2s -> 4s -> 8s -> 最大10s
|
||||
nextInterval = Math.min(
|
||||
interval * Math.pow(2, retryCount - 1),
|
||||
maxBackoffInterval
|
||||
)
|
||||
nextInterval = Math.min(interval * Math.pow(2, retryCount - 1), maxBackoffInterval);
|
||||
}
|
||||
|
||||
// 继续轮询
|
||||
pollTimer = setTimeout(poll, nextInterval)
|
||||
|
||||
pollTimer = setTimeout(poll, nextInterval);
|
||||
} catch (error) {
|
||||
stopPolling()
|
||||
onFailure?.(error)
|
||||
stopPolling();
|
||||
onFailure?.(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 立即执行第一次检查
|
||||
poll()
|
||||
}
|
||||
poll();
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止轮询
|
||||
*/
|
||||
const stopPolling = () => {
|
||||
if (pollTimer) {
|
||||
clearTimeout(pollTimer)
|
||||
pollTimer = null
|
||||
clearTimeout(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
polling.value = false
|
||||
retryCount = 0
|
||||
}
|
||||
polling.value = false;
|
||||
retryCount = 0;
|
||||
};
|
||||
|
||||
// 组件卸载时自动清理
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
})
|
||||
stopPolling();
|
||||
});
|
||||
|
||||
return {
|
||||
polling,
|
||||
startPolling,
|
||||
stopPolling
|
||||
}
|
||||
stopPolling,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const THEME_STORAGE_KEY = 'checkin-app-theme'
|
||||
const THEME_STORAGE_KEY = 'checkin-app-theme';
|
||||
|
||||
// 全局主题状态(单例模式)
|
||||
const theme = ref('light')
|
||||
const theme = ref('light');
|
||||
|
||||
/**
|
||||
* 应用主题到 DOM
|
||||
*/
|
||||
const applyTheme = (newTheme) => {
|
||||
const html = document.documentElement
|
||||
const applyTheme = newTheme => {
|
||||
const html = document.documentElement;
|
||||
|
||||
if (newTheme === 'dark') {
|
||||
html.classList.add('dark')
|
||||
html.classList.add('dark');
|
||||
} else {
|
||||
html.classList.remove('dark')
|
||||
html.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化主题
|
||||
@@ -24,48 +24,48 @@ const applyTheme = (newTheme) => {
|
||||
*/
|
||||
export const initTheme = () => {
|
||||
// 1. 尝试从 localStorage 读取
|
||||
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY)
|
||||
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (savedTheme === 'light' || savedTheme === 'dark') {
|
||||
theme.value = savedTheme
|
||||
applyTheme(savedTheme)
|
||||
return
|
||||
theme.value = savedTheme;
|
||||
applyTheme(savedTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 检测系统偏好
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
theme.value = 'dark'
|
||||
applyTheme('dark')
|
||||
return
|
||||
theme.value = 'dark';
|
||||
applyTheme('dark');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 默认亮色
|
||||
theme.value = 'light'
|
||||
applyTheme('light')
|
||||
}
|
||||
theme.value = 'light';
|
||||
applyTheme('light');
|
||||
};
|
||||
|
||||
/**
|
||||
* 监听系统主题变化
|
||||
*/
|
||||
export const watchSystemTheme = () => {
|
||||
if (!window.matchMedia) return
|
||||
if (!window.matchMedia) return;
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const handleChange = (e) => {
|
||||
const handleChange = e => {
|
||||
// 仅在用户未手动设置主题时才跟随系统
|
||||
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY)
|
||||
const savedTheme = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (!savedTheme) {
|
||||
const systemTheme = e.matches ? 'dark' : 'light'
|
||||
theme.value = systemTheme
|
||||
applyTheme(systemTheme)
|
||||
const systemTheme = e.matches ? 'dark' : 'light';
|
||||
theme.value = systemTheme;
|
||||
applyTheme(systemTheme);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange)
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
|
||||
// 返回清理函数
|
||||
return () => mediaQuery.removeEventListener('change', handleChange)
|
||||
}
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
};
|
||||
|
||||
/**
|
||||
* 主题管理 Composable
|
||||
@@ -76,31 +76,31 @@ export function useTheme() {
|
||||
* 切换主题
|
||||
*/
|
||||
const toggleTheme = () => {
|
||||
const newTheme = theme.value === 'light' ? 'dark' : 'light'
|
||||
theme.value = newTheme
|
||||
applyTheme(newTheme)
|
||||
localStorage.setItem(THEME_STORAGE_KEY, newTheme)
|
||||
}
|
||||
const newTheme = theme.value === 'light' ? 'dark' : 'light';
|
||||
theme.value = newTheme;
|
||||
applyTheme(newTheme);
|
||||
localStorage.setItem(THEME_STORAGE_KEY, newTheme);
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置指定主题
|
||||
*/
|
||||
const setTheme = (newTheme) => {
|
||||
const setTheme = newTheme => {
|
||||
if (newTheme !== 'light' && newTheme !== 'dark') {
|
||||
console.warn(`Invalid theme: ${newTheme}. Using 'light' instead.`)
|
||||
newTheme = 'light'
|
||||
console.warn(`Invalid theme: ${newTheme}. Using 'light' instead.`);
|
||||
newTheme = 'light';
|
||||
}
|
||||
|
||||
theme.value = newTheme
|
||||
applyTheme(newTheme)
|
||||
localStorage.setItem(THEME_STORAGE_KEY, newTheme)
|
||||
}
|
||||
theme.value = newTheme;
|
||||
applyTheme(newTheme);
|
||||
localStorage.setItem(THEME_STORAGE_KEY, newTheme);
|
||||
};
|
||||
|
||||
return {
|
||||
theme,
|
||||
toggleTheme,
|
||||
setTheme,
|
||||
isDark: computed(() => theme.value === 'dark'),
|
||||
isLight: computed(() => theme.value === 'light')
|
||||
}
|
||||
isLight: computed(() => theme.value === 'light'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
/**
|
||||
* Token 过期监控 Composable
|
||||
@@ -16,49 +16,49 @@ import { useRouter } from 'vue-router'
|
||||
*/
|
||||
|
||||
// 全局单例:确保整个应用只有一个监控实例
|
||||
let monitorTimer = null
|
||||
let warningShown = false
|
||||
let isMonitoring = false // 新增:防止重复启动
|
||||
let monitorTimer = null;
|
||||
let warningShown = false;
|
||||
let isMonitoring = false; // 新增:防止重复启动
|
||||
|
||||
// 检查间隔(毫秒)
|
||||
const NORMAL_CHECK_INTERVAL = 15 * 60 * 1000 // 正常情况:15 分钟
|
||||
const URGENT_CHECK_INTERVAL = 5 * 60 * 1000 // Token 即将过期:5 分钟
|
||||
const NORMAL_CHECK_INTERVAL = 15 * 60 * 1000; // 正常情况:15 分钟
|
||||
const URGENT_CHECK_INTERVAL = 5 * 60 * 1000; // Token 即将过期:5 分钟
|
||||
|
||||
export function useTokenMonitor() {
|
||||
const authStore = useAuthStore()
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
const tokenStatus = computed(() => userStore.tokenStatus)
|
||||
const hasPassword = computed(() => authStore.user?.has_password || false)
|
||||
const tokenStatus = computed(() => userStore.tokenStatus);
|
||||
const hasPassword = computed(() => authStore.user?.has_password || false);
|
||||
|
||||
// 计算 Token 剩余分钟数
|
||||
const getRemainingMinutes = () => {
|
||||
if (!tokenStatus.value?.expires_at) return null
|
||||
if (!tokenStatus.value?.expires_at) return null;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const expiresAt = tokenStatus.value.expires_at
|
||||
const diffSeconds = expiresAt - now
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const expiresAt = tokenStatus.value.expires_at;
|
||||
const diffSeconds = expiresAt - now;
|
||||
|
||||
return Math.floor(diffSeconds / 60)
|
||||
}
|
||||
return Math.floor(diffSeconds / 60);
|
||||
};
|
||||
|
||||
// 检查 Token 状态并显示提醒
|
||||
const checkTokenStatus = async () => {
|
||||
// 如果未登录,不检查
|
||||
if (!authStore.isAuthenticated) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取最新的 Token 状态
|
||||
await userStore.fetchTokenStatus()
|
||||
await userStore.fetchTokenStatus();
|
||||
|
||||
const remainingMinutes = getRemainingMinutes()
|
||||
const remainingMinutes = getRemainingMinutes();
|
||||
|
||||
// Token 已过期(负数分钟)
|
||||
if (remainingMinutes !== null && remainingMinutes < 0) {
|
||||
const expiredMinutes = Math.abs(remainingMinutes)
|
||||
const expiredMinutes = Math.abs(remainingMinutes);
|
||||
|
||||
// Token 过期后 5 分钟内提醒
|
||||
if (expiredMinutes <= 5) {
|
||||
@@ -69,8 +69,8 @@ export function useTokenMonitor() {
|
||||
content: `您的登录凭证已过期 ${expiredMinutes} 分钟,部分功能可能受限。建议您扫码刷新凭证。`,
|
||||
duration: 8,
|
||||
key: 'token-expired-warning',
|
||||
})
|
||||
warningShown = true
|
||||
});
|
||||
warningShown = true;
|
||||
}
|
||||
} else {
|
||||
// 没有密码的用户:必须重新登录
|
||||
@@ -78,18 +78,18 @@ export function useTokenMonitor() {
|
||||
content: '您的登录凭证已过期,请重新扫码登录',
|
||||
duration: 5,
|
||||
key: 'token-expired-error',
|
||||
})
|
||||
});
|
||||
|
||||
// 清除登录状态并跳转
|
||||
authStore.logout()
|
||||
router.push('/login')
|
||||
authStore.logout();
|
||||
router.push('/login');
|
||||
}
|
||||
} else if (expiredMinutes > 5) {
|
||||
// 过期超过 5 分钟
|
||||
if (!hasPassword.value) {
|
||||
// 没有密码的用户:强制退出
|
||||
authStore.logout()
|
||||
router.push('/login')
|
||||
authStore.logout();
|
||||
router.push('/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,82 +100,81 @@ export function useTokenMonitor() {
|
||||
content: `您的登录凭证将在 ${remainingMinutes} 分钟后过期,建议您提前刷新`,
|
||||
duration: 6,
|
||||
key: 'token-expiring-warning',
|
||||
})
|
||||
warningShown = true
|
||||
});
|
||||
warningShown = true;
|
||||
}
|
||||
|
||||
// Token 即将过期时,切换到更频繁的检查(5 分钟)
|
||||
adjustCheckInterval(URGENT_CHECK_INTERVAL)
|
||||
adjustCheckInterval(URGENT_CHECK_INTERVAL);
|
||||
}
|
||||
// Token 状态正常
|
||||
else if (remainingMinutes !== null && remainingMinutes > 60) {
|
||||
// 重置警告标志
|
||||
warningShown = false
|
||||
warningShown = false;
|
||||
|
||||
// 恢复正常检查频率(15 分钟)
|
||||
adjustCheckInterval(NORMAL_CHECK_INTERVAL)
|
||||
adjustCheckInterval(NORMAL_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('检查 Token 状态失败:', error)
|
||||
console.error('检查 Token 状态失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 调整检查间隔
|
||||
const adjustCheckInterval = (newInterval) => {
|
||||
const adjustCheckInterval = newInterval => {
|
||||
if (monitorTimer) {
|
||||
const currentInterval = monitorTimer._idleTimeout || 0
|
||||
const currentInterval = monitorTimer._idleTimeout || 0;
|
||||
|
||||
// 只有当新间隔与当前间隔不同时才重启定时器
|
||||
if (currentInterval !== newInterval) {
|
||||
clearInterval(monitorTimer)
|
||||
clearInterval(monitorTimer);
|
||||
monitorTimer = setInterval(() => {
|
||||
checkTokenStatus()
|
||||
}, newInterval)
|
||||
checkTokenStatus();
|
||||
}, newInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 启动监控
|
||||
const startMonitoring = () => {
|
||||
// 避免重复启动(单例模式)
|
||||
if (isMonitoring || monitorTimer) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
isMonitoring = true
|
||||
isMonitoring = true;
|
||||
|
||||
// 立即检查一次
|
||||
checkTokenStatus()
|
||||
checkTokenStatus();
|
||||
|
||||
// 默认使用正常检查频率(15 分钟)
|
||||
monitorTimer = setInterval(() => {
|
||||
checkTokenStatus()
|
||||
}, NORMAL_CHECK_INTERVAL)
|
||||
}
|
||||
checkTokenStatus();
|
||||
}, NORMAL_CHECK_INTERVAL);
|
||||
};
|
||||
|
||||
// 停止监控
|
||||
const stopMonitoring = () => {
|
||||
if (monitorTimer) {
|
||||
clearInterval(monitorTimer)
|
||||
monitorTimer = null
|
||||
clearInterval(monitorTimer);
|
||||
monitorTimer = null;
|
||||
}
|
||||
isMonitoring = false
|
||||
warningShown = false
|
||||
}
|
||||
isMonitoring = false;
|
||||
warningShown = false;
|
||||
};
|
||||
|
||||
// 手动触发检查
|
||||
const checkNow = () => {
|
||||
warningShown = false // 重置警告标志,允许再次显示
|
||||
checkTokenStatus()
|
||||
}
|
||||
warningShown = false; // 重置警告标志,允许再次显示
|
||||
checkTokenStatus();
|
||||
};
|
||||
|
||||
// 组件挂载时启动监控
|
||||
onMounted(() => {
|
||||
if (authStore.isAuthenticated) {
|
||||
startMonitoring()
|
||||
startMonitoring();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// 组件卸载时不停止监控(因为是全局单例)
|
||||
// onUnmounted 中不调用 stopMonitoring(),让监控持续运行
|
||||
@@ -187,5 +186,5 @@ export function useTokenMonitor() {
|
||||
stopMonitoring,
|
||||
checkNow,
|
||||
getRemainingMinutes,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user