完善半自动履约凭证与客户重新授权流程
统一虎牙和微博账号级 CK 保存与导出;CK 失效后回到客户重新登录,后台支持重发授权;客户授权页改为统一深色布局并收敛开发迁移配置。
This commit is contained in:
@@ -2,16 +2,22 @@ import {
|
||||
CheckCircleOutlined,
|
||||
ChromeOutlined,
|
||||
CopyOutlined,
|
||||
DownOutlined,
|
||||
KeyOutlined,
|
||||
LinkOutlined,
|
||||
MoreOutlined,
|
||||
ReloadOutlined,
|
||||
SendOutlined,
|
||||
UserSwitchOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Drawer,
|
||||
Dropdown,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
@@ -29,7 +35,6 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import ImageUpload from '@/components/files/ImageUpload'
|
||||
import PageHeader from '@/components/admin/PageHeader'
|
||||
import PercentTable from '@/components/common/PercentTable'
|
||||
import { uploadFile } from '@/services/files'
|
||||
import {
|
||||
cancelAdminSemiAutoTask,
|
||||
claimAdminSemiAutoTask,
|
||||
@@ -38,9 +43,10 @@ import {
|
||||
exportAdminSemiAutoCredential,
|
||||
fetchAdminSemiAutoTask,
|
||||
fetchAdminSemiAutoTasks,
|
||||
requestAdminSemiAutoCustomerReauth,
|
||||
syncAdminSemiAutoTask,
|
||||
} from '@/services/admin/semi-auto'
|
||||
import type { SemiAutoCredentialExport, SemiAutoTask } from '@/types/admin/semi-auto'
|
||||
import type { SemiAutoTask } from '@/types/admin/semi-auto'
|
||||
import type { UploadedFile } from '@/types/worker-platform'
|
||||
import { hasAdminPermission } from '@/utils/admin-auth'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
@@ -54,6 +60,7 @@ const STATUS_COLORS: Record<string, string> = {
|
||||
waiting_binding: 'gold',
|
||||
binding_confirm: 'orange',
|
||||
waiting_operator: 'volcano',
|
||||
credential_expired: 'red',
|
||||
processing: 'processing',
|
||||
completed: 'success',
|
||||
failed: 'error',
|
||||
@@ -82,7 +89,6 @@ export default function AdminSemiAutoTasksPage() {
|
||||
const [detailTaskId, setDetailTaskId] = useState<number | null>(null)
|
||||
const [completeTask, setCompleteTask] = useState<SemiAutoTask | null>(null)
|
||||
const [completeFiles, setCompleteFiles] = useState<UploadedFile[]>([])
|
||||
const [credentialExport, setCredentialExport] = useState<SemiAutoCredentialExport | null>(null)
|
||||
const [completeForm] = Form.useForm<CompleteFormValues>()
|
||||
|
||||
const params = useMemo(
|
||||
@@ -169,7 +175,7 @@ export default function AdminSemiAutoTasksPage() {
|
||||
return
|
||||
}
|
||||
window.postMessage({ type: 'ORDER_SITE_SEMI_AUTO_INJECT', ticket }, window.location.origin)
|
||||
message.info('正在通过虎牙履约助手注入登录态,请稍候')
|
||||
message.info('正在注入浏览器登录态,请稍候')
|
||||
void refreshAll()
|
||||
},
|
||||
onError: (error) =>
|
||||
@@ -179,13 +185,32 @@ export default function AdminSemiAutoTasksPage() {
|
||||
const credentialExportMutation = useMutation({
|
||||
mutationFn: (taskId: number) => exportAdminSemiAutoCredential(taskId),
|
||||
onSuccess: (response) => {
|
||||
if (!response.data) {
|
||||
message.error('账号 CK 导出失败')
|
||||
const text =
|
||||
String(response.data?.cookieHeader || '').trim() ||
|
||||
(response.data?.cookies || []).map((cookie) => `${cookie.name}=${cookie.value}`).join('; ')
|
||||
if (!text) {
|
||||
message.error('当前任务没有可复制的 CK')
|
||||
return
|
||||
}
|
||||
setCredentialExport(response.data)
|
||||
void navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => message.success('最终 CK 已复制'))
|
||||
.catch(() => message.error('复制失败,请检查浏览器剪贴板权限'))
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '导出账号 CK 失败'),
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '复制账号 CK 失败'),
|
||||
})
|
||||
|
||||
const reauthMutation = useMutation({
|
||||
mutationFn: (task: SemiAutoTask) => requestAdminSemiAutoCustomerReauth(task.id),
|
||||
onSuccess: async (response, task) => {
|
||||
const copied = await copyCustomerLink(response.data?.task || task, '')
|
||||
message.success(
|
||||
copied ? '已重新发起授权,客户链接已复制' : '已重新发起授权,请手动复制客户链接',
|
||||
)
|
||||
await refreshAll()
|
||||
},
|
||||
onError: (error) =>
|
||||
message.error(error instanceof Error ? error.message : '重新发起客户授权失败'),
|
||||
})
|
||||
|
||||
const completeMutation = useMutation({
|
||||
@@ -231,33 +256,114 @@ export default function AdminSemiAutoTasksPage() {
|
||||
])
|
||||
}
|
||||
|
||||
function copyCustomerLink(task: SemiAutoTask) {
|
||||
async function copyCustomerLink(task: SemiAutoTask, successMessage = '客户授权链接已复制') {
|
||||
const url = `${window.location.origin}/c/${encodeURIComponent(task.orderNo)}`
|
||||
void navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => message.success('客户授权链接已复制'))
|
||||
.catch(() => message.error('复制失败,请手动复制'))
|
||||
try {
|
||||
await navigator.clipboard.writeText(url)
|
||||
if (successMessage) message.success(successMessage)
|
||||
return true
|
||||
} catch {
|
||||
message.error('复制失败,请手动复制客户链接')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function copyCredentialText(text: string, label: string) {
|
||||
void navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => message.success(`${label}已复制`))
|
||||
.catch(() => message.error('复制失败,请手动选择文本'))
|
||||
}
|
||||
|
||||
function confirmExportCredential(task: SemiAutoTask) {
|
||||
function confirmReauth(task: SemiAutoTask) {
|
||||
modal.confirm({
|
||||
title: '导出账号 CK',
|
||||
content:
|
||||
'CK 等同于平台登录态。导出后会显示在当前页面,请勿发给无关人员;关闭弹窗后不会保留在页面状态中。',
|
||||
okText: '确认导出',
|
||||
title: '重新发送客户授权',
|
||||
content: '当前登录状态会失效,客户需要打开新的授权链接重新登录。原授权记录会保留。',
|
||||
okText: '重新发送并复制链接',
|
||||
cancelText: '取消',
|
||||
onOk: () => reauthMutation.mutateAsync(task),
|
||||
})
|
||||
}
|
||||
|
||||
function confirmCopyCredential(task: SemiAutoTask) {
|
||||
modal.confirm({
|
||||
title: '复制最终 CK',
|
||||
content: 'CK 等同于平台登录态。复制后请勿发给无关人员,也不要保存到聊天记录或工单备注。',
|
||||
okText: '确认复制',
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: '取消',
|
||||
onOk: () => credentialExportMutation.mutateAsync(task.id),
|
||||
})
|
||||
}
|
||||
|
||||
function credentialMenu(task: SemiAutoTask) {
|
||||
const canExport = hasAdminPermission('semi_auto.credential.export')
|
||||
return {
|
||||
items: [
|
||||
...(canExport
|
||||
? [
|
||||
{
|
||||
key: 'copy',
|
||||
icon: <KeyOutlined />,
|
||||
label: '复制最终 CK',
|
||||
disabled: !task.credentialReady || credentialExportMutation.isPending,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: 'inject',
|
||||
icon: <ChromeOutlined />,
|
||||
label: '注入浏览器',
|
||||
disabled: task.bindingStatus !== 'confirmed' || injectionMutation.isPending,
|
||||
},
|
||||
],
|
||||
onClick: ({ key }: { key: string }) => {
|
||||
if (key === 'copy') {
|
||||
confirmCopyCredential(task)
|
||||
return
|
||||
}
|
||||
if (key === 'inject') injectionMutation.mutate(task)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function taskMenu(task: SemiAutoTask) {
|
||||
const active = task.executionStatus !== 'completed' && task.executionStatus !== 'cancelled'
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
key: 'copy',
|
||||
icon: <LinkOutlined />,
|
||||
label: '复制客户链接',
|
||||
},
|
||||
{
|
||||
key: 'sync',
|
||||
icon: <ReloadOutlined />,
|
||||
label: '刷新客户状态',
|
||||
disabled: syncMutation.isPending,
|
||||
},
|
||||
...(!task.assignedAdminUserId
|
||||
? [
|
||||
{
|
||||
key: 'claim',
|
||||
icon: <UserSwitchOutlined />,
|
||||
label: '领取任务',
|
||||
disabled: claimMutation.isPending,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(active
|
||||
? [
|
||||
{
|
||||
key: 'cancel',
|
||||
danger: true,
|
||||
label: '取消任务',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
onClick: ({ key }: { key: string }) => {
|
||||
if (key === 'copy') void copyCustomerLink(task)
|
||||
if (key === 'sync') syncMutation.mutate(task.id)
|
||||
if (key === 'claim') claimMutation.mutate(task.id)
|
||||
if (key === 'cancel') confirmCancel(task)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function openComplete(task: SemiAutoTask) {
|
||||
setCompleteTask(task)
|
||||
setCompleteFiles([])
|
||||
@@ -300,17 +406,14 @@ export default function AdminSemiAutoTasksPage() {
|
||||
{
|
||||
title: '平台',
|
||||
dataIndex: 'platform',
|
||||
width: 90,
|
||||
render: (value: string) => <Tag color="purple">{PLATFORM_LABELS[value] || value}</Tag>,
|
||||
},
|
||||
{
|
||||
title: 'Connector',
|
||||
dataIndex: 'connectorMode',
|
||||
width: 110,
|
||||
render: (value: string) => (
|
||||
<Tag color={value === 'mock' ? 'orange' : 'green'}>
|
||||
{value === 'mock' ? '内置 Mock' : '真实'}
|
||||
</Tag>
|
||||
width: 120,
|
||||
render: (value: string, task) => (
|
||||
<Space direction="vertical" size={4}>
|
||||
<Tag color="purple">{PLATFORM_LABELS[value] || value}</Tag>
|
||||
<Tag color={task.connectorMode === 'mock' ? 'orange' : 'green'}>
|
||||
{task.connectorMode === 'mock' ? 'Mock' : '真实'}
|
||||
</Tag>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -365,58 +468,46 @@ export default function AdminSemiAutoTasksPage() {
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 360,
|
||||
width: 340,
|
||||
render: (_, task) => (
|
||||
<Space wrap>
|
||||
<Button size="small" onClick={() => setDetailTaskId(task.id)}>
|
||||
详情
|
||||
</Button>
|
||||
<Button size="small" icon={<CopyOutlined />} onClick={() => copyCustomerLink(task)}>
|
||||
客户链接
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={syncMutation.isPending}
|
||||
onClick={() => syncMutation.mutate(task.id)}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
{!task.assignedAdminUserId ? (
|
||||
{taskNeedsCustomerReauth(task) ? (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<UserSwitchOutlined />}
|
||||
loading={claimMutation.isPending}
|
||||
onClick={() => claimMutation.mutate(task.id)}
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
loading={reauthMutation.isPending}
|
||||
onClick={() => confirmReauth(task)}
|
||||
>
|
||||
领取
|
||||
重发授权
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ChromeOutlined />}
|
||||
disabled={task.bindingStatus !== 'confirmed'}
|
||||
loading={injectionMutation.isPending}
|
||||
onClick={() => injectionMutation.mutate(task)}
|
||||
>
|
||||
注入浏览器
|
||||
</Button>
|
||||
{task.executionStatus !== 'completed' && task.executionStatus !== 'cancelled' ? (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<CheckCircleOutlined />}
|
||||
disabled={task.bindingStatus !== 'confirmed'}
|
||||
onClick={() => openComplete(task)}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
<Button size="small" danger onClick={() => confirmCancel(task)}>
|
||||
取消
|
||||
</Button>
|
||||
</>
|
||||
<Dropdown menu={credentialMenu(task)} trigger={['click']}>
|
||||
<Button size="small" icon={<KeyOutlined />}>
|
||||
CK 操作 <DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
{task.executionStatus !== 'completed' &&
|
||||
task.executionStatus !== 'cancelled' &&
|
||||
!taskNeedsCustomerReauth(task) ? (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<CheckCircleOutlined />}
|
||||
disabled={task.bindingStatus !== 'confirmed'}
|
||||
onClick={() => openComplete(task)}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
) : null}
|
||||
<Dropdown menu={taskMenu(task)} trigger={['click']}>
|
||||
<Button size="small" icon={<MoreOutlined />}>
|
||||
更多
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -582,6 +673,14 @@ export default function AdminSemiAutoTasksPage() {
|
||||
{formatAdminDateTime(detail.task.updatedAt)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{taskNeedsCustomerReauth(detail.task) ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="客户登录状态需要更新"
|
||||
description="平台 CK 已失效或任务登录失败。重新发送后,客户需要重新打开链接完成登录。"
|
||||
/>
|
||||
) : null}
|
||||
{detail.task.lastError ? (
|
||||
<Card size="small" title="最近错误">
|
||||
<Text type="danger">{detail.task.lastError}</Text>
|
||||
@@ -589,7 +688,17 @@ export default function AdminSemiAutoTasksPage() {
|
||||
) : null}
|
||||
<Card size="small" title="操作">
|
||||
<Space wrap>
|
||||
<Button icon={<CopyOutlined />} onClick={() => copyCustomerLink(detail.task)}>
|
||||
{taskNeedsCustomerReauth(detail.task) ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
loading={reauthMutation.isPending}
|
||||
onClick={() => confirmReauth(detail.task)}
|
||||
>
|
||||
重新发送授权
|
||||
</Button>
|
||||
) : null}
|
||||
<Button icon={<CopyOutlined />} onClick={() => void copyCustomerLink(detail.task)}>
|
||||
复制客户链接
|
||||
</Button>
|
||||
<Button
|
||||
@@ -599,25 +708,11 @@ export default function AdminSemiAutoTasksPage() {
|
||||
>
|
||||
刷新客户状态
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ChromeOutlined />}
|
||||
disabled={detail.task.bindingStatus !== 'confirmed'}
|
||||
loading={injectionMutation.isPending}
|
||||
onClick={() => injectionMutation.mutate(detail.task)}
|
||||
>
|
||||
注入浏览器
|
||||
</Button>
|
||||
{detail.task.credentialReady &&
|
||||
hasAdminPermission('semi_auto.credential.export') ? (
|
||||
<Button
|
||||
danger
|
||||
icon={<KeyOutlined />}
|
||||
loading={credentialExportMutation.isPending}
|
||||
onClick={() => confirmExportCredential(detail.task)}
|
||||
>
|
||||
导出 CK
|
||||
<Dropdown menu={credentialMenu(detail.task)} trigger={['click']}>
|
||||
<Button icon={<KeyOutlined />}>
|
||||
CK 操作 <DownOutlined />
|
||||
</Button>
|
||||
) : null}
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</Card>
|
||||
<Card size="small" title="时间线">
|
||||
@@ -642,65 +737,6 @@ export default function AdminSemiAutoTasksPage() {
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
open={Boolean(credentialExport)}
|
||||
title={
|
||||
credentialExport
|
||||
? `账号 CK · ${credentialExport.platform === 'weibo' ? '微博' : '平台'} ${credentialExport.platformUid}`
|
||||
: '账号 CK'
|
||||
}
|
||||
width={760}
|
||||
footer={null}
|
||||
onCancel={() => setCredentialExport(null)}
|
||||
>
|
||||
{credentialExport ? (
|
||||
<Space direction="vertical" size={14} style={{ width: '100%' }}>
|
||||
<Text type="danger">
|
||||
以下内容包含平台登录态,仅用于本次受控操作。关闭弹窗后请勿在聊天、日志或工单中留存。
|
||||
</Text>
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<Text strong>{credentialExport.targetUrl || 'Cookie Header'}</Text>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => copyCredentialText(credentialExport.cookieHeader, 'Cookie Header')}
|
||||
>
|
||||
复制 Header
|
||||
</Button>
|
||||
</Space>
|
||||
<Input.TextArea
|
||||
value={credentialExport.cookieHeader}
|
||||
readOnly
|
||||
autoSize={{ minRows: 3, maxRows: 6 }}
|
||||
/>
|
||||
</Space>
|
||||
<Space direction="vertical" size={6} style={{ width: '100%' }}>
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<Text strong>完整域名 Cookie</Text>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() =>
|
||||
copyCredentialText(
|
||||
JSON.stringify(credentialExport.cookies, null, 2),
|
||||
'完整 Cookie JSON',
|
||||
)
|
||||
}
|
||||
>
|
||||
复制 JSON
|
||||
</Button>
|
||||
</Space>
|
||||
<Input.TextArea
|
||||
value={JSON.stringify(credentialExport.cookies, null, 2)}
|
||||
readOnly
|
||||
autoSize={{ minRows: 10, maxRows: 18 }}
|
||||
/>
|
||||
</Space>
|
||||
</Space>
|
||||
) : null}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={Boolean(completeTask)}
|
||||
title="确认完成半自动履约"
|
||||
@@ -753,3 +789,14 @@ function firstTargetUrl(task: SemiAutoTask) {
|
||||
: []
|
||||
return String(targets.find((item) => item?.url)?.url || '')
|
||||
}
|
||||
|
||||
function taskNeedsCustomerReauth(task: SemiAutoTask) {
|
||||
if (task.executionStatus === 'completed' || task.executionStatus === 'cancelled') return false
|
||||
return (
|
||||
task.credentialStatus === 'invalid' ||
|
||||
task.credentialStatus === 'error' ||
|
||||
task.executionStatus === 'failed' ||
|
||||
task.loginStatus === 'failed' ||
|
||||
task.loginStatus === 'expired'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,28 +2,12 @@ import {
|
||||
CheckCircleFilled,
|
||||
LoadingOutlined,
|
||||
QrcodeOutlined,
|
||||
ReloadOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Image,
|
||||
Input,
|
||||
Result,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
Steps,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { App, Button, Image, Input, Select, Space, Spin, Tabs } from 'antd'
|
||||
import QRCode from 'qrcode'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
||||
import { useParams } from 'react-router'
|
||||
|
||||
import {
|
||||
@@ -49,8 +33,6 @@ import type {
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { runWeiboYidunChallenge } from '@/utils/weibo-yidun'
|
||||
|
||||
const { Text, Title, Paragraph } = Typography
|
||||
|
||||
const POLL_INTERVAL_MS = 2000
|
||||
|
||||
const DEFAULT_WEIBO_COUNTRY_CODES: CustomerWeiboCountryCode[] = [
|
||||
@@ -131,6 +113,30 @@ export default function CustomerFulfillmentPage() {
|
||||
void markCustomerFulfillmentOpened(orderNo, selectedTask.id).catch(() => undefined)
|
||||
}, [orderNo, selectedTask?.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTask || !orderNo) return
|
||||
if (
|
||||
selectedTask.executionStatus === 'completed' ||
|
||||
selectedTask.executionStatus === 'cancelled'
|
||||
) {
|
||||
return
|
||||
}
|
||||
const timer = window.setInterval(() => {
|
||||
void load(true)
|
||||
}, POLL_INTERVAL_MS)
|
||||
return () => window.clearInterval(timer)
|
||||
}, [load, orderNo, selectedTask?.executionStatus, selectedTask?.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTask) return
|
||||
if (!['waiting_login', 'failed', 'expired'].includes(selectedTask.loginStatus)) return
|
||||
setLoginInfo(null)
|
||||
setBindingInfo(null)
|
||||
setSmsCode('')
|
||||
setSmsMfaRequired(false)
|
||||
setSmsCountdown(0)
|
||||
}, [selectedTask?.id, selectedTask?.loginStatus])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTask || selectedTask.platform !== 'weibo' || !orderNo) return
|
||||
let cancelled = false
|
||||
@@ -402,58 +408,8 @@ export default function CustomerFulfillmentPage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="customer-fulfillment-page customer-fulfillment-centered">
|
||||
<Spin indicator={<LoadingOutlined spin />} tip="正在读取订单履约信息..." />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loadError || tasks.length === 0) {
|
||||
return (
|
||||
<div className="customer-fulfillment-page customer-fulfillment-centered">
|
||||
<Result
|
||||
status="warning"
|
||||
title="暂时无法打开订单"
|
||||
subTitle={loadError || '该订单没有需要客户授权或绑定的半自动履约任务。'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedTask?.executionStatus === 'completed') {
|
||||
return (
|
||||
<div className="customer-fulfillment-page">
|
||||
<Result
|
||||
status="success"
|
||||
icon={<CheckCircleFilled />}
|
||||
title="履约已完成"
|
||||
subTitle={
|
||||
selectedTask.completionNote
|
||||
? selectedTask.completionNote
|
||||
: isWeibo
|
||||
? '客服已完成本订单的微博发帖操作,请到微博账号中确认。'
|
||||
: '客服已完成本订单的充值或赠送操作,请到平台账号中确认。'
|
||||
}
|
||||
extra={
|
||||
selectedTask.completedAt
|
||||
? `完成时间:${formatAdminDateTime(selectedTask.completedAt)}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const config = selectedTask?.semiAutoConfig || {}
|
||||
const title = config.pageTitle || (isWeibo ? '微博账号授权' : '账号授权与角色绑定')
|
||||
const description =
|
||||
config.pageDescription ||
|
||||
(isWeibo
|
||||
? '请使用本人微博账号完成登录,登录成功后客服会处理您的订单。'
|
||||
: '请按页面指引完成平台登录和游戏角色绑定,客服随后会处理您的订单。')
|
||||
const currentStep = resolveStep(selectedTask, bindingInfo)
|
||||
const loginQrVisible =
|
||||
selectedTask && ['qr_pending', 'scanned'].includes(selectedTask.loginStatus)
|
||||
const bindingQrVisible =
|
||||
@@ -469,21 +425,69 @@ export default function CustomerFulfillmentPage() {
|
||||
bindingInfo?.hasExistingBinding === true &&
|
||||
bindingInfo?.rebindRequested !== true
|
||||
|
||||
return (
|
||||
<div className="customer-fulfillment-page">
|
||||
<Space direction="vertical" size={18} style={{ width: '100%' }}>
|
||||
<div>
|
||||
<Title level={2} style={{ marginBottom: 4 }}>
|
||||
{title}
|
||||
</Title>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
||||
{description}
|
||||
</Paragraph>
|
||||
if (loading) {
|
||||
return (
|
||||
<CustomerAuthFrame eyebrow="账号授权" title="正在读取订单信息">
|
||||
<div className="customer-auth-loading">
|
||||
<Spin indicator={<LoadingOutlined spin />} />
|
||||
<span>正在读取订单履约信息...</span>
|
||||
</div>
|
||||
</CustomerAuthFrame>
|
||||
)
|
||||
}
|
||||
|
||||
if (loadError || tasks.length === 0) {
|
||||
return (
|
||||
<CustomerAuthFrame eyebrow="账号授权" title="暂时无法打开订单">
|
||||
<div className="customer-auth-state is-error">
|
||||
<h2>订单信息读取失败</h2>
|
||||
<p>{loadError || '该订单没有需要客户授权或绑定的任务。'}</p>
|
||||
</div>
|
||||
</CustomerAuthFrame>
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedTask?.executionStatus === 'completed') {
|
||||
return (
|
||||
<CustomerAuthFrame
|
||||
platform={selectedTask.platform}
|
||||
eyebrow={isWeibo ? '微博账号授权' : '虎牙账号授权'}
|
||||
title={title}
|
||||
>
|
||||
<CustomerTaskFacts task={selectedTask} />
|
||||
<div className="customer-auth-state is-success">
|
||||
<div className="customer-auth-state-icon">
|
||||
<CheckCircleFilled />
|
||||
</div>
|
||||
<h2>履约已完成</h2>
|
||||
<p>
|
||||
{selectedTask.completionNote ||
|
||||
(isWeibo
|
||||
? '客服已完成本订单的微博发帖操作,请到微博账号中确认。'
|
||||
: '客服已完成本订单的充值或赠送操作,请到平台账号中确认。')}
|
||||
</p>
|
||||
{selectedTask.completedAt ? (
|
||||
<span className="customer-auth-state-time">
|
||||
完成时间:{formatAdminDateTime(selectedTask.completedAt)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<CustomerPrivacyNote />
|
||||
</CustomerAuthFrame>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomerAuthFrame
|
||||
platform={selectedTask?.platform}
|
||||
eyebrow={isWeibo ? '微博账号授权' : '虎牙账号授权'}
|
||||
title={title}
|
||||
>
|
||||
<div className="customer-auth-content-stack">
|
||||
{tasks.length > 1 ? (
|
||||
<Card size="small" title="请选择需要处理的产品">
|
||||
<Space wrap>
|
||||
<div className="customer-auth-task-switcher">
|
||||
<span>本订单需要处理</span>
|
||||
<div>
|
||||
{tasks.map((task) => (
|
||||
<Button
|
||||
key={task.id}
|
||||
@@ -497,61 +501,33 @@ export default function CustomerFulfillmentPage() {
|
||||
{task.productName || task.skuName} × {task.quantity}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Card>
|
||||
<Row gutter={[24, 20]}>
|
||||
<Col xs={24} lg={8}>
|
||||
<Space direction="vertical" size={10}>
|
||||
<Text type="secondary">订单号</Text>
|
||||
<Text strong style={{ fontSize: 18 }}>
|
||||
{selectedTask?.orderNo}
|
||||
</Text>
|
||||
<Text type="secondary">商品</Text>
|
||||
<Text strong>{selectedTask?.productName || selectedTask?.skuName}</Text>
|
||||
<Tag color="purple">{selectedTask?.platformLabel || selectedTask?.platform}</Tag>
|
||||
<Tag color="blue">{selectedTask?.displayStatus.label}</Tag>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24} lg={16}>
|
||||
<Steps
|
||||
current={currentStep}
|
||||
items={
|
||||
isWeibo
|
||||
? [
|
||||
{ title: '登录微博', icon: <QrcodeOutlined /> },
|
||||
{ title: '客服处理', icon: <LoadingOutlined /> },
|
||||
{ title: '完成' },
|
||||
]
|
||||
: [
|
||||
{ title: '登录平台', icon: <QrcodeOutlined /> },
|
||||
{ title: '扫码绑定', icon: <SafetyCertificateOutlined /> },
|
||||
{ title: '客服处理', icon: <LoadingOutlined /> },
|
||||
{ title: '完成' },
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{selectedTask && selectedTask.executionStatus === 'failed' ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message="当前流程需要客服处理"
|
||||
description={selectedTask.displayStatus.label}
|
||||
/>
|
||||
) : null}
|
||||
<CustomerTaskFacts task={selectedTask} />
|
||||
|
||||
{selectedTask?.loginStatus === 'waiting_login' ||
|
||||
selectedTask?.loginStatus === 'failed' ||
|
||||
selectedTask?.loginStatus === 'expired' ? (
|
||||
<Card title={isWeibo ? '第一步:登录微博账号' : '第一步:登录平台账号'}>
|
||||
<section className="customer-auth-section">
|
||||
<div className="customer-auth-section-heading">
|
||||
<span className="customer-auth-section-icon">
|
||||
<QrcodeOutlined />
|
||||
</span>
|
||||
<div>
|
||||
<h2>{isWeibo ? '登录微博账号' : '登录平台账号'}</h2>
|
||||
<p>
|
||||
{config.loginInstructions ||
|
||||
(isWeibo
|
||||
? '使用微博绑定手机号接收验证码,或使用微博 App 扫码登录。'
|
||||
: '使用虎牙 App 扫码登录,请确认扫码账号属于本人。')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{isWeibo ? (
|
||||
<Tabs
|
||||
className="customer-auth-tabs"
|
||||
activeKey={loginMode}
|
||||
onChange={(key) => setLoginMode(key as 'sms' | 'qr')}
|
||||
items={[
|
||||
@@ -559,29 +535,12 @@ export default function CustomerFulfillmentPage() {
|
||||
key: 'sms',
|
||||
label: '手机号登录',
|
||||
children: (
|
||||
<Space direction="vertical" size={14} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="使用微博绑定手机号登录"
|
||||
description={
|
||||
config.loginInstructions ||
|
||||
'验证码将发送到你的微博绑定手机号。登录仅用于本次订单履约,不会在页面展示账号 Cookie。'
|
||||
}
|
||||
/>
|
||||
{smsMfaRequired ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="正在等待微博安全验证"
|
||||
description="请在安全验证弹窗中完成验证,验证通过后会继续发送短信验证码。"
|
||||
/>
|
||||
) : null}
|
||||
<Space.Compact style={{ width: '100%', maxWidth: 520 }}>
|
||||
<div className="customer-auth-form">
|
||||
<div className="customer-auth-inline-input">
|
||||
<Select
|
||||
value={countryCode}
|
||||
onChange={(value) => setCountryCode(String(value))}
|
||||
style={{ width: 110 }}
|
||||
className="customer-auth-country-code"
|
||||
options={countryCodes.map((item) => ({
|
||||
value: item.code,
|
||||
label: `+${item.code}`,
|
||||
@@ -590,7 +549,9 @@ export default function CustomerFulfillmentPage() {
|
||||
optionRender={(option) => (
|
||||
<Space size={6}>
|
||||
<span>{(option.data as { name?: string }).name}</span>
|
||||
<Text type="secondary">+{option.data.value}</Text>
|
||||
<span className="customer-auth-option-hint">
|
||||
+{option.data.value}
|
||||
</span>
|
||||
</Space>
|
||||
)}
|
||||
filterOption={(input, option) =>
|
||||
@@ -604,20 +565,25 @@ export default function CustomerFulfillmentPage() {
|
||||
<Input
|
||||
value={mobile}
|
||||
onChange={(event) => setMobile(event.target.value)}
|
||||
placeholder="手机号"
|
||||
placeholder="请输入微博绑定手机号"
|
||||
maxLength={countryCode === '86' ? 11 : 15}
|
||||
inputMode="numeric"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={smsSending}
|
||||
disabled={smsCountdown > 0}
|
||||
onClick={() => void handleSendWeiboSms()}
|
||||
>
|
||||
{smsCountdown > 0 ? `重新发送(${smsCountdown}s)` : '发送验证码'}
|
||||
{smsCountdown > 0 ? `${smsCountdown}s 后重发` : '发送验证码'}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<Space.Compact style={{ width: '100%', maxWidth: 520 }}>
|
||||
</div>
|
||||
{smsMfaRequired ? (
|
||||
<div className="customer-auth-note">
|
||||
请在安全验证弹窗中完成验证,通过后会继续发送短信验证码。
|
||||
</div>
|
||||
) : null}
|
||||
<div className="customer-auth-inline-input is-code">
|
||||
<Input
|
||||
value={smsCode}
|
||||
onChange={(event) => setSmsCode(event.target.value)}
|
||||
@@ -632,159 +598,279 @@ export default function CustomerFulfillmentPage() {
|
||||
>
|
||||
登录微博
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<Text type="secondary">
|
||||
</div>
|
||||
<span className="customer-auth-form-hint">
|
||||
如安全验证弹窗未出现,请检查浏览器是否拦截脚本。
|
||||
</Text>
|
||||
</Space>
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'qr',
|
||||
label: '扫码登录',
|
||||
children: (
|
||||
<Space direction="vertical" size={14} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="请使用本人账号登录"
|
||||
description={
|
||||
config.loginInstructions ||
|
||||
'点击下方按钮后使用微博 App 扫码登录。登录仅用于本次订单履约,不会在页面展示账号 Cookie。'
|
||||
}
|
||||
/>
|
||||
<Button type="primary" size="large" onClick={() => void handleStartLogin()}>
|
||||
<div className="customer-auth-form">
|
||||
<p className="customer-auth-form-copy">
|
||||
点击下方按钮后,使用微博 App 扫码并确认登录。
|
||||
</p>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
onClick={() => void handleStartLogin()}
|
||||
>
|
||||
获取登录二维码
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Space direction="vertical" size={14} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="请使用本人账号登录"
|
||||
description={
|
||||
config.loginInstructions ||
|
||||
'点击下方按钮后使用虎牙 App 扫码登录。登录仅用于本次订单履约,不会在页面展示账号 Cookie。'
|
||||
}
|
||||
/>
|
||||
<Button type="primary" size="large" onClick={() => void handleStartLogin()}>
|
||||
<div className="customer-auth-form">
|
||||
<Button type="primary" size="large" block onClick={() => void handleStartLogin()}>
|
||||
获取登录二维码
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{loginQrVisible ? (
|
||||
<Card title={isWeibo ? '使用微博 App 扫码登录' : '使用虎牙 App 扫码登录'}>
|
||||
<Space direction="vertical" align="center" size={14} style={{ width: '100%' }}>
|
||||
<section className="customer-auth-section">
|
||||
<div className="customer-auth-section-heading">
|
||||
<span className="customer-auth-section-icon">
|
||||
<QrcodeOutlined />
|
||||
</span>
|
||||
<div>
|
||||
<h2>{isWeibo ? '使用微博 App 扫码' : '使用虎牙 App 扫码'}</h2>
|
||||
<p>二维码仅用于本次订单授权,请勿转发给他人。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="customer-auth-qr">
|
||||
{loginQrImage ? (
|
||||
<Image
|
||||
src={loginQrImage}
|
||||
alt={isWeibo ? '微博登录二维码' : '虎牙登录二维码'}
|
||||
width={260}
|
||||
preview={false}
|
||||
/>
|
||||
<div className="customer-auth-qr-image">
|
||||
<Image
|
||||
src={loginQrImage}
|
||||
alt={isWeibo ? '微博登录二维码' : '虎牙登录二维码'}
|
||||
width={232}
|
||||
preview={false}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Spin tip="生成二维码..." />
|
||||
<div className="customer-auth-qr-placeholder">
|
||||
<Spin />
|
||||
<span>正在生成二维码...</span>
|
||||
</div>
|
||||
)}
|
||||
<Text type="secondary">{loginInfo?.message || '等待扫码'}</Text>
|
||||
<p>{loginInfo?.message || '等待扫码'}</p>
|
||||
{selectedTask.loginStatus === 'scanned' ? (
|
||||
<Tag color="cyan">
|
||||
<span className="customer-auth-status-pill is-scanned">
|
||||
{isWeibo ? '已扫码,请在微博客户端确认登录' : '已扫码,请在 App 确认'}
|
||||
</Tag>
|
||||
</span>
|
||||
) : null}
|
||||
<Button onClick={() => void handleStartLogin()}>重新获取二维码</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
<Button
|
||||
className="customer-auth-secondary-button"
|
||||
onClick={() => void handleStartLogin()}
|
||||
>
|
||||
重新获取二维码
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{bindingQrVisible ? (
|
||||
<Card
|
||||
title={
|
||||
selectedTask.bindingStatus === 'role_ready'
|
||||
? '第二步:确认游戏角色'
|
||||
: '第二步:扫码绑定游戏角色'
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" align="center" size={14} style={{ width: '100%' }}>
|
||||
<section className="customer-auth-section">
|
||||
<div className="customer-auth-section-heading">
|
||||
<span className="customer-auth-section-icon">
|
||||
<SafetyCertificateOutlined />
|
||||
</span>
|
||||
<div>
|
||||
<h2>
|
||||
{selectedTask.bindingStatus === 'role_ready' ? '确认绑定角色' : '扫码绑定角色'}
|
||||
</h2>
|
||||
<p>
|
||||
{selectedTask.bindingStatus === 'role_ready'
|
||||
? '当前账号已有绑定角色,请确认是否继续使用;如需更换,请重新扫码换绑。'
|
||||
: '请使用虎牙 App 扫码完成游戏角色绑定。'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="customer-auth-qr">
|
||||
{bindingQrPending && bindingInfo?.qrImage ? (
|
||||
<Image src={bindingInfo.qrImage} alt="游戏角色绑定二维码" width={260} />
|
||||
<div className="customer-auth-qr-image">
|
||||
<Image src={bindingInfo.qrImage} alt="游戏角色绑定二维码" width={232} />
|
||||
</div>
|
||||
) : bindingQrPending ? (
|
||||
<Spin tip="生成绑定二维码..." />
|
||||
<div className="customer-auth-qr-placeholder">
|
||||
<Spin />
|
||||
<span>正在生成绑定二维码...</span>
|
||||
</div>
|
||||
) : null}
|
||||
<Text type="secondary">
|
||||
<p>
|
||||
{bindingInfo?.message ||
|
||||
config.bindingInstructions ||
|
||||
'请使用虎牙 App 扫码完成游戏角色绑定。'}
|
||||
</Text>
|
||||
'扫码完成后,请回到本页面确认角色。'}
|
||||
</p>
|
||||
{bindingRole ? (
|
||||
<Alert
|
||||
type={roleChoiceRequired ? 'warning' : 'success'}
|
||||
showIcon
|
||||
message={
|
||||
roleChoiceRequired
|
||||
? `当前已绑定角色:${bindingRole}`
|
||||
: `已识别角色:${bindingRole}`
|
||||
}
|
||||
description={[bindingAccount, bindingChannel].filter(Boolean).join(' · ')}
|
||||
/>
|
||||
<div className="customer-auth-role-card">
|
||||
<span>{roleChoiceRequired ? '当前已绑定角色' : '已识别角色'}</span>
|
||||
<strong>{bindingRole}</strong>
|
||||
<small>{[bindingAccount, bindingChannel].filter(Boolean).join(' · ')}</small>
|
||||
</div>
|
||||
) : null}
|
||||
{selectedTask.bindingStatus === 'role_ready' ? (
|
||||
<Space direction="vertical" align="center" size={10}>
|
||||
<div className="customer-auth-button-stack">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
loading={bindingActionLoading}
|
||||
onClick={() => void handleConfirmBinding()}
|
||||
>
|
||||
{roleChoiceRequired
|
||||
? '使用当前角色'
|
||||
: bindingInfo?.rebindRequested
|
||||
? '确认换绑角色'
|
||||
: '确认绑定角色'}
|
||||
</Button>
|
||||
{roleChoiceRequired ? (
|
||||
<Text type="secondary">
|
||||
请确认这是本次需要的游戏角色;角色不正确时请重新扫码换绑。
|
||||
</Text>
|
||||
) : null}
|
||||
<Space wrap>
|
||||
<Button
|
||||
type="primary"
|
||||
className="customer-auth-secondary-button"
|
||||
size="large"
|
||||
block
|
||||
loading={bindingActionLoading}
|
||||
onClick={() => void handleConfirmBinding()}
|
||||
onClick={() => void handleStartRebind()}
|
||||
>
|
||||
{roleChoiceRequired
|
||||
? '使用当前角色'
|
||||
: bindingInfo?.rebindRequested
|
||||
? '确认换绑角色'
|
||||
: '确认绑定角色'}
|
||||
换绑其他角色
|
||||
</Button>
|
||||
{roleChoiceRequired ? (
|
||||
<Button
|
||||
danger
|
||||
loading={bindingActionLoading}
|
||||
onClick={() => void handleStartRebind()}
|
||||
>
|
||||
角色不对,重新扫码换绑
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
</Space>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{selectedTask?.bindingStatus === 'confirmed' &&
|
||||
selectedTask.executionStatus !== 'completed' ? (
|
||||
<Card>
|
||||
<Result
|
||||
status="info"
|
||||
title={isWeibo ? '微博账号已授权,客服正在处理' : '账号已绑定,客服正在处理'}
|
||||
subTitle={
|
||||
isWeibo
|
||||
? '客服会使用本次授权完成微博发帖,请稍后刷新页面查看结果。'
|
||||
: '客服会使用本次授权完成充值或赠送,请稍后刷新页面查看结果。'
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
<div className="customer-auth-state is-waiting">
|
||||
<div className="customer-auth-state-icon">
|
||||
<LoadingOutlined spin />
|
||||
</div>
|
||||
<h2>{isWeibo ? '微博账号已授权' : '账号角色已绑定'}</h2>
|
||||
<p>
|
||||
{isWeibo
|
||||
? '客服正在使用本次授权完成微博发帖,页面会自动更新。'
|
||||
: '客服正在处理本订单,页面会自动更新。'}
|
||||
</p>
|
||||
<Button
|
||||
className="customer-auth-secondary-button"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => void load()}
|
||||
>
|
||||
刷新状态
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</Space>
|
||||
|
||||
<CustomerPrivacyNote />
|
||||
</div>
|
||||
</CustomerAuthFrame>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomerAuthFrame(input: {
|
||||
platform?: CustomerSemiAutoTask['platform']
|
||||
eyebrow: string
|
||||
title: string
|
||||
children: ReactNode
|
||||
}) {
|
||||
const platformClass =
|
||||
input.platform === 'weibo' ? 'is-weibo' : input.platform === 'huya' ? 'is-huya' : ''
|
||||
return (
|
||||
<div className={`customer-auth-page ${platformClass}`}>
|
||||
<div className="customer-auth-shell">
|
||||
<header className="customer-auth-hero">
|
||||
<div className="customer-auth-brand">
|
||||
<span className="customer-auth-logo">
|
||||
<QrcodeOutlined />
|
||||
</span>
|
||||
<div>
|
||||
<span className="customer-auth-eyebrow">{input.eyebrow}</span>
|
||||
<h1>{input.title}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div className="customer-auth-warning">温馨提示:请使用本人账号完成授权</div>
|
||||
<main className="customer-auth-main">{input.children}</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomerTaskFacts(input: { task: CustomerSemiAutoTask | null }) {
|
||||
const task = input.task
|
||||
if (!task) return null
|
||||
const statusTone =
|
||||
task.displayStatus.key === 'credential_expired' || task.displayStatus.key === 'failed'
|
||||
? 'is-danger'
|
||||
: task.bindingStatus === 'confirmed'
|
||||
? 'is-success'
|
||||
: 'is-pending'
|
||||
return (
|
||||
<div className="customer-auth-facts">
|
||||
<div>
|
||||
<span>订单号</span>
|
||||
<strong>{task.orderNo}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>商品</span>
|
||||
<strong>
|
||||
{task.productName || task.skuName} × {task.quantity}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>平台</span>
|
||||
<strong>{task.platformLabel || task.platform}</strong>
|
||||
</div>
|
||||
{task.huyaUid ? (
|
||||
<div>
|
||||
<span>平台账号 ID</span>
|
||||
<strong>{task.huyaUid}</strong>
|
||||
</div>
|
||||
) : null}
|
||||
{task.platform === 'weibo' && task.platformNickname ? (
|
||||
<div>
|
||||
<span>账号昵称</span>
|
||||
<strong>{task.platformNickname}</strong>
|
||||
</div>
|
||||
) : null}
|
||||
{task.platform === 'huya' && task.bindGameRole ? (
|
||||
<div>
|
||||
<span>绑定角色</span>
|
||||
<strong>{task.bindGameRole}</strong>
|
||||
</div>
|
||||
) : null}
|
||||
{task.platform === 'huya' && task.bindGameChannel ? (
|
||||
<div>
|
||||
<span>区服 / 渠道</span>
|
||||
<strong>{task.bindGameChannel}</strong>
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<span>状态</span>
|
||||
<strong className={`customer-auth-status-pill ${statusTone}`}>
|
||||
{task.displayStatus.label}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomerPrivacyNote() {
|
||||
return (
|
||||
<div className="customer-auth-privacy">
|
||||
资料仅用于本次订单履约,请勿向他人泄露验证码或账号信息。
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -795,16 +881,3 @@ function isValidWeiboMobile(countryCode: string, value: string) {
|
||||
if (countryCode === '86') return /^1\d{10}$/.test(digits)
|
||||
return digits.length >= 5 && digits.length <= 15
|
||||
}
|
||||
|
||||
function resolveStep(task: CustomerSemiAutoTask | null, binding: CustomerBindingInfo | null) {
|
||||
if (!task) return 0
|
||||
if (task.platform === 'weibo') {
|
||||
if (task.executionStatus === 'completed') return 2
|
||||
if (task.bindingStatus === 'confirmed') return 1
|
||||
return 0
|
||||
}
|
||||
if (task.executionStatus === 'completed') return 3
|
||||
if (task.bindingStatus === 'confirmed') return 2
|
||||
if (task.loginStatus === 'authenticated' || binding) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user