From bc0d6fc8aa342d5e06abaf7f8341eaf2cbde56e1 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 13 Apr 2026 21:48:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86qq=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/routes/claims.js | 20 ++ .../services/claim/claim-session-service.js | 203 +++++++++++++++--- apps/backend/src/services/session/session.js | 6 + .../components/tencent/TencentAuthCard.vue | 97 +++++++-- apps/frontend/src/composables/useClaimPage.ts | 162 ++++++++++++-- .../useTencentBrowserSessionPage.ts | 1 + apps/frontend/src/services/claim.ts | 21 +- apps/frontend/src/types/tencent/session.ts | 2 + apps/frontend/src/views/claim/ClaimView.vue | 6 +- .../src/views/tx/TencentBrowserView.vue | 2 + 10 files changed, 453 insertions(+), 67 deletions(-) diff --git a/apps/backend/src/routes/claims.js b/apps/backend/src/routes/claims.js index d7dbd627..15353574 100644 --- a/apps/backend/src/routes/claims.js +++ b/apps/backend/src/routes/claims.js @@ -1,11 +1,13 @@ import { Router } from 'express' import { + closeClaimSession, confirmClaimRole, createClaimSession, getClaimDetail, getClaimScreenshotPath, getClaimSessionSummary, + reloadClaimSession, redeemClaimTask, } from '../services/claim/claim-session-service.js' import { buildNotFoundPayload, buildSuccessPayload, sendRouteError } from '../utils/http.js' @@ -39,6 +41,24 @@ router.get('/:token/session/summary', async (req, res) => { } }) +router.post('/:token/session/refresh', async (req, res) => { + try { + const data = await reloadClaimSession(req.params.token) + res.json(buildSuccessPayload(data, data.session ? (data.session.notice || '后端页面已刷新') : '领取会话已重置')) + } catch (error) { + sendRouteError(res, error, '刷新领取会话失败', '[claims/:token/session/refresh]') + } +}) + +router.delete('/:token/session', async (req, res) => { + try { + const data = await closeClaimSession(req.params.token) + res.json(buildSuccessPayload(data, '领取会话已关闭')) + } catch (error) { + sendRouteError(res, error, '关闭领取会话失败', '[claims/:token/session]') + } +}) + router.post('/:token/confirm-role', async (req, res) => { try { const data = await confirmClaimRole(req.params.token) diff --git a/apps/backend/src/services/claim/claim-session-service.js b/apps/backend/src/services/claim/claim-session-service.js index 79149c28..49e2ab10 100644 --- a/apps/backend/src/services/claim/claim-session-service.js +++ b/apps/backend/src/services/claim/claim-session-service.js @@ -1,8 +1,10 @@ import { + closeTencentBrowserSession, createTencentBrowserSession, getTencentBrowserSession, getTencentBrowserSessionSummary, getTencentBrowserSessionScreenshotPath, + reloadTencentBrowserSession, redeemTencentBrowserSession, } from '../session/session.js' import { findClaimTokenByToken, getClaimTokenById, updateClaimToken } from '../../repositories/claim-token-repo.js' @@ -24,8 +26,8 @@ const CLAIM_TERMINAL_STATUSES = new Set(['expired', 'closed']) export async function getClaimDetail(token, { includeQrImage = true } = {}) { const context = await getClaimContext(token) - const session = await loadTaskSession(context.task, { includeQrImage }) - const syncedTask = session ? await syncTaskWithSession(context.task, session) : context.task + const { task, session } = await loadTaskSession(context.task, { includeQrImage }) + const syncedTask = session ? await syncTaskWithSession(task, session) : task return buildClaimDetailPayload({ claimToken: context.claimToken, @@ -39,30 +41,54 @@ export async function getClaimDetail(token, { includeQrImage = true } = {}) { export async function createClaimSession(token, payload = {}) { const context = await getClaimContext(token) assertTaskCanProceed(context.task) + const requestedLoginType = normalizeClaimLoginType(payload.loginType) + const forceRecreate = Boolean(payload.forceRecreate) + let task = context.task - if (context.task.browser_session_id) { - const existingSession = await getTencentBrowserSession(context.task.browser_session_id) - const syncedTask = await syncTaskWithSession(context.task, existingSession) + if (task.browser_session_id) { + const existing = await loadTaskSession(task, { includeQrImage: true }) + task = existing.task - return buildClaimDetailPayload({ - claimToken: context.claimToken, - task: syncedTask, - order: context.order, - orderItem: context.orderItem, - session: existingSession, - }) + if (existing.session) { + const existingLoginType = normalizeClaimLoginType(existing.session.loginType || task.login_type) + + if (!forceRecreate && existingLoginType === requestedLoginType) { + const syncedTask = await syncTaskWithSession(task, existing.session) + + return buildClaimDetailPayload({ + claimToken: context.claimToken, + task: syncedTask, + order: context.order, + orderItem: context.orderItem, + session: existing.session, + }) + } + + try { + await closeTencentBrowserSession(existing.session.sessionId) + } catch (error) { + if (!isRecoverableSessionError(error)) { + throw error + } + } + + task = await clearTaskSession(task, { + lastError: '', + }) + } } const session = await createTencentBrowserSession({ - loginType: payload.loginType, + loginType: requestedLoginType, }) - const updatedTask = await updateTask(context.task.id, { + const updatedTask = await updateTask(task.id, { task_status: 'claimed', browser_session_id: session.sessionId, login_type: session.loginType, user_action_status: 'claimed', - claimed_at: context.task.claimed_at || nowIso(), + claimed_at: task.claimed_at || nowIso(), updated_at: nowIso(), + last_error: '', }) return buildClaimDetailPayload({ @@ -76,16 +102,21 @@ export async function createClaimSession(token, payload = {}) { export async function getClaimSessionSummary(token) { const context = await getClaimContext(token) + const { task, session } = await loadTaskSession(context.task, { + includeQrImage: false, + }) - if (!context.task.browser_session_id) { - throw createHttpError('当前任务还没有创建浏览器会话', { - statusCode: 409, - errorCode: 'claim_session_not_created', + if (!session) { + return buildClaimDetailPayload({ + claimToken: context.claimToken, + task, + order: context.order, + orderItem: context.orderItem, + session: null, }) } - const session = await getTencentBrowserSession(context.task.browser_session_id) - const syncedTask = await syncTaskWithSession(context.task, session) + const syncedTask = await syncTaskWithSession(task, session) return buildClaimDetailPayload({ claimToken: context.claimToken, @@ -96,6 +127,72 @@ export async function getClaimSessionSummary(token) { }) } +export async function reloadClaimSession(token) { + const context = await getClaimContext(token) + assertTaskCanProceed(context.task) + + const active = await loadTaskSession(context.task, { + includeQrImage: true, + }) + + if (!active.session) { + return buildClaimDetailPayload({ + claimToken: context.claimToken, + task: active.task, + order: context.order, + orderItem: context.orderItem, + session: null, + }) + } + + const session = await reloadTencentBrowserSession(active.session.sessionId) + const syncedTask = await syncTaskWithSession(active.task, session) + + return buildClaimDetailPayload({ + claimToken: context.claimToken, + task: syncedTask, + order: context.order, + orderItem: context.orderItem, + session, + }) +} + +export async function closeClaimSession(token) { + const context = await getClaimContext(token) + assertTaskCanProceed(context.task) + let task = context.task + + if (!task.browser_session_id) { + return buildClaimDetailPayload({ + claimToken: context.claimToken, + task, + order: context.order, + orderItem: context.orderItem, + session: null, + }) + } + + try { + await closeTencentBrowserSession(task.browser_session_id) + } catch (error) { + if (!isRecoverableSessionError(error)) { + throw error + } + } + + task = await clearTaskSession(task, { + lastError: '', + }) + + return buildClaimDetailPayload({ + claimToken: context.claimToken, + task, + order: context.order, + orderItem: context.orderItem, + session: null, + }) +} + export async function confirmClaimRole(token) { const context = await getClaimContext(token) assertPublicClaimActionAllowed(context.task, 'confirm') @@ -430,12 +527,35 @@ async function expireClaimContext(claimToken, task) { async function loadTaskSession(task, { includeQrImage = false } = {}) { if (!task.browser_session_id) { - return null + return { + task, + session: null, + } } - return includeQrImage - ? getTencentBrowserSession(task.browser_session_id) - : getTencentBrowserSessionSummary(task.browser_session_id) + try { + const session = includeQrImage + ? await getTencentBrowserSession(task.browser_session_id) + : await getTencentBrowserSessionSummary(task.browser_session_id) + + return { + task, + session, + } + } catch (error) { + if (!isRecoverableSessionError(error)) { + throw error + } + + const nextTask = await clearTaskSession(task, { + lastError: '浏览器会话已失效,请重新初始化登录', + }) + + return { + task: nextTask, + session: null, + } + } } async function syncTaskWithSession(task, session) { @@ -550,3 +670,36 @@ function parseTaskState(task) { function isAssistedClaimTask(task) { return String(task?.executor_key || '').trim() === 'tencent_claim_assisted' } + +async function clearTaskSession(task, { lastError = '' } = {}) { + const shouldResetClaimProgress = ['link_generated', 'claimed', 'role_confirmed'].includes(String(task.task_status || '')) + const nextTaskStatus = shouldResetClaimProgress ? 'link_generated' : task.task_status + const nextUserActionStatus = shouldResetClaimProgress ? 'pending_claim' : task.user_action_status + const patch = { + task_status: nextTaskStatus, + user_action_status: nextUserActionStatus, + browser_session_id: '', + login_type: '', + nickname: '', + role_id: '', + role_name: '', + area: '', + partition_name: '', + artifacts_json: '{}', + state_json: '{}', + role_confirmed_at: nextTaskStatus === 'link_generated' ? null : task.role_confirmed_at, + last_error: String(lastError || ''), + updated_at: nowIso(), + } + + return updateTask(task.id, patch) +} + +function normalizeClaimLoginType(loginType) { + return String(loginType || '').trim() === 'wx' ? 'wx' : 'qq' +} + +function isRecoverableSessionError(error) { + const errorCode = String(error?.errorCode || error?.code || '').trim() + return errorCode === 'session_not_found' || errorCode === 'session_closed' +} diff --git a/apps/backend/src/services/session/session.js b/apps/backend/src/services/session/session.js index a02051a3..ac6b18e6 100644 --- a/apps/backend/src/services/session/session.js +++ b/apps/backend/src/services/session/session.js @@ -172,6 +172,7 @@ export async function reloadTencentBrowserSession(sessionId) { session.qrImagePath = '' session.qrUpdatedAt = '' await ensureLoginTab(session.page, session.loginType) + await captureSessionQr(session) } session.notice = credentialReady @@ -247,6 +248,11 @@ export async function refreshTencentBrowserSession(sessionId, { includeQrImage = session.expiresAt = Date.now() + SESSION_TTL_MS if (!session.qrImageBase64 || session.status === 'expired') { + if (session.status === 'expired') { + await reloadActivityPageForPresentation(session.page) + await ensureLoginTab(session.page, session.loginType) + } + await captureSessionQr(session) if (session.status === 'expired') { session.notice = '二维码已刷新,请重新扫码' diff --git a/apps/frontend/src/components/tencent/TencentAuthCard.vue b/apps/frontend/src/components/tencent/TencentAuthCard.vue index 37c731f1..bb6677b7 100644 --- a/apps/frontend/src/components/tencent/TencentAuthCard.vue +++ b/apps/frontend/src/components/tencent/TencentAuthCard.vue @@ -23,6 +23,7 @@ const props = defineProps<{ }>() const emit = defineEmits<{ + closeSession: [] createSession: [loginType: TencentLoginType] openQrPreview: [] qrImageLoad: [event: Event] @@ -110,23 +111,33 @@ const emit = defineEmits<{ {{ props.hasSession ? '重新生成二维码' : props.initButtonLabel }} - - 刷新后端页面 - + +
+ + 刷新后端页面 + + + 关闭当前会话 + +
@@ -334,30 +345,78 @@ const emit = defineEmits<{ .action-row { margin-top: 16px; - display: grid; - grid-template-columns: minmax(0, 1.45fr) minmax(132px, 0.95fr); - gap: 10px; + display: flex; + flex-direction: column; + gap: 12px; } .primary-action :deep(span) { font-weight: 700; + letter-spacing: 0.01em; } .action-row :deep(.el-button) { width: 100%; min-width: 0; margin: 0; + height: 52px; + border-radius: 18px; } .action-row :deep(.el-button > span) { white-space: nowrap; } +.secondary-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + .primary-action, -.secondary-action { +.secondary-action, +.tertiary-action { min-width: 0; } +.action-row :deep(.primary-action.el-button) { + box-shadow: 0 16px 30px rgba(91, 143, 240, 0.24); +} + +.action-row :deep(.primary-action.el-button:not(.is-disabled)) { + background: linear-gradient(135deg, #6da9fb 0%, #4f85eb 100%); + border-color: transparent; +} + +.action-row :deep(.secondary-action.el-button), +.action-row :deep(.tertiary-action.el-button) { + background: rgba(248, 251, 255, 0.98); + border: 1px solid #d9e4f2; + color: #4a5e79; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); +} + +.action-row :deep(.secondary-action.el-button:hover), +.action-row :deep(.secondary-action.el-button:focus-visible) { + border-color: #9dc0f6; + color: #2859b8; + background: #f3f8ff; +} + +.action-row :deep(.tertiary-action.el-button:hover), +.action-row :deep(.tertiary-action.el-button:focus-visible) { + border-color: #efb4b4; + color: #b43c3c; + background: #fff6f6; +} + +.action-row :deep(.secondary-action.el-button.is-disabled), +.action-row :deep(.tertiary-action.el-button.is-disabled) { + background: #f8fafc; + border-color: #e5ebf3; + color: #9aa8bc; +} + @media (max-width: 760px) { .auth-card { padding: 18px; @@ -372,13 +431,7 @@ const emit = defineEmits<{ font-size: 15px; } - .session-strip, - .action-row { - flex-direction: column; - align-items: flex-start; - } - - .action-row { + .secondary-actions { grid-template-columns: 1fr; } diff --git a/apps/frontend/src/composables/useClaimPage.ts b/apps/frontend/src/composables/useClaimPage.ts index 3210522f..7082c320 100644 --- a/apps/frontend/src/composables/useClaimPage.ts +++ b/apps/frontend/src/composables/useClaimPage.ts @@ -6,10 +6,16 @@ import { createClaimSession, fetchClaimDetail, fetchClaimSessionSummary, + refreshClaimSession, + removeClaimSession, redeemClaim, } from '@/services/claim' import type { ClaimDetailData, ClaimTaskStatus } from '@/types/claim' -import type { TencentLoginType } from '@/types/tencent/session' +import type { + TencentBrowserSessionData, + TencentBrowserSessionSummaryData, + TencentLoginType, +} from '@/types/tencent/session' import { notifyTencentActionError } from './tencent/session-errors' @@ -163,7 +169,7 @@ export function useClaimPage(token: string) { ) function applyLoginTypeFromDetail(nextDetail: ClaimDetailData) { - loginType.value = syncLoginTypeFromDetail(nextDetail) + loginType.value = syncLoginTypeFromDetail(nextDetail, loginType.value) } watch( @@ -182,6 +188,10 @@ export function useClaimPage(token: string) { { immediate: true }, ) + watch(qrImage, () => { + qrImageNaturalWidth.value = 0 + }) + async function loadDetail() { detailLoading.value = true @@ -209,7 +219,9 @@ export function useClaimPage(token: string) { try { loginType.value = nextLoginType - const response = await createClaimSession(token, nextLoginType) + const response = await createClaimSession(token, nextLoginType, { + forceRecreate: hasSession.value, + }) if (response.code !== 0) { throw new Error(response.msg || '创建领取会话失败') @@ -225,6 +237,75 @@ export function useClaimPage(token: string) { } } + async function reloadSessionPage() { + if (!hasSession.value) { + return false + } + + sessionLoading.value = true + resetPolling() + resetPollingWarning() + + try { + const response = await refreshClaimSession(token) + + if (response.code !== 0) { + throw new Error(response.msg || '刷新后端页面失败') + } + + detail.value = mergeClaimDetailData(detail.value, response.data) + applyLoginTypeFromDetail(response.data) + restartPollingIfNeeded(response.data) + return true + } catch (error) { + notifyTencentActionError(error, '刷新后端页面失败') + await refreshSessionSummary({ silent: true }) + restartPollingIfNeeded() + return false + } finally { + sessionLoading.value = false + } + } + + async function closeSessionFlow({ silent = false } = {}) { + if (!hasSession.value) { + return false + } + + if (!silent) { + sessionLoading.value = true + } + + resetPolling() + resetPollingWarning() + + try { + const response = await removeClaimSession(token) + + if (response.code !== 0) { + throw new Error(response.msg || '关闭领取会话失败') + } + + detail.value = mergeClaimDetailData(detail.value, response.data) + applyLoginTypeFromDetail(response.data) + + if (!silent) { + showSuccess(response.msg || '领取会话已关闭') + } + + return true + } catch (error) { + if (!silent) { + notifyTencentActionError(error, '关闭领取会话失败') + } + return false + } finally { + if (!silent) { + sessionLoading.value = false + } + } + } + async function refreshSessionSummary({ silent = false } = {}) { if (!hasSession.value) { return false @@ -236,14 +317,28 @@ export function useClaimPage(token: string) { } try { + const currentSession = detail.value?.session || null const response = await fetchClaimSessionSummary(token) if (response.code !== 0) { throw new Error(response.msg || '领取会话状态刷新失败') } - detail.value = mergeClaimDetailData(detail.value, response.data) - applyLoginTypeFromDetail(response.data) + let nextDetail = mergeClaimDetailData(detail.value, response.data) + detail.value = nextDetail + applyLoginTypeFromDetail(nextDetail) + + if (shouldRefreshClaimQrImage(currentSession, response.data)) { + const fullResponse = await fetchClaimDetail(token) + + if (fullResponse.code !== 0) { + throw new Error(fullResponse.msg || '领取二维码刷新失败') + } + + nextDetail = mergeClaimDetailData(nextDetail, fullResponse.data) + detail.value = nextDetail + applyLoginTypeFromDetail(nextDetail) + } if (silent) { resetPollingWarning() @@ -324,11 +419,12 @@ export function useClaimPage(token: string) { return } - loginType.value = nextLoginType - if (hasSession.value) { await createSessionFlow(nextLoginType) + return } + + loginType.value = nextLoginType } function startPolling() { @@ -461,6 +557,8 @@ export function useClaimPage(token: string) { screenshotUrl, showScreenshot, createSessionFlow, + reloadSessionPage, + closeSessionFlow, refreshSessionSummary, switchLoginType, confirmRoleNow, @@ -469,9 +567,17 @@ export function useClaimPage(token: string) { } } -function syncLoginTypeFromDetail(detail: ClaimDetailData) { +function syncLoginTypeFromDetail(detail: ClaimDetailData, fallback: TencentLoginType = DEFAULT_LOGIN_TYPE) { const nextLoginType = String(detail.session?.loginType || detail.task.loginType || '').trim() - return nextLoginType === 'wx' ? 'wx' : 'qq' + if (nextLoginType === 'wx') { + return 'wx' + } + + if (nextLoginType === 'qq') { + return 'qq' + } + + return fallback } function shouldKeepPolling(detail: ClaimDetailData | null) { @@ -516,17 +622,17 @@ function resolveTaskStatusLabel(taskStatus?: string, sessionStatus?: string) { } function mergeClaimDetailData(current: ClaimDetailData | null, next: ClaimDetailData) { - if (!current?.session) { - return next - } - - if (!next.session) { + if (next.session === null) { return { ...next, - session: current.session, + session: null, } } + if (!current?.session) { + return next + } + return { ...next, session: { @@ -542,3 +648,29 @@ function mergeClaimDetailData(current: ClaimDetailData | null, next: ClaimDetail }, } } + +function shouldRefreshClaimQrImage( + currentSession: ClaimDetailData['session'], + nextDetail: ClaimDetailData, +) { + if (!currentSession || !nextDetail.session) { + return false + } + + return shouldRefreshQrImage(currentSession, nextDetail.session) +} + +function shouldRefreshQrImage( + current: TencentBrowserSessionData | TencentBrowserSessionSummaryData, + next: TencentBrowserSessionData | TencentBrowserSessionSummaryData, +) { + if (!next.artifacts?.hasQrImage) { + return false + } + + if (!current.qrImageBase64) { + return true + } + + return Boolean(next.qrUpdatedAt && next.qrUpdatedAt !== current.qrUpdatedAt) +} diff --git a/apps/frontend/src/composables/useTencentBrowserSessionPage.ts b/apps/frontend/src/composables/useTencentBrowserSessionPage.ts index fedaaf43..e4c4bad7 100644 --- a/apps/frontend/src/composables/useTencentBrowserSessionPage.ts +++ b/apps/frontend/src/composables/useTencentBrowserSessionPage.ts @@ -210,6 +210,7 @@ export function useTencentBrowserSessionPage() { return { activityInfo, canRedeem, + teardownSession, createSessionFlow, hasSession, initButtonLabel, diff --git a/apps/frontend/src/services/claim.ts b/apps/frontend/src/services/claim.ts index 5900a4b3..a0cdeeb9 100644 --- a/apps/frontend/src/services/claim.ts +++ b/apps/frontend/src/services/claim.ts @@ -1,4 +1,4 @@ -import { apiGet, apiPost } from '@/lib/http' +import { apiDelete, apiGet, apiPost } from '@/lib/http' import type { ClaimDetailData } from '@/types/claim' import type { TencentLoginType } from '@/types/tencent/session' @@ -6,14 +6,29 @@ export function fetchClaimDetail(token: string) { return apiGet(`/api/v1/claim/${token}`) } -export function createClaimSession(token: string, loginType: TencentLoginType) { - return apiPost(`/api/v1/claim/${token}/session`, { loginType }) +export function createClaimSession( + token: string, + loginType: TencentLoginType, + options: { forceRecreate?: boolean } = {}, +) { + return apiPost(`/api/v1/claim/${token}/session`, { + loginType, + forceRecreate: Boolean(options.forceRecreate), + }) } export function fetchClaimSessionSummary(token: string) { return apiGet(`/api/v1/claim/${token}/session/summary`) } +export function refreshClaimSession(token: string) { + return apiPost(`/api/v1/claim/${token}/session/refresh`, {}) +} + +export function removeClaimSession(token: string) { + return apiDelete(`/api/v1/claim/${token}/session`) +} + export function confirmClaimRole(token: string) { return apiPost(`/api/v1/claim/${token}/confirm-role`, { confirm: true }) } diff --git a/apps/frontend/src/types/tencent/session.ts b/apps/frontend/src/types/tencent/session.ts index 846efe3f..f4a3b6ee 100644 --- a/apps/frontend/src/types/tencent/session.ts +++ b/apps/frontend/src/types/tencent/session.ts @@ -6,10 +6,12 @@ export type TencentLoginType = 'qq' | 'wx' export type TencentBrowserSessionStatus = | 'waiting_scan' | 'scanned' + | 'expired' | 'logged_in' | 'ready_to_redeem' | 'redeeming' | 'redeemed' + | 'closed' | 'failed' | (string & {}) diff --git a/apps/frontend/src/views/claim/ClaimView.vue b/apps/frontend/src/views/claim/ClaimView.vue index d9549efb..24eecc69 100644 --- a/apps/frontend/src/views/claim/ClaimView.vue +++ b/apps/frontend/src/views/claim/ClaimView.vue @@ -43,7 +43,8 @@ const { screenshotUrl, showScreenshot, createSessionFlow, - refreshSessionSummary, + reloadSessionPage, + closeSessionFlow, switchLoginType, confirmRoleNow, redeemNow, @@ -127,9 +128,10 @@ const helperText = computed(() => { :session-loading="sessionLoading" :session-notice="sessionNotice" @create-session="createSessionFlow" + @close-session="closeSessionFlow" @open-qr-preview="qrPreviewVisible = true" @qr-image-load="handleQrImageLoad" - @reload-session="refreshSessionSummary()" + @reload-session="reloadSessionPage" @switch-login-type="switchLoginType" /> diff --git a/apps/frontend/src/views/tx/TencentBrowserView.vue b/apps/frontend/src/views/tx/TencentBrowserView.vue index ed3f456a..d9821422 100644 --- a/apps/frontend/src/views/tx/TencentBrowserView.vue +++ b/apps/frontend/src/views/tx/TencentBrowserView.vue @@ -35,6 +35,7 @@ const { sessionNotice, statusLabel, switchLoginType, + teardownSession, } = useTencentBrowserSessionPage() const qrPreviewVisible = ref(false) @@ -109,6 +110,7 @@ const showScreenshot = computed(() => Boolean(screenshotUrl.value)) :session-loading="sessionLoading" :session-notice="sessionNotice" @create-session="createSessionFlow" + @close-session="teardownSession" @open-qr-preview="qrPreviewVisible = true" @qr-image-load="handleQrImageLoad" @reload-session="reloadSessionPage"