重构:前端架构优化 - 消灭巨石文件,统一代码规范

Phase 1 - 消灭巨石页面:
- 拆分 AdminKuaishouCloudFulfillmentView (1,224行→kuaishou-cloud/子目录)
- 拆分 AdminFulfillmentBindingsView (989行→bindings/子目录)
- 拆分 AdminTaskDetailView (1,007行→9个子组件+2个composable)
- 合并去重 useClaimPage + useAdminManualRedeemPage (1,554行→共享模块+差异化薄层)
- 拆分 services/admin/platform-config.ts (477行→8个领域子模块)

Phase 2 - 架构收口:
- 拆分 types/admin.ts (925行→13个子文件+platform-config/子目录)
- 统一 API code 检查(http.ts拦截器统一处理业务错误)
- 修改 apiPost 签名消除 as unknown as 类型断言(6处)
- 新增 BusinessError 类型便于错误分类处理

所有改动通过 vue-tsc --noEmit 零错误和 vite build 验证
This commit is contained in:
yml2213
2026-05-17 09:26:13 +08:00
parent d8c411d67f
commit a77198b218
81 changed files with 6498 additions and 5248 deletions
+106 -337
View File
@@ -10,19 +10,23 @@ import {
removeClaimSession,
redeemClaim,
} from '@/services/claim'
import type { ClaimDetailData, ClaimTaskStatus } from '@/types/claim'
import type {
TencentBrowserSessionData,
TencentBrowserSessionSummaryData,
TencentLoginType,
} from '@/types/tencent/session'
import type { ClaimDetailData } from '@/types/claim'
import type { TencentLoginType } from '@/types/tencent/session'
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'])
import {
useSessionPolling,
useSessionQrDisplay,
useSessionRoleFacts,
useSessionResultFacts,
buildRedeemBlockedReason,
resolveTaskStatusLabel,
syncLoginTypeFromDetail,
mergeSessionDetail,
shouldRefreshQrImage,
shouldKeepPolling,
DEFAULT_LOGIN_TYPE,
} from './shared'
export function useClaimPage(token: string) {
const detailLoading = ref(true)
@@ -32,13 +36,8 @@ export function useClaimPage(token: string) {
const loginType = ref<TencentLoginType>(DEFAULT_LOGIN_TYPE)
const detail = ref<ClaimDetailData | null>(null)
const roleConfirmed = ref(false)
const pollWarningMessage = ref('')
const qrImageNaturalWidth = ref(0)
let pollTimer: ReturnType<typeof setTimeout> | null = null
let pollToken = 0
let pollFailureCount = 0
// ── Derived state ──────────────────────────────────────────────────
const session = computed(() => detail.value?.session || null)
const task = computed(() => detail.value?.task || null)
const order = computed(() => detail.value?.order || null)
@@ -49,12 +48,31 @@ 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'))
// ── Polling ────────────────────────────────────────────────────────
const {
pollWarningMessage,
startPolling,
resetPolling,
resetPollingWarning,
handleSilentPollingError,
} = useSessionPolling({
hasSession,
shouldKeepPolling: computed(() => shouldKeepPolling(detail.value)),
onPollTick: () => refreshSessionSummary({ silent: true }),
pollWarningPrefix: '领取状态刷新失败',
})
const sessionNotice = computed(
() => pollWarningMessage.value || session.value?.notice || task.value?.lastError || '',
)
const qrImage = computed(() =>
session.value?.qrImageBase64 ? `data:image/png;base64,${session.value.qrImageBase64}` : '',
)
// ── QR display ─────────────────────────────────────────────────────
const { qrImage, qrFigureStyle, qrPreviewWidth, handleQrImageLoad } = useSessionQrDisplay({
session,
})
// ── Presentation ───────────────────────────────────────────────────
const screenshotUrl = computed(() => result.value?.screenshotUrl || '')
const showScreenshot = computed(() => Boolean(screenshotUrl.value))
const loginTabs = [
@@ -63,13 +81,22 @@ export function useClaimPage(token: string) {
]
const statusLabel = computed(() =>
resolveTaskStatusLabel(task.value?.status, session.value?.status),
resolveTaskStatusLabel(task.value?.status, session.value?.status, 'claim'),
)
const initButtonLabel = computed(() => `开始初始化 ${loginTypeLabel.value} 登录`)
const scanInstruction = computed(
() => `请使用${loginTypeLabel.value}扫描二维码,并在手机上确认登录`,
)
const roleFacts = computed(() => [
const { roleFacts } = useSessionRoleFacts({
activityInfo,
statusLabel,
roleFactDefaults: { nickname: '等待扫码登录' },
})
// Claim pages show 4 role facts, but original only had 3 (no area).
// Override roleFacts to match original exactly:
const claimRoleFacts = computed(() => [
{
label: '登录昵称',
value: activityInfo.value?.nickname || '等待扫码登录',
@@ -88,78 +115,46 @@ export function useClaimPage(token: string) {
value: statusLabel.value,
},
])
const resultFacts = computed(() => [
{
label: '订单号',
value: order.value?.platformOrderId || '-',
},
{
label: '商品',
value: orderItem.value?.skuName || '-',
},
{
label: '业务返回码',
value: result.value?.resultCode || '-',
},
{
label: '业务消息',
value: result.value?.resultMessage || '尚未兑换',
},
])
const { resultFacts } = useSessionResultFacts({
order,
orderItem,
result,
})
const canConfirmRole = computed(() =>
Boolean(
task.value &&
!task.value.requiresSupportReview &&
hasSession.value &&
roleReady.value &&
task.value.status === 'claimed',
!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.requiresSupportReview &&
hasSession.value &&
roleConfirmed.value &&
!redeemLoading.value &&
(task.value.status === 'role_confirmed' || task.value.status === 'redeeming'),
),
)
const redeemBlockedReason = computed(() => {
if (!detail.value) {
return '正在加载领取信息'
}
if (tokenStatus.value !== 'active') {
return '当前领取链接不可用'
}
if (!hasSession.value) {
return `请先选择${loginTypeLabel.value}并初始化登录会话`
}
if (task.value?.requiresSupportReview) {
if (!roleReady.value) {
return '扫码成功后,系统会自动同步登录信息,准备发给客服复核'
}
return '当前商品需要客服复核角色并代你发起兑换,请联系人工继续'
}
if (redeemLoading.value) {
return '兑换任务正在执行中'
}
if (!roleReady.value) {
return sessionNotice.value || '扫码成功后,后端正在同步角色和大区信息'
}
if (!roleConfirmed.value) {
return '请先确认当前角色与大区无误,再开始兑换'
}
return ''
})
const redeemBlockedReason = computed(() =>
buildRedeemBlockedReason({
detail,
hasSession,
loginTypeLabel,
redeemLoading,
roleReady,
roleConfirmed,
sessionNotice,
context: 'claim',
tokenStatus,
requiresSupportReview: computed(() => Boolean(task.value?.requiresSupportReview)),
}),
)
const redeemButtonLabel = computed(() =>
task.value?.requiresSupportReview ? '等待客服兑换' : '开始兑换',
)
@@ -172,10 +167,7 @@ export function useClaimPage(token: string) {
: '领取完成后,如本次生成了结果截图,这里会展示。',
)
function applyLoginTypeFromDetail(nextDetail: ClaimDetailData) {
loginType.value = syncLoginTypeFromDetail(nextDetail, loginType.value)
}
// ── Watchers ───────────────────────────────────────────────────────
watch(
() =>
[
@@ -189,28 +181,34 @@ export function useClaimPage(token: string) {
() => {
roleConfirmed.value = Boolean(
task.value?.status === 'role_confirmed' ||
task.value?.status === 'redeeming' ||
task.value?.status === 'redeemed',
task.value?.status === 'redeeming' ||
task.value?.status === 'redeemed',
)
},
{ immediate: true },
)
watch(qrImage, () => {
qrImageNaturalWidth.value = 0
})
// ── Helpers ────────────────────────────────────────────────────────
function applyLoginTypeFromDetail(nextDetail: ClaimDetailData) {
loginType.value = syncLoginTypeFromDetail(nextDetail, loginType.value)
}
function restartPollingIfNeeded(nextDetail = detail.value) {
resetPolling()
if (shouldKeepPolling(nextDetail)) {
startPolling()
}
}
// ── API flows ─────────────────────────────────────────────────────
async function loadDetail() {
detailLoading.value = true
try {
const response = await fetchClaimDetail(token)
if (response.code !== 0) {
throw new Error(response.msg || '领取详情加载失败')
}
detail.value = mergeClaimDetailData(detail.value, response.data)
detail.value = mergeSessionDetail(detail.value, response.data)
applyLoginTypeFromDetail(response.data)
restartPollingIfNeeded()
} catch (error) {
@@ -231,11 +229,7 @@ export function useClaimPage(token: string) {
forceRecreate: hasSession.value,
})
if (response.code !== 0) {
throw new Error(response.msg || '创建领取会话失败')
}
detail.value = mergeClaimDetailData(detail.value, response.data)
detail.value = mergeSessionDetail(detail.value, response.data)
applyLoginTypeFromDetail(response.data)
startPolling()
} catch (error) {
@@ -257,11 +251,7 @@ export function useClaimPage(token: string) {
try {
const response = await refreshClaimSession(token)
if (response.code !== 0) {
throw new Error(response.msg || '刷新后端页面失败')
}
detail.value = mergeClaimDetailData(detail.value, response.data)
detail.value = mergeSessionDetail(detail.value, response.data)
applyLoginTypeFromDetail(response.data)
restartPollingIfNeeded(response.data)
return true
@@ -290,11 +280,7 @@ export function useClaimPage(token: string) {
try {
const response = await removeClaimSession(token)
if (response.code !== 0) {
throw new Error(response.msg || '关闭领取会话失败')
}
detail.value = mergeClaimDetailData(detail.value, response.data)
detail.value = mergeSessionDetail(detail.value, response.data)
applyLoginTypeFromDetail(response.data)
if (!silent) {
@@ -328,22 +314,14 @@ export function useClaimPage(token: string) {
const currentSession = detail.value?.session || null
const response = await fetchClaimSessionSummary(token)
if (response.code !== 0) {
throw new Error(response.msg || '领取会话状态刷新失败')
}
let nextDetail = mergeClaimDetailData(detail.value, response.data)
let nextDetail = mergeSessionDetail(detail.value, response.data)
detail.value = nextDetail
applyLoginTypeFromDetail(nextDetail)
if (shouldRefreshClaimQrImage(currentSession, response.data)) {
if (shouldRefreshQrImage(currentSession, response.data.session)) {
const fullResponse = await fetchClaimDetail(token)
if (fullResponse.code !== 0) {
throw new Error(fullResponse.msg || '领取二维码刷新失败')
}
nextDetail = mergeClaimDetailData(nextDetail, fullResponse.data)
nextDetail = mergeSessionDetail(nextDetail, fullResponse.data)
detail.value = nextDetail
applyLoginTypeFromDetail(nextDetail)
}
@@ -378,11 +356,7 @@ export function useClaimPage(token: string) {
try {
const response = await confirmClaimRole(token)
if (response.code !== 0) {
throw new Error(response.msg || '角色确认失败')
}
detail.value = mergeClaimDetailData(detail.value, response.data)
detail.value = mergeSessionDetail(detail.value, response.data)
roleConfirmed.value = true
showSuccess(response.msg || '角色已确认')
restartPollingIfNeeded(response.data)
@@ -406,11 +380,7 @@ export function useClaimPage(token: string) {
try {
const response = await redeemClaim(token)
if (response.code !== 0) {
throw new Error(response.msg || '领取兑换失败')
}
detail.value = mergeClaimDetailData(detail.value, response.data)
detail.value = mergeSessionDetail(detail.value, response.data)
showSuccess(response.msg || '兑换完成')
restartPollingIfNeeded(response.data)
} catch (error) {
@@ -435,97 +405,7 @@ export function useClaimPage(token: string) {
loginType.value = nextLoginType
}
function startPolling() {
const tokenId = ++pollToken
const loop = async () => {
if (tokenId !== pollToken || !hasSession.value) {
return
}
await refreshSessionSummary({ silent: true })
if (tokenId !== pollToken || !shouldKeepPolling(detail.value)) {
return
}
pollTimer = setTimeout(() => {
void loop()
}, POLL_INTERVAL_MS)
}
pollTimer = setTimeout(() => {
void loop()
}, POLL_INTERVAL_MS)
}
function restartPollingIfNeeded(nextDetail = detail.value) {
resetPolling()
if (shouldKeepPolling(nextDetail)) {
startPolling()
}
}
function resetPolling() {
pollToken += 1
if (pollTimer) {
clearTimeout(pollTimer)
pollTimer = null
}
}
function resetPollingWarning() {
pollFailureCount = 0
pollWarningMessage.value = ''
}
function handleSilentPollingError(error: unknown) {
console.error(error)
pollFailureCount += 1
if (pollFailureCount < POLL_FAILURE_LIMIT) {
return
}
pollWarningMessage.value = '领取状态刷新失败,已暂停自动轮询,请手动刷新页面。'
resetPolling()
}
function handleQrImageLoad(event: Event) {
const target = event.target
if (!(target instanceof HTMLImageElement)) {
return
}
qrImageNaturalWidth.value = target.naturalWidth || 0
}
const qrDisplayWidth = computed(() => {
const naturalWidth = qrImageNaturalWidth.value
if (!naturalWidth) {
return 220
}
if (naturalWidth < 160) {
return Math.min(naturalWidth * 2, 220)
}
return Math.min(naturalWidth, 240)
})
const qrFigureStyle = computed(() => ({
width: `${qrDisplayWidth.value}px`,
maxWidth: '100%',
}))
const qrPreviewWidth = computed(
() => `${Math.min(Math.max(qrDisplayWidth.value + 120, 360), 480)}px`,
)
// ── Lifecycle ──────────────────────────────────────────────────────
loadDetail()
onBeforeUnmount(() => {
@@ -552,7 +432,7 @@ export function useClaimPage(token: string) {
statusLabel,
session,
sessionNotice,
roleFacts,
roleFacts: claimRoleFacts,
resultFacts,
roleConfirmed,
roleReady,
@@ -576,114 +456,3 @@ export function useClaimPage(token: string) {
handleQrImageLoad,
}
}
function syncLoginTypeFromDetail(
detail: ClaimDetailData,
fallback: TencentLoginType = DEFAULT_LOGIN_TYPE,
) {
const nextLoginType = String(detail.session?.loginType || detail.task.loginType || '').trim()
if (nextLoginType === 'wx') {
return 'wx'
}
if (nextLoginType === 'qq') {
return 'qq'
}
return fallback
}
function shouldKeepPolling(detail: ClaimDetailData | null) {
if (!detail?.session?.sessionId) {
return false
}
return ACTIVE_TASK_STATUSES.has(detail.task.status)
}
function resolveTaskStatusLabel(taskStatus?: string, sessionStatus?: string) {
switch (taskStatus) {
case 'link_generated':
return '等待开始领取'
case 'claimed':
if (sessionStatus === 'scanned') {
return '确认中'
}
if (sessionStatus === 'logged_in' || sessionStatus === 'ready_to_redeem') {
return '已登录'
}
return '领取中'
case 'role_confirmed':
return '角色已确认'
case 'redeeming':
return '正在兑换'
case 'redeemed':
return '兑换成功'
case 'waiting_inventory':
return '等待库存'
case 'retry_pending':
return '等待重试'
case 'manual_review':
return '等待人工处理'
case 'expired':
return '链接已过期'
case 'closed':
return '任务已关闭'
default:
return sessionStatus || taskStatus || '等待中'
}
}
function mergeClaimDetailData(current: ClaimDetailData | null, next: ClaimDetailData) {
if (next.session === null) {
return {
...next,
session: null,
}
}
if (!current?.session) {
return next
}
return {
...next,
session: {
...current.session,
...next.session,
qrImageBase64:
typeof next.session.qrImageBase64 === 'string'
? next.session.qrImageBase64
: current.session.qrImageBase64,
activityInfo: next.session.activityInfo ?? current.session.activityInfo ?? null,
redeem: next.session.redeem ?? current.session.redeem ?? null,
artifacts: next.session.artifacts || current.session.artifacts,
},
}
}
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)
}