diff --git a/apps/frontend/README.md b/apps/frontend/README.md index b9501501..c86531da 100644 --- a/apps/frontend/README.md +++ b/apps/frontend/README.md @@ -1,64 +1,40 @@ -# 腾讯浏览器兑换前端 +# 快手轻量前端 -这是一个聚焦腾讯浏览器会话兑换流程的 Vue 3 前端,当前只保留单一业务入口,不再承担历史迁移脚手架的职责。 +这是快手自动发货拆分后的 Vue 3 前端,只保留快手领取和运营后台相关入口。 ## 功能范围 -- 提供 `/tx/browser` 单页入口 -- 支持 QQ / 微信扫码登录切换 -- 支持浏览器会话创建、状态轮询、角色确认、兑换提交 -- 支持结果信息展示,以及在后端产物存在时预览截图 +- `/claim/:token` 快手 Cloud 领取页 +- `/admin` 运营后台 +- 91 卡券订单补全与任务生成 +- 快手小店核销配置 +- Cloudtentacles 履约配置与调试 +- 内部通知和定时任务配置 -## 项目原则 +## 已移除内容 -- 保持页面职责单一,只服务腾讯浏览器兑换链路 -- 与本地后端接口保持一致,优先保证稳定性和可维护性 -- 页面层尽量薄,状态编排、轮询、展示映射分别收口 +- 腾讯浏览器调试入口 `/tx/browser` +- 腾讯领取页 fallback +- 人工兑换页面 +- 旧履约规则配置页 +- Agiso 店铺消息配置前端入口 ## 本地开发 -安装依赖并启动开发环境: - ```bash -cd /Users/yml/codes/order-site-rewrite npm install npm run dev ``` -构建生产包: +开发环境默认通过 `VITE_API_TARGET` 代理后端: ```bash +VITE_API_TARGET=http://localhost:3000 npm run dev +``` + +## 验证 + +```bash +npm run typecheck npm run build ``` - -开发环境默认代理到本地后端: - -```bash -http://localhost:3000 -``` - -如需切换后端地址,启动前设置: - -```bash -VITE_API_TARGET=http://你的后端地址 npm run dev -``` - -## 结构说明 - -```text -src/ - lib/ HTTP 基础设施 - router/ 路由 - services/ 腾讯接口调用 - types/ 腾讯会话 / 活动 / 兑换类型 - composables/ 页面编排、轮询、展示映射 - components/ 腾讯页面子组件 - views/ 页面壳 - styles/ 全局样式 -``` - -## 当前状态 - -- 主页面已经拆成扫码卡片、兑换面板、结果面板三个组件 -- 会话轮询、错误提示、展示文案已分别从页面模板中抽离 -- 前端当前没有全局 store,所有状态都围绕单页流程局部管理 diff --git a/apps/frontend/src/components.d.ts b/apps/frontend/src/components.d.ts index a872235d..58b3b015 100644 --- a/apps/frontend/src/components.d.ts +++ b/apps/frontend/src/components.d.ts @@ -25,7 +25,6 @@ declare module 'vue' { ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] - ElDialog: typeof import('element-plus/es')['ElDialog'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] @@ -33,7 +32,6 @@ declare module 'vue' { ElForm: typeof import('element-plus/es')['ElForm'] ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElIcon: typeof import('element-plus/es')['ElIcon'] - ElImage: typeof import('element-plus/es')['ElImage'] ElInput: typeof import('element-plus/es')['ElInput'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElMenu: typeof import('element-plus/es')['ElMenu'] @@ -47,7 +45,6 @@ declare module 'vue' { ElSelect: typeof import('element-plus/es')['ElSelect'] ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] ElSpace: typeof import('element-plus/es')['ElSpace'] - ElStatistic: typeof import('element-plus/es')['ElStatistic'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] @@ -57,9 +54,6 @@ declare module 'vue' { ElTooltip: typeof import('element-plus/es')['ElTooltip'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] - TencentAuthCard: typeof import('./components/tencent/TencentAuthCard.vue')['default'] - TencentRedeemPanel: typeof import('./components/tencent/TencentRedeemPanel.vue')['default'] - TencentResultPanel: typeof import('./components/tencent/TencentResultPanel.vue')['default'] } export interface GlobalDirectives { vLoading: typeof import('element-plus/es')['ElLoadingDirective'] diff --git a/apps/frontend/src/components/tencent/TencentAuthCard.vue b/apps/frontend/src/components/tencent/TencentAuthCard.vue deleted file mode 100644 index 3d0d106b..00000000 --- a/apps/frontend/src/components/tencent/TencentAuthCard.vue +++ /dev/null @@ -1,481 +0,0 @@ - - - - - diff --git a/apps/frontend/src/components/tencent/TencentRedeemPanel.vue b/apps/frontend/src/components/tencent/TencentRedeemPanel.vue deleted file mode 100644 index 0a2774d0..00000000 --- a/apps/frontend/src/components/tencent/TencentRedeemPanel.vue +++ /dev/null @@ -1,288 +0,0 @@ - - - - - diff --git a/apps/frontend/src/components/tencent/TencentResultPanel.vue b/apps/frontend/src/components/tencent/TencentResultPanel.vue deleted file mode 100644 index 1585aad4..00000000 --- a/apps/frontend/src/components/tencent/TencentResultPanel.vue +++ /dev/null @@ -1,158 +0,0 @@ - - - - - diff --git a/apps/frontend/src/composables/shared/index.ts b/apps/frontend/src/composables/shared/index.ts deleted file mode 100644 index ab934b7f..00000000 --- a/apps/frontend/src/composables/shared/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -export { useSessionPolling } from './useSessionPolling' -export type { UseSessionPollingOptions } from './useSessionPolling' - -export { useSessionQrDisplay } from './useSessionQrDisplay' - -export { - useSessionRoleFacts, - useSessionResultFacts, - buildRedeemBlockedReason, - resolveTaskStatusLabel, - syncLoginTypeFromDetail, -} from './useSessionPresentation' -export type { TaskStatusLabelContext, FactItem, UseSessionPresentationOptions, UseSessionResultFactsOptions } from './useSessionPresentation' - -export { mergeSessionDetail, shouldRefreshQrImage, shouldKeepPolling } from './mergeSessionDetail' - -export { DEFAULT_LOGIN_TYPE, POLL_INTERVAL_MS, POLL_FAILURE_LIMIT, ACTIVE_TASK_STATUSES } from './sessionConstants' diff --git a/apps/frontend/src/composables/shared/mergeSessionDetail.ts b/apps/frontend/src/composables/shared/mergeSessionDetail.ts deleted file mode 100644 index d476a9df..00000000 --- a/apps/frontend/src/composables/shared/mergeSessionDetail.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { ClaimDetailData } from '@/types/claim' -import type { - TencentBrowserSessionData, - TencentBrowserSessionSummaryData, -} from '@/types/tencent/session' - -import type { ClaimTaskStatus } from '@/types/claim' -import { ACTIVE_TASK_STATUSES } from './sessionConstants' - -/** - * Generic merge of session detail data, preserving QR image and other - * fields that may be absent in summary responses. - * - * The `extraSessionFields` callback lets callers inject domain-specific - * session merge logic (e.g. `review` for admin, `redeem` for claim). - */ -export function mergeSessionDetail( - current: T | null, - next: T, - extraSessionFields?: ( - currentSession: NonNullable, - nextSession: NonNullable, - ) => Partial>, -): T { - if (next.session === null) { - return { - ...next, - session: null, - } as T - } - - if (!current?.session) { - return next - } - - const sessionBase = { - ...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, - } as NonNullable - - const extras = extraSessionFields - ? extraSessionFields(current.session as NonNullable, next.session as NonNullable) - : {} - - return { - ...next, - session: { - ...sessionBase, - ...extras, - }, - } as T -} - -export function shouldRefreshQrImage( - current: TencentBrowserSessionData | TencentBrowserSessionSummaryData | null, - next: TencentBrowserSessionData | TencentBrowserSessionSummaryData | null, -): boolean { - if (!current || !next || !next.artifacts?.hasQrImage) { - return false - } - - if (!current.qrImageBase64) { - return true - } - - return Boolean(next.qrUpdatedAt && next.qrUpdatedAt !== current.qrUpdatedAt) -} - -export function shouldKeepPolling(detail: T | null): boolean { - if (!detail?.session?.sessionId) { - return false - } - - return ACTIVE_TASK_STATUSES.has(detail.task.status as ClaimTaskStatus) -} diff --git a/apps/frontend/src/composables/shared/sessionConstants.ts b/apps/frontend/src/composables/shared/sessionConstants.ts deleted file mode 100644 index b328b05a..00000000 --- a/apps/frontend/src/composables/shared/sessionConstants.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { TencentLoginType } from '@/types/tencent/session' -import type { ClaimTaskStatus } from '@/types/claim' - -export const DEFAULT_LOGIN_TYPE: TencentLoginType = 'qq' -export const POLL_INTERVAL_MS = 2_500 -export const POLL_FAILURE_LIMIT = 3 -export const ACTIVE_TASK_STATUSES = new Set(['claimed', 'role_confirmed', 'redeeming']) diff --git a/apps/frontend/src/composables/shared/useSessionPolling.ts b/apps/frontend/src/composables/shared/useSessionPolling.ts deleted file mode 100644 index ed8ae613..00000000 --- a/apps/frontend/src/composables/shared/useSessionPolling.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { computed, ref, type ComputedRef } from 'vue' - -import { POLL_INTERVAL_MS, POLL_FAILURE_LIMIT } from './sessionConstants' - -export interface UseSessionPollingOptions { - /** Whether there is an active session – used as a guard inside the poll loop. */ - hasSession: ComputedRef - /** Called on each poll tick (typically refreshSessionSummary). */ - onPollTick: () => Promise - /** Optional: determines whether polling should continue. Defaults to checking hasSession. */ - shouldKeepPolling?: ComputedRef - /** Context-specific warning message prefix when polling fails repeatedly. */ - pollWarningPrefix: string -} - -export function useSessionPolling(options: UseSessionPollingOptions) { - const { hasSession, onPollTick, pollWarningPrefix } = options - - const pollWarningMessage = ref('') - - let pollTimer: ReturnType | null = null - let pollToken = 0 - let pollFailureCount = 0 - - const sessionNotice = computed(() => pollWarningMessage.value) - - function startPolling() { - const tokenId = ++pollToken - - const loop = async () => { - if (tokenId !== pollToken || !hasSession.value) { - return - } - - await onPollTick() - - if (tokenId !== pollToken) { - return - } - - const keepPolling = options.shouldKeepPolling - ? options.shouldKeepPolling.value - : hasSession.value - - if (!keepPolling) { - return - } - - pollTimer = setTimeout(() => { - void loop() - }, POLL_INTERVAL_MS) - } - - pollTimer = setTimeout(() => { - void loop() - }, POLL_INTERVAL_MS) - } - - 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 = `${pollWarningPrefix},已暂停自动轮询,请手动刷新。` - resetPolling() - } - - return { - pollWarningMessage, - sessionNotice, - startPolling, - resetPolling, - resetPollingWarning, - handleSilentPollingError, - } -} diff --git a/apps/frontend/src/composables/shared/useSessionPresentation.ts b/apps/frontend/src/composables/shared/useSessionPresentation.ts deleted file mode 100644 index 57cdde8e..00000000 --- a/apps/frontend/src/composables/shared/useSessionPresentation.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { computed, type ComputedRef, type Ref } from 'vue' - -import type { ClaimDetailData } from '@/types/claim' -import type { TencentLoginType } from '@/types/tencent/session' - -import { DEFAULT_LOGIN_TYPE } from './sessionConstants' - -type ReadableRef = Pick, 'value'> - -// --------------------------------------------------------------------------- -// syncLoginTypeFromDetail -// --------------------------------------------------------------------------- - -export function syncLoginTypeFromDetail( - detail: T, - fallback: TencentLoginType = DEFAULT_LOGIN_TYPE, -): TencentLoginType { - const nextLoginType = String(detail.session?.loginType || detail.task.loginType || '').trim() - if (nextLoginType === 'wx') { - return 'wx' - } - - if (nextLoginType === 'qq') { - return 'qq' - } - - return fallback -} - -// --------------------------------------------------------------------------- -// resolveTaskStatusLabel -// --------------------------------------------------------------------------- - -export type TaskStatusLabelContext = 'claim' | 'admin' - -/** - * Resolves a human-readable task status label. - * - * The `context` parameter controls wording differences between the - * consumer-facing claim page and the admin manual-redeem page. - */ -export function resolveTaskStatusLabel( - taskStatus: string | undefined, - sessionStatus: string | undefined, - context: TaskStatusLabelContext, -): string { - switch (taskStatus) { - case 'link_generated': - return context === 'admin' ? '待生成二维码' : '等待开始领取' - case 'claimed': - if (sessionStatus === 'scanned') { - return context === 'admin' ? '客户待确认' : '确认中' - } - if (sessionStatus === 'logged_in' || sessionStatus === 'ready_to_redeem') { - return context === 'admin' ? '已登录待复核' : '已登录' - } - return context === 'admin' ? '等待客户登录' : '领取中' - case 'role_confirmed': - return '角色已确认' - case 'redeeming': - return '正在兑换' - case 'redeemed': - return context === 'admin' ? '兑换完成' : '兑换成功' - case 'waiting_inventory': - return '等待库存' - case 'retry_pending': - return '等待重试' - case 'manual_review': - return '等待人工处理' - case 'expired': - return '链接已过期' - case 'closed': - return '任务已关闭' - default: - return sessionStatus || taskStatus || '等待中' - } -} - -// --------------------------------------------------------------------------- -// roleFacts / resultFacts helpers -// --------------------------------------------------------------------------- - -export interface FactItem { - label: string - value: string - accent?: boolean -} - -export interface UseSessionPresentationOptions { - activityInfo: ComputedRef<{ - nickname?: string - role?: { - roleId?: string - roleName?: string - area?: string - ready?: boolean - } | null - } | null> - statusLabel: ComputedRef - /** Context-specific default values for roleFacts */ - roleFactDefaults: { - nickname: string - } -} - -export function useSessionRoleFacts(options: UseSessionPresentationOptions) { - const { activityInfo, roleFactDefaults } = options - - const roleFacts = computed(() => [ - { - label: '登录昵称', - value: activityInfo.value?.nickname || roleFactDefaults.nickname, - }, - { - label: '当前角色', - value: activityInfo.value?.role?.roleName || '后端浏览器同步中', - accent: true, - }, - { - label: '角色 ID', - value: activityInfo.value?.role?.roleId || '未识别', - }, - { - label: '所在大区', - value: activityInfo.value?.role?.area || '未识别', - }, - ]) - - return { roleFacts } -} - -export interface UseSessionResultFactsOptions { - order: ComputedRef<{ platformOrderId?: string } | null> - orderItem: ComputedRef<{ skuName?: string } | null> - result: ComputedRef<{ resultCode?: string; resultMessage?: string } | null> - /** Admin context has manualRequest.proofValue as an alternative first label */ - resultFactOverrides?: { - firstLabel?: string - firstValue?: ComputedRef - } -} - -export function useSessionResultFacts(options: UseSessionResultFactsOptions) { - const { order, orderItem, result, resultFactOverrides } = options - - const resultFacts = computed(() => [ - { - label: resultFactOverrides?.firstLabel || '订单号', - value: resultFactOverrides?.firstValue?.value || order.value?.platformOrderId || '-', - }, - { - label: '商品', - value: orderItem.value?.skuName || '-', - }, - { - label: '业务返回码', - value: result.value?.resultCode || '-', - }, - { - label: '业务消息', - value: result.value?.resultMessage || '尚未兑换', - }, - ]) - - return { resultFacts } -} - -// --------------------------------------------------------------------------- -// redeemBlockedReason — builds the "why can't I redeem" explanation -// --------------------------------------------------------------------------- - -export function buildRedeemBlockedReason(options: { - detail: ReadableRef - hasSession: ReadableRef - loginTypeLabel: ReadableRef - redeemLoading: ReadableRef - roleReady: ReadableRef - roleConfirmed: ReadableRef - sessionNotice: ReadableRef - context: 'claim' | 'admin' - /** Claim-specific fields */ - tokenStatus?: ReadableRef - requiresSupportReview?: ReadableRef -}): string { - const { - detail, - hasSession, - loginTypeLabel, - redeemLoading, - roleReady, - roleConfirmed, - sessionNotice, - context, - tokenStatus, - requiresSupportReview, - } = options - - if (context === 'claim') { - if (!detail.value) { - return '正在加载领取信息' - } - if (tokenStatus && tokenStatus.value !== 'active') { - return '当前领取链接不可用' - } - } else { - // admin context - if (!detail.value) { - return '请先创建人工兑换任务并预占库存' - } - } - - if (!hasSession.value) { - return context === 'claim' - ? `请先选择${loginTypeLabel.value}并初始化登录会话` - : `请先生成${loginTypeLabel.value}二维码并等待客户登录` - } - - // Claim-specific: support review flow - if (context === 'claim' && requiresSupportReview?.value) { - if (!roleReady.value) { - return '扫码成功后,系统会自动同步登录信息,准备发给客服复核' - } - return '当前商品需要客服复核角色并代你发起兑换,请联系人工继续' - } - - if (redeemLoading.value) { - return '兑换任务正在执行中' - } - - if (!roleReady.value) { - return ( - sessionNotice.value || - (context === 'claim' - ? '扫码成功后,后端正在同步角色和大区信息' - : '客户登录后,系统会自动同步角色和大区信息') - ) - } - - if (!roleConfirmed.value) { - return context === 'claim' - ? '请先确认当前角色与大区无误,再开始兑换' - : '请先让客户确认角色截图,再点击确认当前角色' - } - - return '' -} diff --git a/apps/frontend/src/composables/shared/useSessionQrDisplay.ts b/apps/frontend/src/composables/shared/useSessionQrDisplay.ts deleted file mode 100644 index 802d805e..00000000 --- a/apps/frontend/src/composables/shared/useSessionQrDisplay.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { computed, ref, watch, type ComputedRef } from 'vue' - -/** - * Shared QR code display logic: renders the base64 QR image, tracks its - * natural width after load, and derives display/preview widths. - */ -export function useSessionQrDisplay(options: { - session: ComputedRef<{ qrImageBase64?: string } | null> -}) { - const { session } = options - - const qrImageNaturalWidth = ref(0) - - const qrImage = computed(() => - session.value?.qrImageBase64 ? `data:image/png;base64,${session.value.qrImageBase64}` : '', - ) - - watch(qrImage, () => { - qrImageNaturalWidth.value = 0 - }) - - 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`, - ) - - return { - qrImage, - qrImageNaturalWidth, - qrDisplayWidth, - qrFigureStyle, - qrPreviewWidth, - handleQrImageLoad, - } -} diff --git a/apps/frontend/src/composables/tencent/session-errors.ts b/apps/frontend/src/composables/tencent/session-errors.ts deleted file mode 100644 index 6bc31eb6..00000000 --- a/apps/frontend/src/composables/tencent/session-errors.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { showError } from '@/lib/feedback' - -export type TencentActionError = Error & { - errorCode?: string - status?: number -} - -export function notifyTencentActionError(error: unknown, fallbackMessage: string) { - const message = resolveTencentActionMessage(error, fallbackMessage) - console.error(error) - showError(message) -} - -export function resolveTencentActionMessage(error: unknown, fallbackMessage: string) { - const normalizedError = error as TencentActionError - const errorCode = String(normalizedError?.errorCode || '').trim() - const message = normalizedError instanceof Error ? normalizedError.message.trim() : '' - - if (errorCode === 'missing_redeem_code') { - return '请输入兑换码' - } - - if (errorCode === 'session_not_ready') { - return '当前会话还不能兑换,请先完成扫码并确认角色信息' - } - - if (errorCode === 'session_not_found' || errorCode === 'not_found') { - return '当前会话不存在或已过期,请重新生成二维码' - } - - if (errorCode === 'session_closed' || errorCode === 'gone') { - return '当前会话已关闭,请重新生成二维码' - } - - if (errorCode === 'dependency_unavailable') { - return '后端依赖暂不可用,请检查浏览器或 OCR 服务是否已就绪' - } - - if (errorCode === 'invalid_request') { - return '请求参数不完整,请检查输入后重试' - } - - if (errorCode === 'conflict') { - return '当前会话状态已变化,请刷新后重试' - } - - return message || fallbackMessage -} diff --git a/apps/frontend/src/composables/tencent/useTencentBrowserSessionPolling.ts b/apps/frontend/src/composables/tencent/useTencentBrowserSessionPolling.ts deleted file mode 100644 index 4064734e..00000000 --- a/apps/frontend/src/composables/tencent/useTencentBrowserSessionPolling.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { computed, ref, type Ref } from 'vue' - -import { - fetchTencentBrowserSession, - fetchTencentBrowserSessionSummary, -} from '@/services/tencent/session' -import type { - TencentBrowserSessionData, - TencentBrowserSessionStatus, - TencentBrowserSessionSummaryData, -} from '@/types/tencent/session' - -import { resolveTencentActionMessage } from './session-errors' - -const POLL_INTERVAL_MS = 2_500 -const POLL_FAILURE_LIMIT = 3 - -export const ACTIVE_TENCENT_SESSION_STATUSES = new Set([ - 'waiting_scan', - 'scanned', - 'logged_in', - 'ready_to_redeem', - 'redeeming', -]) - -export function useTencentBrowserSessionPolling(options: { - session: Ref - sessionLoading: Ref - notifyActionError: (error: unknown, fallbackMessage: string) => void -}) { - const { session, sessionLoading, notifyActionError } = options - const pollFailureCount = ref(0) - const pollWarningMessage = ref('') - - let pollTimer: ReturnType | null = null - let pollToken = 0 - - const sessionNotice = computed(() => pollWarningMessage.value || session.value?.notice || '') - - async function refreshSession({ silent = false } = {}) { - if (!session.value?.sessionId) { - return false - } - - if (!silent) { - sessionLoading.value = true - resetPollingWarning() - } - - try { - const currentSession = session.value - const response = await fetchTencentBrowserSessionSummary(currentSession.sessionId) - - const nextSession = mergeSessionData(currentSession, response.data) - session.value = nextSession - - if (shouldRefreshQrImage(currentSession, response.data)) { - const fullResponse = await fetchTencentBrowserSession(currentSession.sessionId) - - session.value = mergeSessionData(nextSession, fullResponse.data) - } - - if (silent) { - resetPollingWarning() - } - - return true - } catch (error) { - if (silent) { - handleSilentPollingError(error) - } else { - notifyActionError(error, '获取浏览器会话状态失败') - } - - return false - } finally { - if (!silent) { - sessionLoading.value = false - } - } - } - - function startPolling() { - const token = ++pollToken - - const loop = async () => { - if (token !== pollToken || !session.value?.sessionId) { - return - } - - await refreshSession({ silent: true }) - - if (token !== pollToken || !isActiveTencentSessionStatus(session.value?.status)) { - return - } - - pollTimer = setTimeout(() => { - void loop() - }, POLL_INTERVAL_MS) - } - - pollTimer = setTimeout(() => { - void loop() - }, POLL_INTERVAL_MS) - } - - function resetPolling() { - pollToken += 1 - - if (pollTimer) { - clearTimeout(pollTimer) - pollTimer = null - } - } - - function resetPollingWarning() { - pollFailureCount.value = 0 - pollWarningMessage.value = '' - } - - function restartPollingIfActive(nextSession = session.value) { - if (isActiveTencentSessionStatus(nextSession?.status)) { - startPolling() - } - } - - function handleSilentPollingError(error: unknown) { - console.error(error) - pollFailureCount.value += 1 - - if (pollFailureCount.value < POLL_FAILURE_LIMIT) { - return - } - - pollWarningMessage.value = `${resolveTencentActionMessage( - error, - '会话状态刷新失败', - )},已暂停自动轮询,请手动刷新或重新生成二维码。` - resetPolling() - } - - return { - refreshSession, - resetPolling, - resetPollingWarning, - restartPollingIfActive, - sessionNotice, - startPolling, - } -} - -export function isActiveTencentSessionStatus( - status: TencentBrowserSessionStatus | null | undefined, -) { - return Boolean(status && ACTIVE_TENCENT_SESSION_STATUSES.has(status)) -} - -function mergeSessionData( - current: TencentBrowserSessionData | null, - next: TencentBrowserSessionSummaryData | TencentBrowserSessionData, -) { - if (!current) { - return { - ...next, - qrImageBase64: next.qrImageBase64 || '', - } - } - - return { - ...current, - ...next, - qrImageBase64: - typeof next.qrImageBase64 === 'string' ? next.qrImageBase64 : current.qrImageBase64, - activityInfo: next.activityInfo ?? current.activityInfo ?? null, - redeem: next.redeem ?? current.redeem ?? null, - artifacts: next.artifacts || current.artifacts, - } -} - -function shouldRefreshQrImage( - current: TencentBrowserSessionData, - next: 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/tencent/useTencentBrowserSessionPresentation.ts b/apps/frontend/src/composables/tencent/useTencentBrowserSessionPresentation.ts deleted file mode 100644 index b96c15a4..00000000 --- a/apps/frontend/src/composables/tencent/useTencentBrowserSessionPresentation.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { computed, type ComputedRef, type Ref } from 'vue' - -import { buildTencentBrowserSessionScreenshotUrl } from '@/services/tencent/artifacts' -import type { TencentBrowserActivityInfo } from '@/types/tencent/activity' -import type { TencentBrowserSessionData } from '@/types/tencent/session' - -const REDEEM_ALLOWED_STATUSES = new Set(['logged_in', 'ready_to_redeem', 'redeemed']) - -export function useTencentBrowserSessionPresentation(options: { - activityInfo: ComputedRef - hasSession: ComputedRef - loginTypeLabel: ComputedRef - redeemLoading: Ref - roleConfirmed: Ref - roleReady: ComputedRef - session: Ref - sessionLoading: Ref - sessionNotice: ComputedRef -}) { - const { - activityInfo, - hasSession, - loginTypeLabel, - redeemLoading, - roleConfirmed, - roleReady, - session, - sessionLoading, - sessionNotice, - } = options - - const initButtonLabel = computed(() => `开始初始化 ${loginTypeLabel.value} 登录`) - const scanInstruction = computed( - () => `请使用${loginTypeLabel.value}扫描二维码,并在手机上确认登录`, - ) - - const qrImage = computed(() => - session.value?.qrImageBase64 ? `data:image/png;base64,${session.value.qrImageBase64}` : '', - ) - - const screenshotUrl = computed(() => { - if (!session.value?.sessionId || !session.value?.artifacts?.hasScreenshot) { - return '' - } - - return buildTencentBrowserSessionScreenshotUrl(session.value.sessionId, session.value.updatedAt) - }) - - const statusLabel = computed(() => { - if (!hasSession.value) { - return '待初始化' - } - - switch (session.value?.status) { - case 'waiting_scan': - return '等待扫码' - case 'scanned': - return '已扫码待确认' - case 'logged_in': - return '页面已登录' - case 'ready_to_redeem': - return '可以兑换' - case 'redeeming': - return '兑换中' - case 'redeemed': - return '已完成' - case 'failed': - return '会话异常' - default: - return '初始化中' - } - }) - - const canRedeem = computed(() => { - if (redeemLoading.value || sessionLoading.value || !session.value?.sessionId) { - return false - } - - return ( - REDEEM_ALLOWED_STATUSES.has(String(session.value.status || '')) && - roleReady.value && - roleConfirmed.value - ) - }) - - const redeemBlockedReason = computed(() => { - if (!session.value?.sessionId) { - return `请先选择${loginTypeLabel.value}登录,并手动开始初始化浏览器会话` - } - - if (redeemLoading.value) { - return '兑换任务正在执行中' - } - - if (sessionLoading.value) { - return '正在刷新浏览器会话状态' - } - - if (!roleReady.value) { - return sessionNotice.value || '扫码成功后,后端正在同步角色和大区信息' - } - - if (!roleConfirmed.value) { - return '请先确认当前角色与大区无误,再开始兑换' - } - - if (REDEEM_ALLOWED_STATUSES.has(String(session.value.status || ''))) { - return '' - } - - switch (session.value.status) { - case 'waiting_scan': - return `请先使用${loginTypeLabel.value}扫码` - case 'scanned': - return '请在手机上确认登录后再兑换' - case 'failed': - return sessionNotice.value || '浏览器会话异常,请重新生成二维码' - default: - return sessionNotice.value || '当前状态还不能开始兑换' - } - }) - - const finalRedeemMessage = computed(() => { - const result = session.value?.redeem?.final?.redeem - - if (!result || typeof result !== 'object') { - return '' - } - - const record = result as Record - return String(record.sMsg || record.msg || '') - }) - - const finalRetCode = computed(() => { - const result = session.value?.redeem?.final?.redeem - - if (!result || typeof result !== 'object') { - return '' - } - - const record = result as Record - return String(record.iRet ?? '') - }) - - const roleFacts = computed(() => [ - { - label: '登录昵称', - value: activityInfo.value?.nickname || '等待扫码登录', - }, - { - label: '当前角色', - value: activityInfo.value?.role?.roleName || '后端浏览器同步中', - accent: true, - }, - { - label: '角色 ID', - value: activityInfo.value?.role?.roleId || '未识别', - }, - { - label: '验证码', - value: - activityInfo.value?.form?.verifyValue || - (activityInfo.value?.verify?.visible ? '等待识别' : '等待显示'), - }, - ]) - - const resultFacts = computed(() => [ - { - label: '业务返回码', - value: finalRetCode.value || '-', - }, - { - label: '业务消息', - value: finalRedeemMessage.value || '尚未兑换', - }, - { - label: 'OCR 尝试次数', - value: String(session.value?.redeem?.attempts.length || 0), - }, - { - label: '证明截图', - value: session.value?.artifacts?.hasScreenshot ? '已生成' : '未生成', - }, - ]) - - const redeemButtonLabel = computed(() => '开始兑换') - const screenshotEmptyTitle = computed(() => - session.value?.status === 'redeemed' ? '本次没有可展示的截图' : '等待截图', - ) - const screenshotEmptyMessage = computed(() => - session.value?.status === 'redeemed' - ? '本次兑换可能未生成截图,或截图产物已被后端配置关闭。' - : '如本次兑换生成了结果截图,这里会展示。', - ) - - return { - canRedeem, - initButtonLabel, - qrImage, - redeemBlockedReason, - redeemButtonLabel, - resultFacts, - roleFacts, - scanInstruction, - screenshotEmptyMessage, - screenshotEmptyTitle, - screenshotUrl, - statusLabel, - } -} diff --git a/apps/frontend/src/composables/useAdminManualRedeemPage.ts b/apps/frontend/src/composables/useAdminManualRedeemPage.ts deleted file mode 100644 index 7e3446af..00000000 --- a/apps/frontend/src/composables/useAdminManualRedeemPage.ts +++ /dev/null @@ -1,714 +0,0 @@ -import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue' -import { useRoute, useRouter } from 'vue-router' - -import { showConfirm, showSuccess } from '@/lib/feedback' -import { - closeAdminManualRedeemTask, - confirmAdminManualRedeemRole, - createAdminManualRedeemSession, - createAdminManualRedeemTask, - fetchAdminManualRedeemDetail, - fetchAdminManualRedeemSessionSummary, - fetchAdminManualRedeemSkuSuggestions, - fetchAdminTaskScreenshot, - refreshAdminManualRedeemSession, - redeemAdminManualRedeem, - removeAdminManualRedeemSession, -} from '@/services/admin' -import type { AdminInventorySkuSuggestion, AdminManualRedeemDetail } from '@/types/admin' -import type { TencentLoginType } from '@/types/tencent/session' - -import { notifyTencentActionError } from './tencent/session-errors' -import { - useSessionPolling, - useSessionQrDisplay, - useSessionResultFacts, - buildRedeemBlockedReason, - resolveTaskStatusLabel, - syncLoginTypeFromDetail, - mergeSessionDetail, - shouldRefreshQrImage, - shouldKeepPolling, - DEFAULT_LOGIN_TYPE, -} from './shared' - -export function useAdminManualRedeemPage() { - const route = useRoute() - const router = useRouter() - - // ── Form state (admin-only) ──────────────────────────────────────── - const form = reactive({ - proofValue: '', - skuCode: '', - remark: '', - }) - const suggestions = ref([]) - - // ── Core state ──────────────────────────────────────────────────── - const detail = ref(null) - const detailLoading = ref(false) - const createLoading = ref(false) - const sessionLoading = ref(false) - const roleConfirmLoading = ref(false) - const redeemLoading = ref(false) - const closeLoading = ref(false) - const loginType = ref(DEFAULT_LOGIN_TYPE) - - // ── Screenshot state (admin-only) ────────────────────────────────── - const screenshotBlob = ref(null) - const screenshotUrl = ref('') - const screenshotLoading = ref(false) - - // ── Derived state ────────────────────────────────────────────────── - const session = computed(() => detail.value?.session || null) - const task = computed(() => detail.value?.task || null) - const order = computed(() => detail.value?.order || null) - const orderItem = computed(() => detail.value?.orderItem || null) - const result = computed(() => detail.value?.result || null) - const manualRequest = computed(() => detail.value?.manualRequest || null) - const activityInfo = computed(() => session.value?.activityInfo || null) - const roleReady = computed(() => Boolean(activityInfo.value?.role?.ready)) - const roleConfirmed = computed(() => - Boolean(task.value && ['role_confirmed', 'redeeming', 'redeemed'].includes(task.value.status)), - ) - const hasSession = computed(() => Boolean(session.value?.sessionId)) - const loginTypeLabel = computed(() => (loginType.value === 'wx' ? '微信' : 'QQ')) - 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', - ) - - // ── Polling ──────────────────────────────────────────────────────── - const { - pollWarningMessage, - startPolling, - resetPolling, - resetPollingWarning, - handleSilentPollingError, - } = useSessionPolling({ - hasSession: computed(() => Boolean(task.value?.taskId && hasSession.value)), - shouldKeepPolling: computed(() => shouldKeepPolling(detail.value)), - onPollTick: async () => { await refreshSessionSummary({ silent: true }) }, - pollWarningPrefix: '人工兑换状态刷新失败', - }) - - const sessionNotice = computed( - () => pollWarningMessage.value || session.value?.notice || task.value?.lastError || '', - ) - - // ── QR display ───────────────────────────────────────────────────── - const { - qrImage, - qrFigureStyle, - qrPreviewWidth, - handleQrImageLoad, - } = useSessionQrDisplay({ session }) - - // ── Presentation ─────────────────────────────────────────────────── - const loginTabs = [ - { value: 'qq' as const, label: 'QQ账号登录' }, - { value: 'wx' as const, label: '微信账号登录' }, - ] - - const statusLabel = computed(() => - resolveTaskStatusLabel(task.value?.status, session.value?.status, 'admin'), - ) - const initButtonLabel = computed(() => `生成${loginTypeLabel.value}二维码`) - const scanInstruction = computed( - () => `请让客户使用${loginTypeLabel.value}扫码,并在手机上确认登录`, - ) - - const proofValue = computed(() => manualRequest.value?.proofValue || order.value?.platformOrderId || '-') - - const { resultFacts } = useSessionResultFacts({ - order, - orderItem, - result, - resultFactOverrides: { - firstLabel: '唯一凭据', - firstValue: proofValue, - }, - }) - - // Admin overrides roleFacts to include "所在大区" and exclude "任务状态" - const adminRoleFacts = computed(() => [ - { - label: '登录昵称', - value: activityInfo.value?.nickname || '等待客户登录', - }, - { - label: '当前角色', - value: activityInfo.value?.role?.roleName || '后端浏览器同步中', - accent: true, - }, - { - label: '角色 ID', - value: activityInfo.value?.role?.roleId || '未识别', - }, - { - label: '所在大区', - value: activityInfo.value?.role?.area || '未识别', - }, - ]) - - const canConfirmRole = computed(() => - Boolean( - 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, - ), - ) - const redeemBlockedReason = computed(() => - buildRedeemBlockedReason({ - detail, - hasSession, - loginTypeLabel, - redeemLoading, - roleReady, - roleConfirmed, - sessionNotice, - context: 'admin', - }), - ) - const redeemButtonLabel = computed(() => (redeemLoading.value ? '兑换中...' : '开始兑换')) - 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 hasActiveTask = computed(() => Boolean(task.value?.taskId)) - - // ── Watchers ─────────────────────────────────────────────────────── - watch(qrImage, () => { - // QR image natural width reset handled by useSessionQrDisplay - }) - - watch( - () => currentTaskId.value, - (taskId, previousTaskId) => { - if (!taskId || taskId === previousTaskId) { - return - } - - void loadDetail(taskId) - }, - { immediate: true }, - ) - - onMounted(() => { - void loadSkuSuggestions() - }) - - onBeforeUnmount(() => { - resetPolling() - clearScreenshotPreview() - }) - - // ── Helpers ──────────────────────────────────────────────────────── - function restartPollingIfNeeded(nextDetail = detail.value) { - resetPolling() - - if (shouldKeepPolling(nextDetail)) { - startPolling() - } - } - - function resolveSkuName(skuCode: string) { - const normalized = String(skuCode || '').trim() - const matched = suggestions.value.find( - (item) => String(item.skuCode || '').trim() === normalized, - ) - return matched?.skuCode || normalized - } - - // ── Screenshot helpers (admin-only) ─────────────────────────────── - async function loadScreenshotPreview(nextDetail = detail.value) { - const taskId = Number(nextDetail?.task?.taskId || 0) - const shouldFetch = Boolean( - taskId && (nextDetail?.session?.review?.capturedAt || nextDetail?.result?.screenshotReady), - ) - - if (!shouldFetch) { - clearScreenshotPreview() - return - } - - screenshotLoading.value = true - - try { - const blob = await fetchAdminTaskScreenshot(taskId) - clearScreenshotPreview() - screenshotBlob.value = blob - screenshotUrl.value = URL.createObjectURL(blob) - } catch { - clearScreenshotPreview() - } finally { - screenshotLoading.value = false - } - } - - function clearScreenshotPreview() { - screenshotBlob.value = null - - if (screenshotUrl.value) { - URL.revokeObjectURL(screenshotUrl.value) - screenshotUrl.value = '' - } - } - - async function writeImageBlobToClipboard(blob: Blob, fallbackMessage: string) { - if ( - typeof navigator === 'undefined' || - !navigator.clipboard || - typeof window === 'undefined' || - !('ClipboardItem' in window) - ) { - throw new Error('当前浏览器不支持直接复制图片,请改用截图发送或下载') - } - - try { - const ClipboardItemCtor = window.ClipboardItem as typeof ClipboardItem - await navigator.clipboard.write([ - new ClipboardItemCtor({ - [blob.type || 'image/png']: blob, - }), - ]) - } catch (error) { - throw error instanceof Error ? error : new Error(fallbackMessage) - } - } - - // ── API flows ───────────────────────────────────────────────────── - async function loadSkuSuggestions(keyword = '') { - try { - const response = await fetchAdminManualRedeemSkuSuggestions({ - credentialType: 'tencent_code', - keyword: keyword.trim(), - }) - suggestions.value = response.data.items - } catch (error) { - notifyTencentActionError(error, '读取人工兑换 SKU 建议失败') - } - } - - async function loadDetail(taskId = currentTaskId.value) { - if (!taskId) { - detail.value = null - clearScreenshotPreview() - return false - } - - detailLoading.value = true - - try { - const response = await fetchAdminManualRedeemDetail(taskId) - detail.value = response.data - loginType.value = syncLoginTypeFromDetail(response.data, loginType.value) - await loadScreenshotPreview(response.data) - restartPollingIfNeeded() - return true - } catch (error) { - notifyTencentActionError(error, '读取人工兑换详情失败') - return false - } finally { - detailLoading.value = false - } - } - - async function createTaskNow() { - if (!canCreateTask.value) { - return - } - - createLoading.value = true - - try { - const response = await createAdminManualRedeemTask({ - proofValue: form.proofValue.trim(), - skuCode: form.skuCode.trim(), - skuName: resolveSkuName(form.skuCode), - remark: form.remark.trim(), - }) - - detail.value = response.data - loginType.value = syncLoginTypeFromDetail(response.data, loginType.value) - await loadScreenshotPreview(response.data) - await router.replace({ - path: route.path, - query: { - taskId: String(response.data.task.taskId), - }, - }) - showSuccess(response.msg || '人工兑换任务已创建') - } catch (error) { - notifyTencentActionError(error, '创建人工兑换任务失败') - } finally { - createLoading.value = false - } - } - - async function createSessionFlow(nextLoginType = loginType.value) { - if (!task.value?.taskId) { - return - } - - sessionLoading.value = true - resetPolling() - resetPollingWarning() - - try { - loginType.value = nextLoginType - const response = await createAdminManualRedeemSession(task.value.taskId, { - loginType: nextLoginType, - forceRecreate: hasSession.value, - }) - - detail.value = response.data - loginType.value = syncLoginTypeFromDetail(response.data, nextLoginType) - await loadScreenshotPreview(response.data) - startPolling() - } catch (error) { - notifyTencentActionError(error, '创建人工兑换登录会话失败') - } finally { - sessionLoading.value = false - } - } - - async function refreshSessionFlow() { - if (!task.value?.taskId) { - return false - } - - sessionLoading.value = true - resetPolling() - resetPollingWarning() - - try { - const response = await refreshAdminManualRedeemSession(task.value.taskId) - detail.value = response.data - loginType.value = syncLoginTypeFromDetail(response.data, loginType.value) - await loadScreenshotPreview(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 (!task.value?.taskId) { - return false - } - - if (!silent) { - sessionLoading.value = true - } - - resetPolling() - resetPollingWarning() - - try { - const response = await removeAdminManualRedeemSession(task.value.taskId) - detail.value = response.data - loginType.value = syncLoginTypeFromDetail(response.data, loginType.value) - await loadScreenshotPreview(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 (!task.value?.taskId) { - return false - } - - if (!silent) { - sessionLoading.value = true - resetPollingWarning() - } - - try { - const currentSession = detail.value?.session || null - const response = await fetchAdminManualRedeemSessionSummary(task.value.taskId) - detail.value = mergeSessionDetail( - detail.value, - response.data, - (currentSession, nextSession) => ({ - review: nextSession.review ?? currentSession.review ?? null, - }), - ) - loginType.value = syncLoginTypeFromDetail(response.data, loginType.value) - - if (shouldRefreshQrImage(currentSession, response.data.session)) { - const fullResponse = await fetchAdminManualRedeemDetail(task.value.taskId!) - detail.value = mergeSessionDetail( - detail.value, - fullResponse.data, - (currentSession, nextSession) => ({ - review: nextSession.review ?? currentSession.review ?? null, - }), - ) - loginType.value = syncLoginTypeFromDetail(fullResponse.data, loginType.value) - } - - await loadScreenshotPreview(detail.value) - - if (silent) { - resetPollingWarning() - } - - return true - } catch (error) { - if (silent) { - handleSilentPollingError(error) - } else { - notifyTencentActionError(error, '刷新人工兑换会话状态失败') - } - - return false - } finally { - if (!silent) { - sessionLoading.value = false - } - } - } - - async function confirmRoleNow() { - if (!task.value?.taskId || !canConfirmRole.value) { - return - } - - roleConfirmLoading.value = true - - try { - const response = await confirmAdminManualRedeemRole(task.value.taskId) - detail.value = response.data - await loadScreenshotPreview(response.data) - showSuccess(response.msg || '角色已确认') - restartPollingIfNeeded(response.data) - } catch (error) { - notifyTencentActionError(error, '确认角色失败') - await refreshSessionSummary({ silent: true }) - } finally { - roleConfirmLoading.value = false - } - } - - async function redeemNow() { - if (!task.value?.taskId || !canRedeem.value) { - return - } - - redeemLoading.value = true - resetPolling() - resetPollingWarning() - - try { - const response = await redeemAdminManualRedeem(task.value.taskId) - detail.value = response.data - await loadScreenshotPreview(response.data) - showSuccess(response.msg || '兑换完成') - restartPollingIfNeeded(response.data) - } catch (error) { - notifyTencentActionError(error, '人工兑换失败') - await refreshSessionSummary({ silent: true }) - restartPollingIfNeeded() - } finally { - redeemLoading.value = false - } - } - - async function closeTaskNow() { - if (!task.value?.taskId) { - return - } - - try { - await showConfirm( - `确认关闭任务 ${task.value.taskNo} 吗?关闭后会释放未消耗的资源,并结束当前人工兑换流程。`, - '关闭人工兑换', - ) - } catch { - return - } - - closeLoading.value = true - - try { - const response = await closeAdminManualRedeemTask(task.value.taskId) - detail.value = response.data - await loadScreenshotPreview(response.data) - resetPolling() - showSuccess(response.msg || '人工兑换任务已关闭') - } catch (error) { - notifyTencentActionError(error, '关闭人工兑换任务失败') - } finally { - closeLoading.value = false - } - } - - async function switchLoginType(nextLoginType: TencentLoginType) { - if (nextLoginType === loginType.value) { - return - } - - if (hasSession.value) { - await createSessionFlow(nextLoginType) - return - } - - loginType.value = nextLoginType - } - - function resetCurrentTask() { - resetPolling() - detail.value = null - clearScreenshotPreview() - void router.replace({ - path: route.path, - query: {}, - }) - } - - async function copyQrImage() { - if (!qrImage.value) { - return false - } - - try { - const response = await fetch(qrImage.value) - const blob = await response.blob() - await writeImageBlobToClipboard(blob, '复制二维码失败') - showSuccess('二维码已复制到剪贴板') - return true - } catch (error) { - notifyTencentActionError(error, '复制二维码失败') - return false - } - } - - async function copyScreenshot() { - if (!screenshotBlob.value) { - return false - } - - try { - await writeImageBlobToClipboard(screenshotBlob.value, '复制截图失败') - showSuccess('截图已复制到剪贴板') - return true - } catch (error) { - notifyTencentActionError(error, '复制截图失败') - return false - } - } - - function downloadScreenshot() { - if (!screenshotBlob.value || !screenshotUrl.value) { - return false - } - - 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.click() - return true - } - - return { - form, - suggestions, - detail, - detailLoading, - createLoading, - sessionLoading, - roleConfirmLoading, - redeemLoading, - closeLoading, - loginType, - loginTypeLabel, - loginTabs, - session, - task, - order, - orderItem, - manualRequest, - activityInfo, - roleReady, - roleConfirmed, - hasSession, - qrImage, - qrFigureStyle, - qrPreviewWidth, - statusLabel, - sessionNotice, - roleFacts: adminRoleFacts, - resultFacts, - canConfirmRole, - canRedeem, - redeemBlockedReason, - redeemButtonLabel, - initButtonLabel, - scanInstruction, - screenshotEmptyTitle, - screenshotEmptyMessage, - screenshotUrl, - screenshotLoading, - reviewReady, - resultReady, - canCreateTask, - hasActiveTask, - createTaskNow, - loadSkuSuggestions, - createSessionFlow, - refreshSessionFlow, - closeSessionFlow, - switchLoginType, - confirmRoleNow, - redeemNow, - closeTaskNow, - resetCurrentTask, - handleQrImageLoad, - copyQrImage, - copyScreenshot, - downloadScreenshot, - } -} diff --git a/apps/frontend/src/composables/useClaimPage.ts b/apps/frontend/src/composables/useClaimPage.ts deleted file mode 100644 index 824fc642..00000000 --- a/apps/frontend/src/composables/useClaimPage.ts +++ /dev/null @@ -1,451 +0,0 @@ -import { computed, onBeforeUnmount, ref, watch } from 'vue' - -import { showSuccess } from '@/lib/feedback' -import { - confirmClaimRole, - createClaimSession, - fetchClaimDetail, - fetchClaimSessionSummary, - refreshClaimSession, - removeClaimSession, - redeemClaim, -} from '@/services/claim' -import type { ClaimDetailData } from '@/types/claim' -import type { TencentLoginType } from '@/types/tencent/session' - -import { notifyTencentActionError } from './tencent/session-errors' -import { - useSessionPolling, - useSessionQrDisplay, - useSessionResultFacts, - buildRedeemBlockedReason, - resolveTaskStatusLabel, - syncLoginTypeFromDetail, - mergeSessionDetail, - shouldRefreshQrImage, - shouldKeepPolling, - DEFAULT_LOGIN_TYPE, -} from './shared' - -export function useClaimPage(token: string) { - const detailLoading = ref(true) - const sessionLoading = ref(false) - const redeemLoading = ref(false) - const roleConfirmLoading = ref(false) - const loginType = ref(DEFAULT_LOGIN_TYPE) - const detail = ref(null) - const roleConfirmed = ref(false) - - // ── Derived state ────────────────────────────────────────────────── - const session = computed(() => detail.value?.session || null) - const task = computed(() => detail.value?.task || null) - const order = computed(() => detail.value?.order || null) - const orderItem = computed(() => detail.value?.orderItem || null) - const result = computed(() => detail.value?.result || null) - const tokenStatus = computed(() => detail.value?.tokenStatus || 'active') - const activityInfo = computed(() => session.value?.activityInfo || null) - 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: async () => { await refreshSessionSummary({ silent: true }) }, - pollWarningPrefix: '领取状态刷新失败', - }) - - const sessionNotice = computed( - () => pollWarningMessage.value || session.value?.notice || task.value?.lastError || '', - ) - - // ── QR display ───────────────────────────────────────────────────── - const { qrImage, qrFigureStyle, qrPreviewWidth, handleQrImageLoad } = useSessionQrDisplay({ - session, - }) - - // ── Presentation ─────────────────────────────────────────────────── - const screenshotUrl = computed(() => result.value?.screenshotUrl || '') - const showScreenshot = computed(() => Boolean(screenshotUrl.value)) - const loginTabs = [ - { value: 'qq' as const, label: 'QQ账号登录' }, - { value: 'wx' as const, label: '微信账号登录' }, - ] - - const statusLabel = computed(() => - resolveTaskStatusLabel(task.value?.status, session.value?.status, 'claim'), - ) - const initButtonLabel = computed(() => `开始初始化 ${loginTypeLabel.value} 登录`) - const scanInstruction = computed( - () => `请使用${loginTypeLabel.value}扫描二维码,并在手机上确认登录`, - ) - - // 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 || '等待扫码登录', - }, - { - label: '当前角色', - value: activityInfo.value?.role?.roleName || '后端浏览器同步中', - accent: true, - }, - { - label: '角色 ID', - value: activityInfo.value?.role?.roleId || '未识别', - }, - { - label: '任务状态', - value: statusLabel.value, - }, - ]) - - const { resultFacts } = useSessionResultFacts({ - order, - orderItem, - result, - }) - - const canConfirmRole = computed(() => - Boolean( - 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'), - ), - ) - 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 ? '等待客服兑换' : '开始兑换', - ) - const screenshotEmptyTitle = computed(() => - task.value?.status === 'redeemed' ? '本次没有可展示的截图' : '等待截图', - ) - const screenshotEmptyMessage = computed(() => - task.value?.status === 'redeemed' - ? '本次兑换可能未生成截图,或截图产物还未同步完成。' - : '领取完成后,如本次生成了结果截图,这里会展示。', - ) - - // ── Watchers ─────────────────────────────────────────────────────── - watch( - () => - [ - task.value?.taskId || '', - activityInfo.value?.nickname || '', - activityInfo.value?.role?.roleId || '', - activityInfo.value?.role?.roleName || '', - activityInfo.value?.role?.area || '', - activityInfo.value?.role?.partition || '', - ].join('|'), - () => { - roleConfirmed.value = Boolean( - task.value?.status === 'role_confirmed' || - task.value?.status === 'redeeming' || - task.value?.status === 'redeemed', - ) - }, - { immediate: true }, - ) - - // ── 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) - - detail.value = mergeSessionDetail(detail.value, response.data) - applyLoginTypeFromDetail(response.data) - restartPollingIfNeeded() - } catch (error) { - notifyTencentActionError(error, '领取详情加载失败') - } finally { - detailLoading.value = false - } - } - - async function createSessionFlow(nextLoginType = loginType.value) { - sessionLoading.value = true - resetPolling() - resetPollingWarning() - - try { - loginType.value = nextLoginType - const response = await createClaimSession(token, nextLoginType, { - forceRecreate: hasSession.value, - }) - - detail.value = mergeSessionDetail(detail.value, response.data) - applyLoginTypeFromDetail(response.data) - startPolling() - } catch (error) { - notifyTencentActionError(error, '创建领取会话失败') - } finally { - sessionLoading.value = false - } - } - - async function reloadSessionPage() { - if (!hasSession.value) { - return false - } - - sessionLoading.value = true - resetPolling() - resetPollingWarning() - - try { - const response = await refreshClaimSession(token) - - detail.value = mergeSessionDetail(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) - - detail.value = mergeSessionDetail(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 - } - - if (!silent) { - sessionLoading.value = true - resetPollingWarning() - } - - try { - const currentSession = detail.value?.session || null - const response = await fetchClaimSessionSummary(token) - - let nextDetail = mergeSessionDetail(detail.value, response.data) - detail.value = nextDetail - applyLoginTypeFromDetail(nextDetail) - - if (shouldRefreshQrImage(currentSession, response.data.session)) { - const fullResponse = await fetchClaimDetail(token) - - nextDetail = mergeSessionDetail(nextDetail, fullResponse.data) - detail.value = nextDetail - applyLoginTypeFromDetail(nextDetail) - } - - if (silent) { - resetPollingWarning() - } - - return true - } catch (error) { - if (silent) { - handleSilentPollingError(error) - } else { - notifyTencentActionError(error, '领取会话状态刷新失败') - } - - return false - } finally { - if (!silent) { - sessionLoading.value = false - } - } - } - - async function confirmRoleNow() { - if (!canConfirmRole.value) { - return - } - - roleConfirmLoading.value = true - - try { - const response = await confirmClaimRole(token) - - detail.value = mergeSessionDetail(detail.value, response.data) - roleConfirmed.value = true - showSuccess(response.msg || '角色已确认') - restartPollingIfNeeded(response.data) - } catch (error) { - notifyTencentActionError(error, '角色确认失败') - await refreshSessionSummary({ silent: true }) - } finally { - roleConfirmLoading.value = false - } - } - - async function redeemNow() { - if (!canRedeem.value) { - return - } - - redeemLoading.value = true - resetPolling() - resetPollingWarning() - - try { - const response = await redeemClaim(token) - - detail.value = mergeSessionDetail(detail.value, response.data) - showSuccess(response.msg || '兑换完成') - restartPollingIfNeeded(response.data) - } catch (error) { - notifyTencentActionError(error, '领取兑换失败') - await refreshSessionSummary({ silent: true }) - restartPollingIfNeeded() - } finally { - redeemLoading.value = false - } - } - - async function switchLoginType(nextLoginType: TencentLoginType) { - if (nextLoginType === loginType.value) { - return - } - - if (hasSession.value) { - await createSessionFlow(nextLoginType) - return - } - - loginType.value = nextLoginType - } - - // ── Lifecycle ────────────────────────────────────────────────────── - loadDetail() - - onBeforeUnmount(() => { - resetPolling() - }) - - return { - detailLoading, - sessionLoading, - redeemLoading, - roleConfirmLoading, - loginType, - loginTypeLabel, - loginTabs, - detail, - task, - order, - orderItem, - activityInfo, - hasSession, - qrImage, - qrFigureStyle, - qrPreviewWidth, - statusLabel, - session, - sessionNotice, - roleFacts: claimRoleFacts, - resultFacts, - roleConfirmed, - roleReady, - canConfirmRole, - canRedeem, - redeemBlockedReason, - redeemButtonLabel, - initButtonLabel, - scanInstruction, - screenshotEmptyTitle, - screenshotEmptyMessage, - screenshotUrl, - showScreenshot, - createSessionFlow, - reloadSessionPage, - closeSessionFlow, - refreshSessionSummary, - switchLoginType, - confirmRoleNow, - redeemNow, - handleQrImageLoad, - } -} diff --git a/apps/frontend/src/composables/useTencentBrowserSessionPage.ts b/apps/frontend/src/composables/useTencentBrowserSessionPage.ts deleted file mode 100644 index 9070d492..00000000 --- a/apps/frontend/src/composables/useTencentBrowserSessionPage.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { computed, onBeforeUnmount, ref, watch } from 'vue' - -import { showError, showSuccess } from '@/lib/feedback' -import { - createTencentBrowserSession, - refreshTencentBrowserSessionPage, - removeTencentBrowserSession, -} from '@/services/tencent/session' -import { redeemTencentBrowserSession } from '@/services/tencent/redeem' -import type { TencentBrowserSessionData, TencentLoginType } from '@/types/tencent/session' - -import { notifyTencentActionError } from './tencent/session-errors' -import { useTencentBrowserSessionPolling } from './tencent/useTencentBrowserSessionPolling' -import { useTencentBrowserSessionPresentation } from './tencent/useTencentBrowserSessionPresentation' - -const DEFAULT_LOGIN_TYPE: TencentLoginType = 'qq' - -export function useTencentBrowserSessionPage() { - const sessionLoading = ref(false) - const redeemLoading = ref(false) - const loginType = ref(DEFAULT_LOGIN_TYPE) - const session = ref(null) - const redeemCode = ref('') - const maxAttempts = ref(6) - const roleConfirmed = ref(false) - - const activityInfo = computed(() => session.value?.activityInfo || null) - const roleReady = computed(() => Boolean(activityInfo.value?.role?.ready)) - const hasSession = computed(() => Boolean(session.value?.sessionId)) - const loginTypeLabel = computed(() => (loginType.value === 'wx' ? '微信' : 'QQ')) - - const loginTabs = [ - { value: 'qq' as const, label: 'QQ账号登录' }, - { value: 'wx' as const, label: '微信账号登录' }, - ] - - const { - refreshSession, - resetPolling, - resetPollingWarning, - restartPollingIfActive, - sessionNotice, - startPolling, - } = useTencentBrowserSessionPolling({ - session, - sessionLoading, - notifyActionError: notifyTencentActionError, - }) - - const { - canRedeem, - initButtonLabel, - qrImage, - redeemBlockedReason, - redeemButtonLabel, - resultFacts, - roleFacts, - scanInstruction, - screenshotEmptyMessage, - screenshotEmptyTitle, - screenshotUrl, - statusLabel, - } = useTencentBrowserSessionPresentation({ - activityInfo, - hasSession, - loginTypeLabel, - redeemLoading, - roleConfirmed, - roleReady, - session, - sessionLoading, - sessionNotice, - }) - - watch( - () => - [ - session.value?.sessionId || '', - activityInfo.value?.nickname || '', - activityInfo.value?.role?.roleId || '', - activityInfo.value?.role?.roleName || '', - activityInfo.value?.role?.area || '', - activityInfo.value?.role?.partition || '', - ].join('|'), - () => { - roleConfirmed.value = false - }, - ) - - async function createSessionFlow(nextLoginType = loginType.value) { - resetPolling() - resetPollingWarning() - sessionLoading.value = true - - try { - loginType.value = nextLoginType - await teardownSession() - - const response = await createTencentBrowserSession({ - loginType: nextLoginType, - }) - - session.value = response.data - startPolling() - } catch (error) { - notifyTencentActionError(error, '创建浏览器会话失败') - } finally { - sessionLoading.value = false - } - } - - async function reloadSessionPage() { - if (!session.value?.sessionId) { - return - } - - resetPolling() - resetPollingWarning() - sessionLoading.value = true - - try { - const response = await refreshTencentBrowserSessionPage(session.value.sessionId) - - session.value = response.data - restartPollingIfActive(response.data) - } catch (error) { - notifyTencentActionError(error, '刷新后端页面失败') - await refreshSession({ silent: true }) - restartPollingIfActive() - } finally { - sessionLoading.value = false - } - } - - async function switchLoginType(nextLoginType: TencentLoginType) { - if (nextLoginType === loginType.value) { - return - } - - loginType.value = nextLoginType - - if (session.value?.sessionId) { - await createSessionFlow(nextLoginType) - } - } - - async function redeemNow() { - if (!session.value?.sessionId) { - showError('请先创建浏览器会话') - return - } - - if (!redeemCode.value.trim()) { - showError('请输入兑换码') - return - } - - redeemLoading.value = true - resetPolling() - resetPollingWarning() - - try { - const response = await redeemTencentBrowserSession(session.value.sessionId, { - code: redeemCode.value.trim(), - maxAttempts: maxAttempts.value, - }) - - session.value = response.data - showSuccess(response.msg || '兑换完成') - } catch (error) { - notifyTencentActionError(error, '浏览器兑换失败') - await refreshSession({ silent: true }) - } finally { - redeemLoading.value = false - } - } - - async function teardownSession() { - if (!session.value?.sessionId) { - return - } - - const sessionId = session.value.sessionId - session.value = null - - try { - await removeTencentBrowserSession(sessionId) - } catch { - // ignore close failures on local teardown - } - } - - onBeforeUnmount(() => { - resetPolling() - void teardownSession() - }) - - return { - activityInfo, - canRedeem, - teardownSession, - createSessionFlow, - hasSession, - initButtonLabel, - loginTabs, - loginType, - loginTypeLabel, - maxAttempts, - qrImage, - redeemBlockedReason, - redeemButtonLabel, - redeemCode, - redeemLoading, - redeemNow, - reloadSessionPage, - resultFacts, - roleConfirmed, - roleFacts, - scanInstruction, - screenshotEmptyMessage, - screenshotEmptyTitle, - screenshotUrl, - session, - sessionLoading, - sessionNotice, - statusLabel, - switchLoginType, - } -} diff --git a/apps/frontend/src/router/index.ts b/apps/frontend/src/router/index.ts index db9adcc0..1eead01f 100644 --- a/apps/frontend/src/router/index.ts +++ b/apps/frontend/src/router/index.ts @@ -1,22 +1,12 @@ import { createRouter, createWebHashHistory } from 'vue-router' import { getAdminRole, hasAdminSession } from '@/utils/admin-auth' -const isKuaishouMode = import.meta.env.VITE_APP_MODE === 'kuaishou' - const router = createRouter({ history: createWebHashHistory(), routes: [ { path: '/', - redirect: isKuaishouMode ? '/admin/dashboard' : '/tx/browser', - }, - { - path: '/tx', - redirect: '/tx/browser', - }, - { - path: '/tx/browser', - component: () => import('@/views/tx/TencentBrowserView.vue'), + redirect: '/admin/dashboard', }, { path: '/claim/:token', @@ -59,10 +49,6 @@ const router = createRouter({ path: 'tasks', component: () => import('@/views/admin/tasks/AdminTasksView.vue'), }, - { - path: 'manual-redeem', - component: () => import('@/views/admin/manual-redeem/AdminManualRedeemView.vue'), - }, { path: 'tasks/:taskId', component: () => import('@/views/admin/tasks/AdminTaskDetailView.vue'), diff --git a/apps/frontend/src/services/admin/index.ts b/apps/frontend/src/services/admin/index.ts index 5c0cdc09..11902e5f 100644 --- a/apps/frontend/src/services/admin/index.ts +++ b/apps/frontend/src/services/admin/index.ts @@ -6,6 +6,5 @@ export * from './platform-config' export * from './orders' export * from './tasks' export * from './inventory' -export * from './manual-redeem' export * from './message-deliveries' export * from './webhook-events' diff --git a/apps/frontend/src/services/admin/manual-redeem.ts b/apps/frontend/src/services/admin/manual-redeem.ts deleted file mode 100644 index 6941c52f..00000000 --- a/apps/frontend/src/services/admin/manual-redeem.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { apiDelete, apiGet, apiPost } from '@/lib/http' -import type { AdminInventorySkuSuggestion, AdminManualRedeemDetail } from '@/types/admin' - -export function fetchAdminManualRedeemSkuSuggestions(params?: Record) { - return apiGet<{ items: AdminInventorySkuSuggestion[] }>( - '/api/v1/admin/manual-redeem/sku-suggestions', - params, - ) -} - -export function createAdminManualRedeemTask(payload: { - proofValue: string - skuCode: string - skuName?: string - remark?: string -}) { - return apiPost('/api/v1/admin/manual-redeem', payload) -} - -export function fetchAdminManualRedeemDetail(taskId: number | string) { - return apiGet(`/api/v1/admin/manual-redeem/${taskId}`) -} - -export function createAdminManualRedeemSession( - taskId: number | string, - payload: { loginType: string; forceRecreate?: boolean }, -) { - return apiPost(`/api/v1/admin/manual-redeem/${taskId}/session`, payload) -} - -export function fetchAdminManualRedeemSessionSummary(taskId: number | string) { - return apiGet(`/api/v1/admin/manual-redeem/${taskId}/session/summary`) -} - -export function refreshAdminManualRedeemSession(taskId: number | string) { - return apiPost( - `/api/v1/admin/manual-redeem/${taskId}/session/refresh`, - {}, - ) -} - -export function removeAdminManualRedeemSession(taskId: number | string) { - return apiDelete(`/api/v1/admin/manual-redeem/${taskId}/session`) -} - -export function confirmAdminManualRedeemRole(taskId: number | string) { - return apiPost(`/api/v1/admin/manual-redeem/${taskId}/confirm-role`, {}) -} - -export function redeemAdminManualRedeem(taskId: number | string) { - return apiPost(`/api/v1/admin/manual-redeem/${taskId}/redeem`, {}) -} - -export function closeAdminManualRedeemTask(taskId: number | string) { - return apiPost(`/api/v1/admin/manual-redeem/${taskId}/close`, {}) -} diff --git a/apps/frontend/src/services/admin/platform-config/agiso.ts b/apps/frontend/src/services/admin/platform-config/agiso.ts deleted file mode 100644 index cc7ee38e..00000000 --- a/apps/frontend/src/services/admin/platform-config/agiso.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { apiGet, apiPost } from '@/lib/http' -import type { - AdminAgisoMessagingDefaults, - AdminAgisoObservedShopItem, - AdminAgisoShopConfigItem, -} from '@/types/admin' - -export function fetchAdminAgisoShopConfigs() { - return apiGet<{ - filePath: string - defaults: AdminAgisoMessagingDefaults - shops: AdminAgisoShopConfigItem[] - observedShops: AdminAgisoObservedShopItem[] - }>('/api/v1/admin/platform-config/agiso-shops') -} - -export function saveAdminAgisoShopConfigs(payload: { - defaults: AdminAgisoMessagingDefaults - shops: Array> -}) { - return apiPost<{ - filePath: string - defaults: AdminAgisoMessagingDefaults - shops: AdminAgisoShopConfigItem[] - }>('/api/v1/admin/platform-config/agiso-shops', payload) -} diff --git a/apps/frontend/src/services/admin/platform-config/fulfillment-bindings.ts b/apps/frontend/src/services/admin/platform-config/fulfillment-bindings.ts deleted file mode 100644 index 918f347b..00000000 --- a/apps/frontend/src/services/admin/platform-config/fulfillment-bindings.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { apiGet, apiPost } from '@/lib/http' -import type { - AdminFulfillmentBindingConfigItem, - AdminFulfillmentLookupResult, - AdminObservedProductItem, -} from '@/types/admin' - -export function fetchAdminFulfillmentBindingConfigs() { - return apiGet<{ - filePath: string - bindings: AdminFulfillmentBindingConfigItem[] - observedProducts: AdminObservedProductItem[] - }>('/api/v1/admin/platform-config/fulfillment-bindings') -} - -export function lookupAdminFulfillmentBindingOrder(payload: { - provider?: string - platform?: string - shopId: string - platformOrderId: string -}) { - return apiPost( - '/api/v1/admin/platform-config/fulfillment-bindings/lookup-order', - payload, - ) -} - -export function saveAdminFulfillmentBindingConfigs(payload: { - bindings: Array> -}) { - return apiPost<{ - filePath: string - bindings: AdminFulfillmentBindingConfigItem[] - }>('/api/v1/admin/platform-config/fulfillment-bindings', payload) -} diff --git a/apps/frontend/src/services/admin/platform-config/index.ts b/apps/frontend/src/services/admin/platform-config/index.ts index f7260eb5..3a29bf1b 100644 --- a/apps/frontend/src/services/admin/platform-config/index.ts +++ b/apps/frontend/src/services/admin/platform-config/index.ts @@ -1,8 +1,6 @@ -export * from './agiso' export * from './notifications' export * from './scheduled-jobs' export * from './ninetyone' export * from './kuaishou-eticket' export * from './cloudtentacles' -export * from './fulfillment-bindings' export * from './kuaishou-cloud-fulfillment' diff --git a/apps/frontend/src/services/claim.ts b/apps/frontend/src/services/claim.ts index ddeb6d58..ad6f1899 100644 --- a/apps/frontend/src/services/claim.ts +++ b/apps/frontend/src/services/claim.ts @@ -1,26 +1,10 @@ -import { apiDelete, apiGet, apiPost } from '@/lib/http' +import { apiGet, apiPost } from '@/lib/http' import type { ClaimDetailData } from '@/types/claim' -import type { TencentLoginType } from '@/types/tencent/session' export function fetchClaimDetail(token: string) { return apiGet(`/api/v1/claim/${token}`) } -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 verifyKuaishouCloudClaimTicket( token: string, payload: { @@ -37,19 +21,3 @@ export function confirmKuaishouCloudClaimRole(token: string) { export function redeemKuaishouCloudClaim(token: string) { return apiPost(`/api/v1/claim/${token}/kuaishou-cloud/redeem`, {}) } - -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 }) -} - -export function redeemClaim(token: string) { - return apiPost(`/api/v1/claim/${token}/redeem`, {}) -} diff --git a/apps/frontend/src/services/tencent/artifacts.ts b/apps/frontend/src/services/tencent/artifacts.ts deleted file mode 100644 index 0387e34d..00000000 --- a/apps/frontend/src/services/tencent/artifacts.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function buildTencentBrowserSessionScreenshotUrl(sessionId: string, cacheKey = '') { - const suffix = cacheKey ? `?t=${encodeURIComponent(cacheKey)}` : '' - return `/api/v1/tencent/browser/session/${sessionId}/screenshot${suffix}` -} diff --git a/apps/frontend/src/services/tencent/redeem.ts b/apps/frontend/src/services/tencent/redeem.ts deleted file mode 100644 index f63ad7c9..00000000 --- a/apps/frontend/src/services/tencent/redeem.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { apiPost } from '@/lib/http' -import type { TencentBrowserSessionData } from '@/types/tencent/session' - -export interface TencentBrowserRedeemPayload { - code: string - maxAttempts?: number -} - -export function redeemTencentBrowserSession( - sessionId: string, - payload: TencentBrowserRedeemPayload, -) { - return apiPost( - `/api/v1/tencent/browser/session/${sessionId}/redeem`, - payload, - ) -} diff --git a/apps/frontend/src/services/tencent/session.ts b/apps/frontend/src/services/tencent/session.ts deleted file mode 100644 index a1864052..00000000 --- a/apps/frontend/src/services/tencent/session.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { apiDelete, apiGet, apiPost } from '@/lib/http' -import type { - TencentBrowserSessionData, - TencentBrowserSessionSummaryData, - TencentLoginType, -} from '@/types/tencent/session' - -export interface CreateTencentBrowserSessionPayload { - loginType?: TencentLoginType -} - -export function createTencentBrowserSession(payload?: CreateTencentBrowserSessionPayload) { - return apiPost( - '/api/v1/tencent/browser/session', - payload as Record | undefined, - ) -} - -export function fetchTencentBrowserSession(sessionId: string) { - return apiGet(`/api/v1/tencent/browser/session/${sessionId}`) -} - -export function fetchTencentBrowserSessionSummary(sessionId: string) { - return apiGet( - `/api/v1/tencent/browser/session/${sessionId}/summary`, - ) -} - -export function refreshTencentBrowserSessionPage(sessionId: string) { - return apiPost(`/api/v1/tencent/browser/session/${sessionId}/refresh`) -} - -export function removeTencentBrowserSession(sessionId: string) { - return apiDelete>(`/api/v1/tencent/browser/session/${sessionId}`) -} diff --git a/apps/frontend/src/styles/admin-config-pages.css b/apps/frontend/src/styles/admin-config-pages.css index a582ab35..80a06eca 100644 --- a/apps/frontend/src/styles/admin-config-pages.css +++ b/apps/frontend/src/styles/admin-config-pages.css @@ -1,6 +1,6 @@ /* ============================================================ 管理员配置页面共享结构样式 - 引入: AdminFulfillmentBindings.css, AdminKuaishouCloudFulfillment.css + 引入: AdminKuaishouCloudFulfillment.css 各页面自行覆盖颜色、圆角、主题属性 ============================================================ */ diff --git a/apps/frontend/src/types/admin/fulfillment.ts b/apps/frontend/src/types/admin/fulfillment.ts deleted file mode 100644 index 8f674424..00000000 --- a/apps/frontend/src/types/admin/fulfillment.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { AdminObservedProductItem } from './platform-config/fulfillment-bindings' - -export interface AdminFulfillmentLookupOrder { - provider: string - platform: string - shopId: string - shopName: string - platformOrderId: string - buyerName: string - totalAmount: string - totalAmountFen: number - paidAt: string | null - enriched: boolean - enrichReason: string - errorMessage: string -} - -export interface AdminFulfillmentLookupItem extends AdminObservedProductItem { - lineId: string - itemTitle: string - quantity: number -} - -export interface AdminFulfillmentLookupResult { - order: AdminFulfillmentLookupOrder - items: AdminFulfillmentLookupItem[] -} diff --git a/apps/frontend/src/types/admin/index.ts b/apps/frontend/src/types/admin/index.ts index 5c4c2761..c354ca19 100644 --- a/apps/frontend/src/types/admin/index.ts +++ b/apps/frontend/src/types/admin/index.ts @@ -34,9 +34,6 @@ export type { // Inventory types export type { AdminInventorySkuSuggestion, AdminInventoryItemListItem } from './inventory' -// Manual redeem types -export type { AdminManualRedeemDetail } from './manual-redeem' - // Webhook events types export type { AdminWebhookEventListItem, @@ -47,18 +44,8 @@ export type { // Message deliveries types export type { AdminMessageDeliveryListItem } from './message-deliveries' -// Fulfillment types -export type { - AdminFulfillmentLookupOrder, - AdminFulfillmentLookupItem, - AdminFulfillmentLookupResult, -} from './fulfillment' - // Platform config types export type { - AdminAgisoShopConfigItem, - AdminAgisoMessagingDefaults, - AdminAgisoObservedShopItem, AdminNotificationBarkRecipient, AdminNotificationWpushRecipient, AdminNotificationConfig, @@ -92,8 +79,6 @@ export type { AdminCloudtentaclesValidateSessionResult, AdminCloudtentaclesSkuItem, AdminCloudtentaclesSkuListResult, - AdminFulfillmentBindingConfigItem, - AdminObservedProductItem, AdminKuaishouCloudFulfillmentItem, AdminKuaishouCloudFulfillmentConfig, } from './platform-config' diff --git a/apps/frontend/src/types/admin/manual-redeem.ts b/apps/frontend/src/types/admin/manual-redeem.ts deleted file mode 100644 index 026f53e0..00000000 --- a/apps/frontend/src/types/admin/manual-redeem.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ClaimDetailData } from '../claim' - -export interface AdminManualRedeemDetail extends ClaimDetailData { - manualRequest: { - sourceType: string - proofValue: string - remark: string - } -} diff --git a/apps/frontend/src/types/admin/platform-config/agiso.ts b/apps/frontend/src/types/admin/platform-config/agiso.ts deleted file mode 100644 index 7288a5e1..00000000 --- a/apps/frontend/src/types/admin/platform-config/agiso.ts +++ /dev/null @@ -1,26 +0,0 @@ -export interface AdminAgisoShopConfigItem { - shopId: string - shopName: string - AccessToken: string - accessTokenMasked: string - enabled: boolean | null - messageTemplate: string - autoDeliveryMessageTemplate: string - appSecretConfigured: boolean - apiVersion: string - sendMessageEndpoint: string -} - -export interface AdminAgisoMessagingDefaults { - messageTemplate: string - autoDeliveryMessageTemplate: string -} - -export interface AdminAgisoObservedShopItem { - shopId: string - detectedShopName: string - displayShopName: string - latestSeenAt: string | null - webhookEventCount: number - configured: boolean -} diff --git a/apps/frontend/src/types/admin/platform-config/fulfillment-bindings.ts b/apps/frontend/src/types/admin/platform-config/fulfillment-bindings.ts deleted file mode 100644 index 3aa383ea..00000000 --- a/apps/frontend/src/types/admin/platform-config/fulfillment-bindings.ts +++ /dev/null @@ -1,36 +0,0 @@ -export interface AdminFulfillmentBindingConfigItem { - provider: string - platform: string - shopId: string - shopName: string - skuCode: string - skuName: string - profileKey: string - enabled: boolean - priority: number - config: Record - match: { - externalSkuCode: string - externalItemId: string - externalSkuName: string - config: Record - } -} - -export interface AdminObservedProductItem { - provider: string - platform: string - shopId: string - shopName: string - externalItemId: string - externalSkuCode: string - externalSkuName: string - latestSeenAt: string | null - orderItemCount: number - configured: boolean - matchedBinding: null | { - skuCode: string - skuName: string - profileKey: string - } -} diff --git a/apps/frontend/src/types/admin/platform-config/index.ts b/apps/frontend/src/types/admin/platform-config/index.ts index 62af5fb4..bdded7ba 100644 --- a/apps/frontend/src/types/admin/platform-config/index.ts +++ b/apps/frontend/src/types/admin/platform-config/index.ts @@ -1,9 +1,3 @@ -export type { - AdminAgisoShopConfigItem, - AdminAgisoMessagingDefaults, - AdminAgisoObservedShopItem, -} from './agiso' - export type { AdminNotificationBarkRecipient, AdminNotificationWpushRecipient, @@ -52,11 +46,6 @@ export type { AdminCloudtentaclesSkuListResult, } from './cloudtentacles' -export type { - AdminFulfillmentBindingConfigItem, - AdminObservedProductItem, -} from './fulfillment-bindings' - export type { AdminKuaishouCloudFulfillmentItem, AdminKuaishouCloudFulfillmentConfig, diff --git a/apps/frontend/src/types/claim.ts b/apps/frontend/src/types/claim.ts index 59b7b087..e13ffc6b 100644 --- a/apps/frontend/src/types/claim.ts +++ b/apps/frontend/src/types/claim.ts @@ -1,5 +1,3 @@ -import type { TencentBrowserSessionData, TencentBrowserSessionSummaryData } from './tencent/session' - export type ClaimTokenStatus = 'active' | 'used' | 'expired' | 'revoked' | (string & {}) export type ClaimTaskStatus = @@ -128,11 +126,11 @@ export interface ClaimKuaishouCloudFlowInfo { export interface ClaimDetailData { tokenStatus: ClaimTokenStatus claimUrl: string - flowType: 'tencent_claim' | 'kuaishou_cloud' | (string & {}) + flowType: 'kuaishou_cloud' | 'kuaishou_ct_assisted' | (string & {}) task: ClaimTaskInfo order: ClaimOrderInfo orderItem: ClaimOrderItemInfo - session: TencentBrowserSessionData | TencentBrowserSessionSummaryData | null + session: unknown | null kuaishouCloudFulfillment: ClaimKuaishouCloudFlowInfo | null result: ClaimResultInfo | null } diff --git a/apps/frontend/src/types/tencent/activity.ts b/apps/frontend/src/types/tencent/activity.ts deleted file mode 100644 index dd66aac3..00000000 --- a/apps/frontend/src/types/tencent/activity.ts +++ /dev/null @@ -1,51 +0,0 @@ -export interface TencentBrowserSessionArtifacts { - hasQrImage: boolean - hasScreenshot: boolean -} - -export interface TencentBrowserReviewInfo { - capturedAt: string - roleId: string - roleName: string -} - -export interface TencentBrowserRoleInfo { - ready: boolean - roleName: string - roleId: string - area: string - partition: string - platId: string - md5str: string - checkparam: string -} - -export interface TencentBrowserFormInfo { - cdkeyInputId: string - cdkeyValue: string - verifyInputId: string - verifyValue: string - verifyImgId: string - submitId: string -} - -export interface TencentBrowserVerifyInfo { - visible: boolean - src: string - naturalWidth: number - naturalHeight: number -} - -export interface TencentBrowserPopupInfo { - visible: boolean - text: string - detail: string -} - -export interface TencentBrowserActivityInfo { - nickname: string - role: TencentBrowserRoleInfo - form: TencentBrowserFormInfo - verify: TencentBrowserVerifyInfo - popup: TencentBrowserPopupInfo -} diff --git a/apps/frontend/src/types/tencent/redeem.ts b/apps/frontend/src/types/tencent/redeem.ts deleted file mode 100644 index 4df77a64..00000000 --- a/apps/frontend/src/types/tencent/redeem.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface TencentBrowserRedeemAttempt { - attempt: number - redeem?: Record | null -} - -export interface TencentBrowserRedeemResult { - code: string - area: string - attempts: TencentBrowserRedeemAttempt[] - final?: TencentBrowserRedeemAttempt | null - finishedAt: string -} diff --git a/apps/frontend/src/types/tencent/session.ts b/apps/frontend/src/types/tencent/session.ts deleted file mode 100644 index 908bd2f0..00000000 --- a/apps/frontend/src/types/tencent/session.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { - TencentBrowserActivityInfo, - TencentBrowserReviewInfo, - TencentBrowserSessionArtifacts, -} from './activity' -import type { TencentBrowserRedeemResult } from './redeem' - -export type TencentLoginType = 'qq' | 'wx' - -export type TencentBrowserSessionStatus = - | 'waiting_scan' - | 'scanned' - | 'expired' - | 'logged_in' - | 'ready_to_redeem' - | 'redeeming' - | 'redeemed' - | 'closed' - | 'failed' - | (string & {}) - -export interface TencentBrowserSessionData { - sessionId: string - loginType: TencentLoginType | string - activityUrl: string - status: TencentBrowserSessionStatus - notice: string - createdAt: string - updatedAt: string - expiresAt: string - qrImageBase64: string - qrUpdatedAt: string - credentialReady: boolean - activityInfo?: TencentBrowserActivityInfo | null - review?: TencentBrowserReviewInfo | null - redeem?: TencentBrowserRedeemResult | null - artifacts: TencentBrowserSessionArtifacts -} - -export interface TencentBrowserSessionSummaryData extends Omit< - TencentBrowserSessionData, - 'qrImageBase64' -> { - qrImageBase64?: string -} diff --git a/apps/frontend/src/views/NotFoundView.vue b/apps/frontend/src/views/NotFoundView.vue index d6f04c77..50882789 100644 --- a/apps/frontend/src/views/NotFoundView.vue +++ b/apps/frontend/src/views/NotFoundView.vue @@ -3,8 +3,8 @@

