feat(frontend): add P2 quality gates

This commit is contained in:
yml2213
2026-05-15 17:35:50 +08:00
parent a65c0713fb
commit 787b01749c
62 changed files with 4907 additions and 1355 deletions
@@ -154,7 +154,9 @@ export function useTencentBrowserSessionPolling(options: {
}
}
export function isActiveTencentSessionStatus(status: TencentBrowserSessionStatus | null | undefined) {
export function isActiveTencentSessionStatus(
status: TencentBrowserSessionStatus | null | undefined,
) {
return Boolean(status && ACTIVE_TENCENT_SESSION_STATUSES.has(status))
}
@@ -30,7 +30,9 @@ export function useTencentBrowserSessionPresentation(options: {
} = options
const initButtonLabel = computed(() => `开始初始化 ${loginTypeLabel.value} 登录`)
const scanInstruction = computed(() => `请使用${loginTypeLabel.value}扫描二维码,并在手机上确认登录`)
const scanInstruction = computed(
() => `请使用${loginTypeLabel.value}扫描二维码,并在手机上确认登录`,
)
const qrImage = computed(() =>
session.value?.qrImageBase64 ? `data:image/png;base64,${session.value.qrImageBase64}` : '',
@@ -188,7 +190,7 @@ export function useTencentBrowserSessionPresentation(options: {
const screenshotEmptyMessage = computed(() =>
session.value?.status === 'redeemed'
? '本次兑换可能未生成截图,或截图产物已被后端配置关闭。'
: '如本次兑换生成了结果截图,这里会展示。'
: '如本次兑换生成了结果截图,这里会展示。',
)
return {
@@ -66,21 +66,31 @@ export function useAdminManualRedeemPage() {
)
const hasSession = computed(() => Boolean(session.value?.sessionId))
const loginTypeLabel = computed(() => (loginType.value === 'wx' ? '微信' : 'QQ'))
const sessionNotice = computed(() => pollWarningMessage.value || session.value?.notice || task.value?.lastError || '')
const sessionNotice = computed(
() => pollWarningMessage.value || session.value?.notice || task.value?.lastError || '',
)
const qrImage = computed(() =>
session.value?.qrImageBase64 ? `data:image/png;base64,${session.value.qrImageBase64}` : '',
)
const currentTaskId = computed(() => Number(task.value?.taskId || route.query.taskId || 0) || 0)
const reviewReady = computed(() => Boolean(session.value?.review?.capturedAt) && task.value?.status !== 'redeemed')
const resultReady = computed(() => Boolean(result.value?.screenshotReady) && task.value?.status === 'redeemed')
const reviewReady = computed(
() => Boolean(session.value?.review?.capturedAt) && task.value?.status !== 'redeemed',
)
const resultReady = computed(
() => Boolean(result.value?.screenshotReady) && task.value?.status === 'redeemed',
)
const loginTabs = [
{ value: 'qq' as const, label: 'QQ账号登录' },
{ value: 'wx' as const, label: '微信账号登录' },
]
const statusLabel = computed(() => resolveTaskStatusLabel(task.value?.status, session.value?.status))
const statusLabel = computed(() =>
resolveTaskStatusLabel(task.value?.status, session.value?.status),
)
const initButtonLabel = computed(() => `生成${loginTypeLabel.value}二维码`)
const scanInstruction = computed(() => `请让客户使用${loginTypeLabel.value}扫码,并在手机上确认登录`)
const scanInstruction = computed(
() => `请让客户使用${loginTypeLabel.value}扫码,并在手机上确认登录`,
)
const roleFacts = computed(() => [
{
label: '登录昵称',
@@ -120,18 +130,18 @@ export function useAdminManualRedeemPage() {
])
const canConfirmRole = computed(() =>
Boolean(
task.value
&& task.value.status === 'claimed'
&& hasSession.value
&& roleReady.value
&& !roleConfirmLoading.value,
task.value &&
task.value.status === 'claimed' &&
hasSession.value &&
roleReady.value &&
!roleConfirmLoading.value,
),
)
const canRedeem = computed(() =>
Boolean(
task.value
&& ['role_confirmed', 'redeeming'].includes(task.value.status)
&& !redeemLoading.value,
task.value &&
['role_confirmed', 'redeeming'].includes(task.value.status) &&
!redeemLoading.value,
),
)
const redeemBlockedReason = computed(() => {
@@ -158,13 +168,17 @@ export function useAdminManualRedeemPage() {
return ''
})
const redeemButtonLabel = computed(() => (redeemLoading.value ? '兑换中...' : '开始兑换'))
const screenshotEmptyTitle = computed(() => (task.value?.status === 'redeemed' ? '未获取到结果图' : '等待角色截图'))
const screenshotEmptyTitle = computed(() =>
task.value?.status === 'redeemed' ? '未获取到结果图' : '等待角色截图',
)
const screenshotEmptyMessage = computed(() =>
task.value?.status === 'redeemed'
? '本次兑换可能未生成最终截图,或截图仍在同步中。'
: '客户登录并识别到角色后,这里会展示给客服复核的角色截图。'
: '客户登录并识别到角色后,这里会展示给客服复核的角色截图。',
)
const canCreateTask = computed(
() => Boolean(form.proofValue.trim() && form.skuCode.trim()) && !createLoading.value,
)
const canCreateTask = computed(() => Boolean(form.proofValue.trim() && form.skuCode.trim()) && !createLoading.value)
const hasActiveTask = computed(() => Boolean(task.value?.taskId))
watch(qrImage, () => {
@@ -538,19 +552,20 @@ export function useAdminManualRedeemPage() {
const anchor = document.createElement('a')
anchor.href = screenshotUrl.value
anchor.download = task.value?.status === 'redeemed'
? `manual-redeem-result-${task.value?.taskNo || 'task'}.png`
: `manual-redeem-review-${task.value?.taskNo || 'task'}.png`
anchor.download =
task.value?.status === 'redeemed'
? `manual-redeem-result-${task.value?.taskNo || 'task'}.png`
: `manual-redeem-review-${task.value?.taskNo || 'task'}.png`
anchor.click()
return true
}
async function writeImageBlobToClipboard(blob: Blob, fallbackMessage: string) {
if (
typeof navigator === 'undefined'
|| !navigator.clipboard
|| typeof window === 'undefined'
|| !('ClipboardItem' in window)
typeof navigator === 'undefined' ||
!navigator.clipboard ||
typeof window === 'undefined' ||
!('ClipboardItem' in window)
) {
throw new Error('当前浏览器不支持直接复制图片,请改用截图发送或下载')
}
@@ -569,7 +584,9 @@ export function useAdminManualRedeemPage() {
async function loadScreenshotPreview(nextDetail = detail.value) {
const taskId = Number(nextDetail?.task?.taskId || 0)
const shouldFetch = Boolean(taskId && (nextDetail?.session?.review?.capturedAt || nextDetail?.result?.screenshotReady))
const shouldFetch = Boolean(
taskId && (nextDetail?.session?.review?.capturedAt || nextDetail?.result?.screenshotReady),
)
if (!shouldFetch) {
clearScreenshotPreview()
@@ -601,7 +618,9 @@ export function useAdminManualRedeemPage() {
function resolveSkuName(skuCode: string) {
const normalized = String(skuCode || '').trim()
const matched = suggestions.value.find((item) => String(item.skuCode || '').trim() === normalized)
const matched = suggestions.value.find(
(item) => String(item.skuCode || '').trim() === normalized,
)
return matched?.skuCode || normalized
}
@@ -682,7 +701,9 @@ export function useAdminManualRedeemPage() {
maxWidth: '100%',
}))
const qrPreviewWidth = computed(() => `${Math.min(Math.max(qrDisplayWidth.value + 120, 360), 480)}px`)
const qrPreviewWidth = computed(
() => `${Math.min(Math.max(qrDisplayWidth.value + 120, 360), 480)}px`,
)
return {
form,
@@ -744,7 +765,10 @@ export function useAdminManualRedeemPage() {
}
}
function syncLoginTypeFromDetail(detail: AdminManualRedeemDetail, fallback: TencentLoginType = DEFAULT_LOGIN_TYPE) {
function syncLoginTypeFromDetail(
detail: AdminManualRedeemDetail,
fallback: TencentLoginType = DEFAULT_LOGIN_TYPE,
) {
const nextLoginType = String(detail.session?.loginType || detail.task.loginType || '').trim()
if (nextLoginType === 'wx') {
return 'wx'
@@ -823,7 +847,10 @@ function mergeDetail(current: AdminManualRedeemDetail | null, next: AdminManualR
}
}
function shouldRefreshQrImage(currentSession: AdminManualRedeemDetail['session'], nextSession: AdminManualRedeemDetail['session']) {
function shouldRefreshQrImage(
currentSession: AdminManualRedeemDetail['session'],
nextSession: AdminManualRedeemDetail['session'],
) {
if (!currentSession || !nextSession || !nextSession.artifacts?.hasQrImage) {
return false
}
+37 -24
View File
@@ -22,11 +22,7 @@ import { notifyTencentActionError } from './tencent/session-errors'
const DEFAULT_LOGIN_TYPE: TencentLoginType = 'qq'
const POLL_INTERVAL_MS = 2500
const POLL_FAILURE_LIMIT = 3
const ACTIVE_TASK_STATUSES = new Set<ClaimTaskStatus>([
'claimed',
'role_confirmed',
'redeeming',
])
const ACTIVE_TASK_STATUSES = new Set<ClaimTaskStatus>(['claimed', 'role_confirmed', 'redeeming'])
export function useClaimPage(token: string) {
const detailLoading = ref(true)
@@ -53,7 +49,9 @@ export function useClaimPage(token: string) {
const roleReady = computed(() => Boolean(activityInfo.value?.role?.ready))
const hasSession = computed(() => Boolean(session.value?.sessionId))
const loginTypeLabel = computed(() => (loginType.value === 'wx' ? '微信' : 'QQ'))
const sessionNotice = computed(() => pollWarningMessage.value || session.value?.notice || task.value?.lastError || '')
const sessionNotice = computed(
() => pollWarningMessage.value || session.value?.notice || task.value?.lastError || '',
)
const qrImage = computed(() =>
session.value?.qrImageBase64 ? `data:image/png;base64,${session.value.qrImageBase64}` : '',
)
@@ -64,9 +62,13 @@ export function useClaimPage(token: string) {
{ value: 'wx' as const, label: '微信账号登录' },
]
const statusLabel = computed(() => resolveTaskStatusLabel(task.value?.status, session.value?.status))
const statusLabel = computed(() =>
resolveTaskStatusLabel(task.value?.status, session.value?.status),
)
const initButtonLabel = computed(() => `开始初始化 ${loginTypeLabel.value} 登录`)
const scanInstruction = computed(() => `请使用${loginTypeLabel.value}扫描二维码,并在手机上确认登录`)
const scanInstruction = computed(
() => `请使用${loginTypeLabel.value}扫描二维码,并在手机上确认登录`,
)
const roleFacts = computed(() => [
{
label: '登录昵称',
@@ -106,21 +108,21 @@ export function useClaimPage(token: string) {
])
const canConfirmRole = computed(() =>
Boolean(
task.value
&& !task.value.requiresSupportReview
&& hasSession.value
&& roleReady.value
&& task.value.status === 'claimed',
task.value &&
!task.value.requiresSupportReview &&
hasSession.value &&
roleReady.value &&
task.value.status === 'claimed',
),
)
const canRedeem = computed(() =>
Boolean(
task.value
&& !task.value.requiresSupportReview
&& hasSession.value
&& roleConfirmed.value
&& !redeemLoading.value
&& (task.value.status === 'role_confirmed' || task.value.status === 'redeeming'),
task.value &&
!task.value.requiresSupportReview &&
hasSession.value &&
roleConfirmed.value &&
!redeemLoading.value &&
(task.value.status === 'role_confirmed' || task.value.status === 'redeeming'),
),
)
const redeemBlockedReason = computed(() => {
@@ -158,14 +160,16 @@ export function useClaimPage(token: string) {
return ''
})
const redeemButtonLabel = computed(() => (task.value?.requiresSupportReview ? '等待客服兑换' : '开始兑换'))
const redeemButtonLabel = computed(() =>
task.value?.requiresSupportReview ? '等待客服兑换' : '开始兑换',
)
const screenshotEmptyTitle = computed(() =>
task.value?.status === 'redeemed' ? '本次没有可展示的截图' : '等待截图',
)
const screenshotEmptyMessage = computed(() =>
task.value?.status === 'redeemed'
? '本次兑换可能未生成截图,或截图产物还未同步完成。'
: '领取完成后,如本次生成了结果截图,这里会展示。'
: '领取完成后,如本次生成了结果截图,这里会展示。',
)
function applyLoginTypeFromDetail(nextDetail: ClaimDetailData) {
@@ -183,7 +187,11 @@ export function useClaimPage(token: string) {
activityInfo.value?.role?.partition || '',
].join('|'),
() => {
roleConfirmed.value = Boolean(task.value?.status === 'role_confirmed' || task.value?.status === 'redeeming' || task.value?.status === 'redeemed')
roleConfirmed.value = Boolean(
task.value?.status === 'role_confirmed' ||
task.value?.status === 'redeeming' ||
task.value?.status === 'redeemed',
)
},
{ immediate: true },
)
@@ -514,7 +522,9 @@ export function useClaimPage(token: string) {
maxWidth: '100%',
}))
const qrPreviewWidth = computed(() => `${Math.min(Math.max(qrDisplayWidth.value + 120, 360), 480)}px`)
const qrPreviewWidth = computed(
() => `${Math.min(Math.max(qrDisplayWidth.value + 120, 360), 480)}px`,
)
loadDetail()
@@ -567,7 +577,10 @@ export function useClaimPage(token: string) {
}
}
function syncLoginTypeFromDetail(detail: ClaimDetailData, fallback: TencentLoginType = DEFAULT_LOGIN_TYPE) {
function syncLoginTypeFromDetail(
detail: ClaimDetailData,
fallback: TencentLoginType = DEFAULT_LOGIN_TYPE,
) {
const nextLoginType = String(detail.session?.loginType || detail.task.loginType || '').trim()
if (nextLoginType === 'wx') {
return 'wx'