// ============================================================================= // IT智能服务台 — Portal 扫码登录 Composable (Phase 1.3, task #16) // ============================================================================= // 说明:跟 frontend-agent/src/composables/useQrcodeLogin.ts 同款逻辑 // Portal 端的 onSuccess 由调用方提供,通常实现"按角色跳对应端" // ============================================================================= import { ref, onUnmounted, type Ref } from 'vue' import { ElMessage } from 'element-plus' import { createQrcode, pollQrcode } from '@/api/qrcode' import type { QrcodePollStatus } from '@/api/qrcode' const POLL_INTERVAL_MS = 2000 const COUNTDOWN_TICK_MS = 1000 export interface UseQrcodeLoginOptions { /** 登录成功回调(token, employeeId, roles)— Portal 一般这里按角色跳对应端 */ onSuccess: (token: string, employeeId: string, roles: string[]) => void onError?: (message: string) => void } export interface UseQrcodeLoginReturn { qrcodePngBase64: Ref qrcodeUrl: Ref countdown: Ref status: Ref scannedBy: Ref loading: Ref errorMessage: Ref startLogin: () => Promise refreshQrcode: () => Promise stopPolling: () => void } export function useQrcodeLogin(options: UseQrcodeLoginOptions): UseQrcodeLoginReturn { const qrcodePngBase64 = ref(null) const qrcodeUrl = ref(null) const countdown = ref(0) const status = ref('waiting') const scannedBy = ref(null) const loading = ref(false) const errorMessage = ref(null) let ticket: string | null = null let pollTimer: ReturnType | null = null let countdownTimer: ReturnType | null = null let expiresAt: number | null = null function clearTimers(): void { if (pollTimer) { clearInterval(pollTimer) pollTimer = null } if (countdownTimer) { clearInterval(countdownTimer) countdownTimer = null } } function startCountdown(): void { if (countdownTimer) clearInterval(countdownTimer) countdownTimer = setInterval(() => { if (!expiresAt) { countdown.value = 0 return } const remaining = Math.max(0, Math.floor((expiresAt - Date.now()) / 1000)) countdown.value = remaining if (remaining === 0 && status.value === 'waiting') { status.value = 'expired' clearTimers() } }, COUNTDOWN_TICK_MS) } function startPolling(): void { if (pollTimer) clearInterval(pollTimer) pollTimer = setInterval(async () => { if (!ticket) return try { const data = await pollQrcode(ticket) status.value = data.status scannedBy.value = data.name || null if (data.status === 'confirmed' && data.token && data.employee_id) { clearTimers() const roles = data.roles || ['user'] options.onSuccess(data.token, data.employee_id, roles) } else if (data.status === 'expired') { clearTimers() } } catch (err: any) { console.warn('[useQrcodeLogin] poll error:', err) } }, POLL_INTERVAL_MS) } function stopPolling(): void { clearTimers() } async function startLogin(): Promise { if (loading.value) return loading.value = true errorMessage.value = null clearTimers() try { const data = await createQrcode() ticket = data.ticket qrcodeUrl.value = data.qrcode_url qrcodePngBase64.value = data.qrcode_png_base64 || null countdown.value = data.expires_in expiresAt = Date.now() + data.expires_in * 1000 status.value = 'waiting' scannedBy.value = null startCountdown() startPolling() } catch (err: any) { const msg = err?.message || '生成二维码失败' errorMessage.value = msg if (options.onError) { options.onError(msg) } else { ElMessage.error(msg) } } finally { loading.value = false } } async function refreshQrcode(): Promise { await startLogin() } onUnmounted(() => { clearTimers() }) return { qrcodePngBase64, qrcodeUrl, countdown, status, scannedBy, loading, errorMessage, startLogin, refreshQrcode, stopPolling, } }