404

页面不存在

-

当前前端只保留腾讯浏览器兑换页,这个地址没有对应页面。

- 前往兑换入口 +

当前快手轻量前端没有对应页面。

+ 前往后台
diff --git a/apps/frontend/src/views/admin/fulfillment/AdminFulfillmentConfigHubView.vue b/apps/frontend/src/views/admin/fulfillment/AdminFulfillmentConfigHubView.vue index b2f58824..e4e151b6 100644 --- a/apps/frontend/src/views/admin/fulfillment/AdminFulfillmentConfigHubView.vue +++ b/apps/frontend/src/views/admin/fulfillment/AdminFulfillmentConfigHubView.vue @@ -1,45 +1,15 @@ @@ -48,21 +18,4 @@ const activeTab = computed({ display: grid; gap: var(--space-4); } - -:deep(.hub-tabs .el-tabs__header) { - margin-bottom: 0; -} - -:deep(.hub-tabs .el-tabs__content) { - padding-top: var(--space-4); -} - -:deep(.hub-tabs .el-tabs__item) { - font-weight: 500; - font-size: var(--text-base); -} - -:deep(.hub-tabs .el-tabs__item.is-active) { - font-weight: 600; -} diff --git a/apps/frontend/src/views/admin/fulfillment/bindings/AdminFulfillmentBindings.css b/apps/frontend/src/views/admin/fulfillment/bindings/AdminFulfillmentBindings.css deleted file mode 100644 index fbccea8a..00000000 --- a/apps/frontend/src/views/admin/fulfillment/bindings/AdminFulfillmentBindings.css +++ /dev/null @@ -1,290 +0,0 @@ -.admin-panel { - display: grid; - gap: var(--space-4); -} - -.section-card { - border-radius: var(--radius-lg); - border: 1px solid var(--border-default); - background: var(--bg-surface); -} - -.section-card :deep(.el-card__header) { - background: var(--bg-surface); - border-bottom: 1px solid var(--border-default); - padding: var(--space-4) var(--space-5); -} - -.section-card :deep(.el-card__body) { - padding: var(--space-5); -} - -.card-header { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.card-header--split { - flex-direction: row; - justify-content: space-between; - align-items: flex-start; -} - -.card-title { - display: block; - font-size: var(--text-lg); - font-weight: 600; - color: var(--text-primary); - margin-bottom: var(--space-1); -} - -.card-desc { - display: block; - font-size: var(--text-sm); - color: var(--text-secondary); - line-height: 1.5; -} - -.overview-card { - display: grid; - gap: var(--space-5); -} - -.overview-file { - margin-top: var(--space-1); -} - -.overview-file code { - color: var(--text-primary); - word-break: break-all; - font-size: var(--text-sm); -} - -.overview-stats { - display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); - gap: var(--space-4); -} - -.overview-stat { - min-width: 0; - padding: var(--space-4); - border: 1px solid var(--border-default); - border-radius: var(--radius-md); - background: var(--bg-surface); -} - -.overview-stat :deep(.el-statistic__head) { - margin-bottom: var(--space-2); - color: var(--text-secondary); - font-size: var(--text-xs); - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.overview-stat :deep(.el-statistic__content) { - color: var(--text-primary); - font-size: var(--text-2xl); - font-weight: 700; -} - -.overview-notes, -.section-mini-stats { - display: flex; - flex-wrap: wrap; - gap: var(--space-2); -} - -.lookup-toolbar, -.lookup-summary { - display: grid; - gap: var(--space-4); -} - -.lookup-toolbar { - grid-template-columns: 96px 110px minmax(140px, 1fr) minmax(180px, 1.2fr) 136px; - align-items: end; -} - -.lookup-summary { - margin-top: var(--space-1); -} - -.field-help { - display: block; - margin-top: var(--space-2); - color: var(--text-secondary); - font-size: var(--text-xs); - line-height: 1.6; -} - -.field-wide { - grid-column: 1 / -1; -} - -.text-input { - width: 100%; -} - -.admin-panel :deep(.text-input--invalid .el-input__wrapper), -.admin-panel :deep(.text-input--invalid .el-select__wrapper) { - border-color: var(--color-danger); - box-shadow: 0 0 0 1px var(--color-danger) inset; -} - -.admin-panel :deep(.el-form-item) { - margin-bottom: 0; -} - -.admin-panel :deep(.el-form-item__label) { - color: var(--text-secondary); - font-size: var(--text-sm); - font-weight: 600; - line-height: 1.5; - margin-bottom: var(--space-2); -} - -.cell-subtle { - color: var(--text-secondary); - display: block; - font-size: var(--text-xs); - line-height: 1.6; -} - -.cell-stack { - display: flex; - flex-direction: column; - gap: var(--space-1); -} - -.binding-card { - margin-top: var(--space-3); - background: var(--bg-surface); - border: 1px solid var(--border-default); - border-radius: var(--radius-md); -} - -.binding-card :deep(.el-card__header) { - padding: var(--space-3) var(--space-4); - background: var(--bg-surface); - border-bottom: 1px solid var(--border-default); -} - -.binding-card :deep(.el-card__body) { - display: grid; - gap: var(--space-4); - padding: var(--space-4); -} - -.binding-card--invalid { - border-color: var(--color-danger); - box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.08); -} - -.binding-header { - display: flex; - justify-content: space-between; - gap: var(--space-4); - align-items: flex-start; -} - -.binding-summary { - display: grid; - gap: var(--space-2); - flex: 1; -} - -.binding-title-row { - display: flex; - gap: var(--space-2); - align-items: center; - flex-wrap: wrap; - color: var(--text-primary); - font-size: var(--text-base); -} - -.binding-subtle { - color: var(--text-secondary); - font-size: var(--text-sm); - line-height: 1.5; -} - -.binding-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: var(--space-4) var(--space-5); -} - -.binding-collapsed-preview { - color: var(--text-secondary); - font-size: var(--text-sm); -} - -.binding-collapsed-preview :deep(.el-descriptions__label) { - font-weight: 600; -} - -.binding-actions { - display: flex; - justify-content: space-between; - gap: var(--space-3); - align-items: center; - padding-top: var(--space-3); - border-top: 1px solid var(--border-default); -} - -@media (max-width: 900px) { - .card-header--split, - .binding-actions, - .binding-header { - display: grid; - grid-template-columns: 1fr; - align-items: stretch; - gap: var(--space-3); - } - - .overview-stats, - .lookup-toolbar, - .binding-grid { - grid-template-columns: 1fr; - } - - .binding-collapsed-preview :deep(.el-descriptions__body .el-descriptions__table) { - display: block; - } - - .section-card :deep(.el-card__header), - .section-card :deep(.el-card__body) { - padding: var(--space-4); - } - - .binding-card :deep(.el-card__header), - .binding-card :deep(.el-card__body) { - padding: var(--space-3); - } -} - -.data-table { - margin-top: var(--space-4); -} - -.data-table :deep(.el-table__header th) { - background: var(--bg-surface); - color: var(--text-secondary); - font-weight: 600; - font-size: var(--text-sm); -} - -.data-table :deep(.el-table__body td) { - padding: var(--space-3) var(--space-2); -} - -.mt-4 { - margin-top: var(--space-4); -} - -.w-full { - width: 100%; -} diff --git a/apps/frontend/src/views/admin/fulfillment/bindings/AdminFulfillmentBindingsView.vue b/apps/frontend/src/views/admin/fulfillment/bindings/AdminFulfillmentBindingsView.vue deleted file mode 100644 index a3cf2b6e..00000000 --- a/apps/frontend/src/views/admin/fulfillment/bindings/AdminFulfillmentBindingsView.vue +++ /dev/null @@ -1,73 +0,0 @@ - - - - - diff --git a/apps/frontend/src/views/admin/fulfillment/bindings/components/AdminBindingRuleCard.vue b/apps/frontend/src/views/admin/fulfillment/bindings/components/AdminBindingRuleCard.vue deleted file mode 100644 index 5f10455c..00000000 --- a/apps/frontend/src/views/admin/fulfillment/bindings/components/AdminBindingRuleCard.vue +++ /dev/null @@ -1,213 +0,0 @@ - - - - - diff --git a/apps/frontend/src/views/admin/fulfillment/bindings/components/AdminBindingsLookupSection.vue b/apps/frontend/src/views/admin/fulfillment/bindings/components/AdminBindingsLookupSection.vue deleted file mode 100644 index 397a2f38..00000000 --- a/apps/frontend/src/views/admin/fulfillment/bindings/components/AdminBindingsLookupSection.vue +++ /dev/null @@ -1,156 +0,0 @@ - - - - - diff --git a/apps/frontend/src/views/admin/fulfillment/bindings/components/AdminBindingsObservedSection.vue b/apps/frontend/src/views/admin/fulfillment/bindings/components/AdminBindingsObservedSection.vue deleted file mode 100644 index d1fe9e15..00000000 --- a/apps/frontend/src/views/admin/fulfillment/bindings/components/AdminBindingsObservedSection.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - - - diff --git a/apps/frontend/src/views/admin/fulfillment/bindings/components/AdminBindingsOverviewSection.vue b/apps/frontend/src/views/admin/fulfillment/bindings/components/AdminBindingsOverviewSection.vue deleted file mode 100644 index 8c711113..00000000 --- a/apps/frontend/src/views/admin/fulfillment/bindings/components/AdminBindingsOverviewSection.vue +++ /dev/null @@ -1,48 +0,0 @@ - - - - - diff --git a/apps/frontend/src/views/admin/fulfillment/bindings/components/AdminBindingsRuleSection.vue b/apps/frontend/src/views/admin/fulfillment/bindings/components/AdminBindingsRuleSection.vue deleted file mode 100644 index a021340e..00000000 --- a/apps/frontend/src/views/admin/fulfillment/bindings/components/AdminBindingsRuleSection.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - - - diff --git a/apps/frontend/src/views/admin/fulfillment/bindings/composables/types.ts b/apps/frontend/src/views/admin/fulfillment/bindings/composables/types.ts deleted file mode 100644 index bc5c17c0..00000000 --- a/apps/frontend/src/views/admin/fulfillment/bindings/composables/types.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { AdminObservedProductItem } from '@/types/admin' - -export type EditableBinding = { - id: string - provider: string - platform: string - shopId: string - shopName: string - skuCode: string - skuName: string - profileKey: string - enabled: boolean - priority: number - externalSkuCode: string - externalItemId: string - externalSkuName: string - resolvedSkuName: string -} - -export type SaveBindingPayload = { - provider: string - platform: string - shopId: string - shopName: string - skuCode: string - skuName: string - profileKey: string - enabled: boolean - priority: number - match: { - externalSkuCode: string - externalItemId: string - externalSkuName: string - config: { - resolvedSkuName: string - } - } -} - -export type ValidationField = 'skuCode' | 'match' | 'profileKey' | 'shopId' - -export type ValidationState = { - bindingId: string - field: ValidationField - message: string -} | null - -export type ImportableProductCandidate = Pick< - AdminObservedProductItem, - | 'provider' - | 'platform' - | 'shopId' - | 'shopName' - | 'externalSkuCode' - | 'externalItemId' - | 'externalSkuName' -> & {} - -export const PROFILE_OPTIONS = [ - { label: '腾讯领取兑换', value: 'tencent_claim_redeem' }, - { label: '腾讯领取兑换(半自动+人工)', value: 'tencent_claim_assisted' }, - { label: '快手 Cloud 履约', value: 'kuaishou_ct_assisted' }, - { label: '人工发货', value: 'manual_review' }, -] - -export const VALID_PROFILE_KEYS = new Set(PROFILE_OPTIONS.map((item) => item.value)) diff --git a/apps/frontend/src/views/admin/fulfillment/bindings/composables/useFulfillmentBindings.ts b/apps/frontend/src/views/admin/fulfillment/bindings/composables/useFulfillmentBindings.ts deleted file mode 100644 index b75019d8..00000000 --- a/apps/frontend/src/views/admin/fulfillment/bindings/composables/useFulfillmentBindings.ts +++ /dev/null @@ -1,453 +0,0 @@ -import { computed, nextTick, ref } from 'vue' - -import { showSuccess } from '@/lib/feedback' -import { - fetchAdminFulfillmentBindingConfigs, - saveAdminFulfillmentBindingConfigs, -} from '@/services/admin' -import type { - AdminFulfillmentBindingConfigItem, - AdminObservedProductItem, -} from '@/types/admin' -import { hasAdminRole } from '@/utils/admin-auth' - -import { - VALID_PROFILE_KEYS, - type EditableBinding, - type ImportableProductCandidate, - type SaveBindingPayload, - type ValidationField, - type ValidationState, -} from './types' - -export function useFulfillmentBindings() { - const loading = ref(true) - const saving = ref(false) - const errorMessage = ref('') - const filePath = ref('') - const bindings = ref([]) - const observedProducts = ref([]) - const validationState = ref(null) - const collapsedBindingIds = ref([]) - - // ── computed ────────────────────────────────────────── - - const ruleMetrics = computed(() => { - const completedCount = bindings.value.filter(isEditableBindingComplete).length - const manualCount = bindings.value.filter( - (item) => item.profileKey.trim() === 'manual_review', - ).length - const assistedCount = bindings.value.filter( - (item) => item.profileKey.trim() === 'tencent_claim_assisted', - ).length - const kuaishouCloudCount = bindings.value.filter( - (item) => item.profileKey.trim() === 'kuaishou_ct_assisted', - ).length - - return { - totalCount: bindings.value.length, - completedCount, - draftCount: Math.max(bindings.value.length - completedCount, 0), - manualCount, - assistedCount, - kuaishouCloudCount, - } - }) - - const pendingObservedCount = computed( - () => observedProducts.value.filter((item) => !item.configured).length, - ) - - // ── factories ──────────────────────────────────────── - - function createEmptyBinding(): EditableBinding { - return { - id: crypto.randomUUID(), - provider: 'agiso', - platform: '', - shopId: '', - shopName: '', - skuCode: '', - skuName: '', - profileKey: 'manual_review', - enabled: true, - priority: 100, - externalSkuCode: '', - externalItemId: '', - externalSkuName: '', - resolvedSkuName: '', - } - } - - function mapEditableBinding(item: AdminFulfillmentBindingConfigItem): EditableBinding { - return { - id: crypto.randomUUID(), - provider: item.provider || 'agiso', - platform: item.platform, - shopId: item.shopId, - shopName: item.shopName || '', - skuCode: item.skuCode, - skuName: item.skuName, - profileKey: item.profileKey || 'manual_review', - enabled: item.enabled !== false, - priority: item.priority || 100, - externalSkuCode: item.match.externalSkuCode, - externalItemId: item.match.externalItemId, - externalSkuName: item.match.externalSkuName, - resolvedSkuName: String(item.match.config?.resolvedSkuName || ''), - } - } - - // ── validation ──────────────────────────────────────── - - function hasMatchCondition( - item: Pick, - ) { - return Boolean( - item.externalSkuCode.trim() || item.externalItemId.trim() || item.externalSkuName.trim(), - ) - } - - function isEditableBindingComplete(item: EditableBinding) { - return Boolean( - item.skuCode.trim() && - hasMatchCondition(item) && - VALID_PROFILE_KEYS.has(item.profileKey.trim() || 'manual_review'), - ) - } - - // ── collapse state ──────────────────────────────────── - - function buildCollapsedIds(items: EditableBinding[]) { - return items.filter(isEditableBindingComplete).map((item) => item.id) - } - - function isBindingCollapsed(bindingId: string) { - return collapsedBindingIds.value.includes(bindingId) - } - - function setBindingCollapsed(bindingId: string, collapsed: boolean) { - const next = new Set(collapsedBindingIds.value) - - if (collapsed) { - next.add(bindingId) - } else { - next.delete(bindingId) - } - - collapsedBindingIds.value = Array.from(next) - } - - function toggleBindingCollapsed(bindingId: string) { - setBindingCollapsed(bindingId, !isBindingCollapsed(bindingId)) - } - - // ── display helpers ─────────────────────────────────── - - function getBindingTitle(binding: EditableBinding, index: number) { - const internalName = binding.skuName.trim() - const externalName = binding.externalSkuName.trim() - const skuCode = binding.skuCode.trim() - - return internalName || externalName || skuCode || `规则 ${index + 1}` - } - - function getBindingStatusLabel(binding: EditableBinding) { - return isEditableBindingComplete(binding) ? '已完成' : '待完善' - } - - function getBindingSummary(binding: EditableBinding) { - const parts = [ - binding.provider.trim() || 'agiso', - binding.platform.trim() || '未选平台', - binding.shopName.trim() || binding.shopId.trim() || '跨店铺', - binding.profileKey.trim() || 'manual_review', - ] - - return parts.join(' / ') - } - - // ── data loading ────────────────────────────────────── - - async function loadConfigs() { - if (!hasAdminRole('admin')) { - loading.value = false - return - } - - loading.value = true - errorMessage.value = '' - - try { - const response = await fetchAdminFulfillmentBindingConfigs() - filePath.value = response.data.filePath - const nextBindings = response.data.bindings.map(mapEditableBinding) - bindings.value = nextBindings - collapsedBindingIds.value = buildCollapsedIds(nextBindings) - observedProducts.value = response.data.observedProducts - } catch (error) { - errorMessage.value = error instanceof Error ? error.message : '读取履约配置失败' - } finally { - loading.value = false - } - } - - // ── mutation ────────────────────────────────────────── - - function addBinding() { - validationState.value = null - const next = createEmptyBinding() - setBindingCollapsed(next.id, false) - bindings.value.unshift(next) - } - - function removeBinding(id: string) { - if (validationState.value?.bindingId === id) { - validationState.value = null - } - setBindingCollapsed(id, false) - bindings.value = bindings.value.filter((item) => item.id !== id) - } - - // ── import from observed products ──────────────────── - - function createBindingFromProductCandidate(item: ImportableProductCandidate): EditableBinding { - const binding = { - id: crypto.randomUUID(), - provider: item.provider || 'agiso', - platform: item.platform, - shopId: item.shopId, - shopName: item.shopName, - skuCode: '', - skuName: item.externalSkuName, - profileKey: 'manual_review', - enabled: true, - priority: 100, - externalSkuCode: item.externalSkuCode, - externalItemId: item.externalItemId, - externalSkuName: item.externalSkuName, - resolvedSkuName: item.externalSkuName, - } - - return binding - } - - function importObservedProduct(item: ImportableProductCandidate) { - validationState.value = null - const nextBinding = createBindingFromProductCandidate(item) - setBindingCollapsed(nextBinding.id, false) - bindings.value.unshift(nextBinding) - } - - // ── save ────────────────────────────────────────────── - - function normalizeBindingForSave(item: EditableBinding): SaveBindingPayload { - return { - provider: item.provider.trim() || 'agiso', - platform: item.platform.trim(), - shopId: item.shopId.trim(), - shopName: item.shopName.trim(), - skuCode: item.skuCode.trim(), - skuName: item.skuName.trim(), - profileKey: item.profileKey.trim() || 'manual_review', - enabled: item.enabled, - priority: Number(item.priority || 100), - match: { - externalSkuCode: item.externalSkuCode.trim(), - externalItemId: item.externalItemId.trim(), - externalSkuName: item.externalSkuName.trim(), - config: { - resolvedSkuName: item.resolvedSkuName.trim(), - }, - }, - } - } - - function hasBindingContent(item: SaveBindingPayload) { - return Boolean( - item.platform || - item.shopId || - item.shopName || - item.skuCode || - item.skuName || - item.match.externalSkuCode || - item.match.externalItemId || - item.match.externalSkuName || - item.match.config.resolvedSkuName || - item.provider !== 'agiso' || - item.profileKey !== 'manual_review' || - item.priority !== 100 || - item.enabled !== true, - ) - } - - function resolveBindingValidationState( - item: SaveBindingPayload, - index: number, - bindingId: string, - ): ValidationState { - const label = `第 ${index + 1} 条规则` - - if (!item.skuCode) { - return { - bindingId, - field: 'skuCode', - message: `${label} 还没填写"内部履约 SKU"`, - } - } - - if (!item.match.externalSkuCode && !item.match.externalItemId && !item.match.externalSkuName) { - return { - bindingId, - field: 'match', - message: `${label} 至少填写一种外部匹配条件`, - } - } - - if (!VALID_PROFILE_KEYS.has(item.profileKey)) { - return { - bindingId, - field: 'profileKey', - message: `${label} 的履约方式无效`, - } - } - - return null - } - - async function focusValidationTarget(state: ValidationState) { - if (!state?.bindingId) { - return - } - - setBindingCollapsed(state.bindingId, false) - await nextTick() - - const card = document.querySelector(`[data-binding-id="${state.bindingId}"]`) - if (!card) { - return - } - - card.scrollIntoView({ behavior: 'smooth', block: 'center' }) - - const selectors: Record = { - shopId: '[data-field="shopId"]', - skuCode: '[data-field="skuCode"]', - match: '[data-field="externalSkuCode"]', - profileKey: '[data-field="profileKey"]', - } - - const target = card.querySelector(selectors[state.field]) - target?.focus() - } - - function clearValidationState() { - validationState.value = null - } - - function isBindingInvalid(bindingId: string) { - return validationState.value?.bindingId === bindingId - } - - function isFieldInvalid( - bindingId: string, - field: ValidationField | 'externalSkuCode' | 'externalItemId' | 'externalSkuName', - ) { - if (validationState.value?.bindingId !== bindingId) { - return false - } - - if (validationState.value.field === 'match') { - return ['externalSkuCode', 'externalItemId', 'externalSkuName'].includes(field) - } - - return validationState.value.field === field - } - - async function saveConfigs() { - const normalizedBindings = bindings.value.map(normalizeBindingForSave) - const nonEmptyBindings = normalizedBindings.filter(hasBindingContent) - const invalidState = normalizedBindings.reduce((state, item, index) => { - if (state || !hasBindingContent(item)) { - return state - } - - return resolveBindingValidationState(item, index, bindings.value[index]?.id || '') - }, null) - - if (invalidState) { - validationState.value = invalidState - await focusValidationTarget(invalidState) - return - } - - if (bindings.value.length > 0 && nonEmptyBindings.length === 0) { - validationState.value = { - bindingId: '', - field: 'skuCode', - message: '当前没有可保存的规则,请至少填写内部履约 SKU 和外部匹配条件', - } - return - } - - const payload = nonEmptyBindings - - saving.value = true - validationState.value = null - - try { - const response = await saveAdminFulfillmentBindingConfigs({ bindings: payload }) - filePath.value = response.data.filePath - bindings.value = response.data.bindings.map(mapEditableBinding) - showSuccess('履约配置已保存') - await loadConfigs() - } catch (error) { - validationState.value = { - bindingId: '', - field: 'skuCode', - message: error instanceof Error ? error.message : '保存履约配置失败', - } - } finally { - saving.value = false - } - } - - return { - // state - loading, - saving, - errorMessage, - filePath, - bindings, - observedProducts, - validationState, - collapsedBindingIds, - // computed - ruleMetrics, - pendingObservedCount, - // factories - createEmptyBinding, - mapEditableBinding, - // validation - isEditableBindingComplete, - clearValidationState, - isBindingInvalid, - isFieldInvalid, - // collapse - isBindingCollapsed, - setBindingCollapsed, - toggleBindingCollapsed, - // display - getBindingTitle, - getBindingStatusLabel, - getBindingSummary, - // data - loadConfigs, - addBinding, - removeBinding, - importObservedProduct, - createBindingFromProductCandidate, - normalizeBindingForSave, - saveConfigs, - } -} diff --git a/apps/frontend/src/views/admin/fulfillment/bindings/composables/useFulfillmentBindingsLookup.ts b/apps/frontend/src/views/admin/fulfillment/bindings/composables/useFulfillmentBindingsLookup.ts deleted file mode 100644 index 6b99cead..00000000 --- a/apps/frontend/src/views/admin/fulfillment/bindings/composables/useFulfillmentBindingsLookup.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { reactive, ref } from 'vue' - -import { lookupAdminFulfillmentBindingOrder } from '@/services/admin' -import type { AdminFulfillmentLookupItem, AdminFulfillmentLookupResult } from '@/types/admin' - -import type { useFulfillmentBindings } from './useFulfillmentBindings' - -export function useFulfillmentBindingsLookup( - bindings: ReturnType, -) { - const lookupLoading = ref(false) - const lookupErrorMessage = ref('') - const lookupResult = ref(null) - const importedLookupLineIds = ref([]) - const lookupForm = reactive({ - shopId: '', - platformOrderId: '', - }) - - function importLookupProduct(item: AdminFulfillmentLookupItem) { - bindings.importObservedProduct(item) - if (!importedLookupLineIds.value.includes(item.lineId)) { - importedLookupLineIds.value = [...importedLookupLineIds.value, item.lineId] - } - } - - function isLookupProductImported(lineId: string) { - return importedLookupLineIds.value.includes(lineId) - } - - async function lookupOrderProducts() { - const shopId = lookupForm.shopId.trim() - const platformOrderId = lookupForm.platformOrderId.trim() - - if (!shopId || !platformOrderId) { - lookupErrorMessage.value = '请先填写店铺 ID 和平台订单号' - return - } - - lookupLoading.value = true - lookupErrorMessage.value = '' - - try { - const response = await lookupAdminFulfillmentBindingOrder({ - provider: 'agiso', - platform: 'xianyu', - shopId, - platformOrderId, - }) - lookupResult.value = response.data - importedLookupLineIds.value = [] - } catch (error) { - lookupResult.value = null - lookupErrorMessage.value = error instanceof Error ? error.message : '订单商品查询失败' - } finally { - lookupLoading.value = false - } - } - - return { - lookupLoading, - lookupErrorMessage, - lookupResult, - importedLookupLineIds, - lookupForm, - importLookupProduct, - isLookupProductImported, - lookupOrderProducts, - } -} diff --git a/apps/frontend/src/views/admin/layout/AdminLayout.vue b/apps/frontend/src/views/admin/layout/AdminLayout.vue index fd556e86..211db269 100644 --- a/apps/frontend/src/views/admin/layout/AdminLayout.vue +++ b/apps/frontend/src/views/admin/layout/AdminLayout.vue @@ -11,7 +11,6 @@ import { Fold, List, Message, - Service, Setting, SwitchButton, Tickets, @@ -63,10 +62,7 @@ const navItems = computed(() => { items.push({ to: '/admin/inventory', label: '库存', icon: Box }) } - items.push( - { to: '/admin/manual-redeem', label: '人工兑换', icon: Service }, - { to: '/admin/message-deliveries', label: '消息发送', icon: Message }, - ) + items.push({ to: '/admin/message-deliveries', label: '消息发送', icon: Message }) if (isAdmin.value || isOperator.value) { items.push({ to: '/admin/webhook-events', label: 'Webhook', icon: Connection }) diff --git a/apps/frontend/src/views/admin/manual-redeem/AdminManualRedeemView.vue b/apps/frontend/src/views/admin/manual-redeem/AdminManualRedeemView.vue deleted file mode 100644 index 23999b94..00000000 --- a/apps/frontend/src/views/admin/manual-redeem/AdminManualRedeemView.vue +++ /dev/null @@ -1,275 +0,0 @@ - - - - - diff --git a/apps/frontend/src/views/admin/manual-redeem/components/AdminManualRedeemCreateCard.vue b/apps/frontend/src/views/admin/manual-redeem/components/AdminManualRedeemCreateCard.vue deleted file mode 100644 index 277e48ca..00000000 --- a/apps/frontend/src/views/admin/manual-redeem/components/AdminManualRedeemCreateCard.vue +++ /dev/null @@ -1,117 +0,0 @@ - - - - - diff --git a/apps/frontend/src/views/admin/manual-redeem/components/AdminManualRedeemReviewCard.vue b/apps/frontend/src/views/admin/manual-redeem/components/AdminManualRedeemReviewCard.vue deleted file mode 100644 index 255820d4..00000000 --- a/apps/frontend/src/views/admin/manual-redeem/components/AdminManualRedeemReviewCard.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - - - diff --git a/apps/frontend/src/views/admin/manual-redeem/components/AdminManualRedeemSummaryCard.vue b/apps/frontend/src/views/admin/manual-redeem/components/AdminManualRedeemSummaryCard.vue deleted file mode 100644 index ef99cbfc..00000000 --- a/apps/frontend/src/views/admin/manual-redeem/components/AdminManualRedeemSummaryCard.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - - - diff --git a/apps/frontend/src/views/admin/platform-shops/AdminPlatformShopsView.vue b/apps/frontend/src/views/admin/platform-shops/AdminPlatformShopsView.vue index 72251d16..db3a162b 100644 --- a/apps/frontend/src/views/admin/platform-shops/AdminPlatformShopsView.vue +++ b/apps/frontend/src/views/admin/platform-shops/AdminPlatformShopsView.vue @@ -1,22 +1,19 @@ - - - - diff --git a/apps/frontend/src/views/admin/platform-shops/composables/types.ts b/apps/frontend/src/views/admin/platform-shops/composables/types.ts index 1f89f4a5..b62b5e67 100644 --- a/apps/frontend/src/views/admin/platform-shops/composables/types.ts +++ b/apps/frontend/src/views/admin/platform-shops/composables/types.ts @@ -1,25 +1,9 @@ export type PlatformTab = - | 'agiso' | 'notifications' | 'ninetyone' | 'kuaishouEticket' | 'cloudtentacles' -export type EditableDefaults = { - messageTemplate: string - autoDeliveryMessageTemplate: string -} - -export type EditableShop = { - id: string - shopId: string - shopName: string - accessToken: string - enabled: boolean - messageTemplate: string - autoDeliveryMessageTemplate: string -} - export type EditableKuaishouEticketShop = { id: string shopId: string diff --git a/apps/frontend/src/views/admin/platform-shops/composables/useAdminAgisoPlatform.ts b/apps/frontend/src/views/admin/platform-shops/composables/useAdminAgisoPlatform.ts deleted file mode 100644 index c6b4a79e..00000000 --- a/apps/frontend/src/views/admin/platform-shops/composables/useAdminAgisoPlatform.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { computed, ref } from 'vue' - -import { showError, showSuccess } from '@/lib/feedback' -import { saveAdminAgisoShopConfigs } from '@/services/admin' -import type { - AdminAgisoMessagingDefaults, - AdminAgisoObservedShopItem, - AdminAgisoShopConfigItem, -} from '@/types/admin' - -import type { EditableDefaults, EditableShop } from './types' - -export function useAdminAgisoPlatform() { - const saving = ref(false) - const agisoFilePath = ref('') - const defaults = ref(createEmptyDefaults()) - const shops = ref([]) - const observedShops = ref([]) - const expandedShopIds = ref([]) - - const agisoStats = computed(() => ({ - configuredShopCount: shops.value.length, - observedShopCount: observedShops.value.length, - pendingObservedCount: observedShops.value.filter((item) => !item.configured).length, - })) - - function hydrateAgisoConfig(data: { - filePath: string - defaults: AdminAgisoMessagingDefaults - shops: AdminAgisoShopConfigItem[] - observedShops: AdminAgisoObservedShopItem[] - }) { - agisoFilePath.value = data.filePath - defaults.value = mapEditableDefaults(data.defaults) - shops.value = data.shops.map(mapEditableShop) - observedShops.value = data.observedShops - expandedShopIds.value = [] - } - - function addShop() { - const shop = createEmptyShop() - shops.value.unshift(shop) - expandShop(shop.id) - } - - function removeShop(id: string) { - shops.value = shops.value.filter((item) => item.id !== id) - collapseShop(id) - } - - function importObservedShop(item: AdminAgisoObservedShopItem) { - const shop: EditableShop = { - id: crypto.randomUUID(), - shopId: item.shopId, - shopName: item.detectedShopName || item.displayShopName || '', - accessToken: '', - enabled: true, - messageTemplate: '', - autoDeliveryMessageTemplate: '', - } - shops.value.unshift(shop) - expandShop(shop.id) - } - - function isShopExpanded(id: string) { - return expandedShopIds.value.includes(id) - } - - function expandShop(id: string) { - if (expandedShopIds.value.includes(id)) { - return - } - expandedShopIds.value = [id, ...expandedShopIds.value] - } - - function collapseShop(id: string) { - expandedShopIds.value = expandedShopIds.value.filter((item) => item !== id) - } - - function toggleShop(id: string) { - if (isShopExpanded(id)) { - collapseShop(id) - return - } - expandShop(id) - } - - function expandAllShops() { - expandedShopIds.value = shops.value.map((item) => item.id) - } - - function collapseAllShops() { - expandedShopIds.value = [] - } - - function describeShop(shop: EditableShop) { - const overrides: string[] = [] - - if (shop.messageTemplate.trim()) { - overrides.push('领取模板') - } - if (shop.autoDeliveryMessageTemplate.trim()) { - overrides.push('发货模板') - } - - return overrides.length === 0 ? '使用默认模板' : `已覆盖 ${overrides.join('、')}` - } - - async function saveAgisoConfigs(reloadConfigs: () => Promise) { - const payloadDefaults = { - messageTemplate: defaults.value.messageTemplate.trim(), - autoDeliveryMessageTemplate: defaults.value.autoDeliveryMessageTemplate.trim(), - } - const payload = shops.value - .map((item) => ({ - shopId: item.shopId.trim(), - shopName: item.shopName.trim(), - accessToken: item.accessToken.trim(), - enabled: item.enabled, - messageTemplate: item.messageTemplate.trim(), - autoDeliveryMessageTemplate: item.autoDeliveryMessageTemplate.trim(), - })) - .filter((item) => item.shopId && item.accessToken) - - saving.value = true - - try { - const response = await saveAdminAgisoShopConfigs({ - defaults: payloadDefaults, - shops: payload, - }) - agisoFilePath.value = response.data.filePath - defaults.value = mapEditableDefaults(response.data.defaults) - shops.value = response.data.shops.map(mapEditableShop) - showSuccess('Agiso 配置已保存') - await reloadConfigs() - } catch (error) { - showError(error instanceof Error ? error.message : '保存 Agiso 配置失败') - } finally { - saving.value = false - } - } - - return { - saving, - agisoFilePath, - defaults, - shops, - observedShops, - expandedShopIds, - agisoStats, - hydrateAgisoConfig, - addShop, - removeShop, - importObservedShop, - isShopExpanded, - expandShop, - collapseShop, - toggleShop, - expandAllShops, - collapseAllShops, - describeShop, - saveAgisoConfigs, - } -} - -function createEmptyDefaults(): EditableDefaults { - return { - messageTemplate: '', - autoDeliveryMessageTemplate: '', - } -} - -function createEmptyShop(): EditableShop { - return { - id: crypto.randomUUID(), - shopId: '', - shopName: '', - accessToken: '', - enabled: true, - messageTemplate: '', - autoDeliveryMessageTemplate: '', - } -} - -function mapEditableDefaults(item?: AdminAgisoMessagingDefaults): EditableDefaults { - return { - messageTemplate: item?.messageTemplate ?? '', - autoDeliveryMessageTemplate: item?.autoDeliveryMessageTemplate ?? '', - } -} - -function mapEditableShop(item: AdminAgisoShopConfigItem): EditableShop { - return { - id: crypto.randomUUID(), - shopId: item.shopId, - shopName: item.shopName, - accessToken: item.AccessToken, - enabled: item.enabled !== false, - messageTemplate: item.messageTemplate, - autoDeliveryMessageTemplate: item.autoDeliveryMessageTemplate, - } -} diff --git a/apps/frontend/src/views/claim/ClaimTencentView.vue b/apps/frontend/src/views/claim/ClaimTencentView.vue deleted file mode 100644 index 056e312d..00000000 --- a/apps/frontend/src/views/claim/ClaimTencentView.vue +++ /dev/null @@ -1,357 +0,0 @@ - - - - - diff --git a/apps/frontend/src/views/claim/ClaimView.vue b/apps/frontend/src/views/claim/ClaimView.vue index 72756140..45f05643 100644 --- a/apps/frontend/src/views/claim/ClaimView.vue +++ b/apps/frontend/src/views/claim/ClaimView.vue @@ -1,90 +1,13 @@ - - diff --git a/apps/frontend/src/views/tx/TencentBrowserView.vue b/apps/frontend/src/views/tx/TencentBrowserView.vue deleted file mode 100644 index 3aa7e8c3..00000000 --- a/apps/frontend/src/views/tx/TencentBrowserView.vue +++ /dev/null @@ -1,303 +0,0 @@ - - - - - diff --git a/deploy/docker/frontend.Dockerfile b/deploy/docker/frontend.Dockerfile index cd6c489d..d72fcaae 100644 --- a/deploy/docker/frontend.Dockerfile +++ b/deploy/docker/frontend.Dockerfile @@ -1,11 +1,9 @@ FROM docker.m.daocloud.io/library/node:22-bookworm AS builder ARG NPM_REGISTRY=https://registry.npmmirror.com -ARG VITE_APP_MODE= ENV TZ=Asia/Shanghai ENV NPM_CONFIG_REGISTRY=${NPM_REGISTRY} -ENV VITE_APP_MODE=${VITE_APP_MODE} WORKDIR /app diff --git a/docker-compose.kuaishou.dev.yml b/docker-compose.kuaishou.dev.yml index 65818617..8366c427 100644 --- a/docker-compose.kuaishou.dev.yml +++ b/docker-compose.kuaishou.dev.yml @@ -73,7 +73,6 @@ services: NPM_CONFIG_REGISTRY: https://registry.npmmirror.com VITE_API_TARGET: http://backend:3000 VITE_ALLOWED_HOSTS: ${VITE_ALLOWED_HOSTS:-localhost,127.0.0.1} - VITE_APP_MODE: kuaishou volumes: - ./apps/frontend:/app - frontend_kuaishou_node_modules:/app/node_modules diff --git a/docker-compose.kuaishou.yml b/docker-compose.kuaishou.yml index baddd318..e19f1ee3 100644 --- a/docker-compose.kuaishou.yml +++ b/docker-compose.kuaishou.yml @@ -57,7 +57,6 @@ services: dockerfile: deploy/docker/frontend.Dockerfile args: APP_DOMAIN: ${APP_DOMAIN:-localhost} - VITE_APP_MODE: kuaishou restart: unless-stopped depends_on: backend: