优化半自动履约客户页与后台操作布局

用户侧统一显示等待客服处理并精简平台标题;移除 Cookie 等专业表述;后台操作改为两行行内按钮,取消操作下拉。
This commit is contained in:
yml
2026-11-14 01:23:15 +08:00
parent d178556f03
commit 9d738becd3
6 changed files with 100 additions and 145 deletions
@@ -366,7 +366,7 @@ export async function createSemiAutoMock(
pageDescription,
loginInstructions:
platform === 'weibo'
? '真实登录:使用微博绑定手机号接收验证码登录,或使用微博 App 扫码登录。登录仅用于本次订单履约,不会在页面展示账号 Cookie。'
? '真实登录:使用微博绑定手机号接收验证码登录,或使用微博 App 扫码登录。登录信息仅用于本次订单履约。'
: connectorMode === 'mock'
? 'Mock 登录:点击获取二维码后,页面轮询两次会自动模拟登录成功。'
: '真实登录:点击获取二维码后,请使用虎牙 App 扫码并在手机上确认登录。',
@@ -172,7 +172,7 @@ function displayStatus(task: TaskRow) {
return { key: 'failed', label: '需客服处理' }
}
if (task.login_status === 'authenticated') {
return { key: 'waiting_operator', label: 'CK 已获取,等待客服处理' }
return { key: 'waiting_operator', label: '等待客服处理' }
}
if (task.login_status === 'scanned') return { key: 'login_scanned', label: '已扫码,确认中' }
if (task.login_status === 'qr_pending') return { key: 'waiting_login', label: '等待客户登录' }
@@ -141,7 +141,7 @@ test(
)
const authenticatedList = await listAdminSemiAutoTasks({ keyword: orderNo })
assert.equal(authenticatedList.items[0]?.loginStatus, 'authenticated')
assert.equal(authenticatedList.items[0]?.displayStatus.label, 'CK 已获取,等待客服处理')
assert.equal(authenticatedList.items[0]?.displayStatus.label, '等待客服处理')
const exported = await exportAdminSemiAutoCredential(taskId)
assert.equal(exported.credentialId, credentialId)
assert.equal(exported.platform, 'huya')
@@ -2,10 +2,7 @@ import {
CheckCircleOutlined,
ChromeOutlined,
CopyOutlined,
DownOutlined,
KeyOutlined,
LinkOutlined,
MoreOutlined,
ReloadOutlined,
SendOutlined,
UserSwitchOutlined,
@@ -17,7 +14,6 @@ import {
Card,
Descriptions,
Drawer,
Dropdown,
Form,
Input,
InputNumber,
@@ -288,76 +284,6 @@ export default function AdminSemiAutoTasksPage() {
})
}
function credentialMenu(task: SemiAutoTask) {
return {
items: [
{
key: 'copy',
icon: <KeyOutlined />,
label: '复制最终 CK',
disabled: !task.credentialReady || credentialExportMutation.isPending,
},
{
key: 'inject',
icon: <ChromeOutlined />,
label: '注入浏览器',
disabled: !taskCanUseCredential(task) || 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([])
@@ -461,46 +387,88 @@ export default function AdminSemiAutoTasksPage() {
{
title: '操作',
key: 'actions',
width: 340,
width: 520,
render: (_, task) => (
<Space wrap>
<Button size="small" onClick={() => setDetailTaskId(task.id)}>
详情
</Button>
{taskNeedsCustomerReauth(task) ? (
<Space direction="vertical" size={6} style={{ width: '100%' }}>
<Space wrap>
<Button size="small" onClick={() => setDetailTaskId(task.id)}>
详情
</Button>
{taskNeedsCustomerReauth(task) ? (
<Button
size="small"
type="primary"
icon={<SendOutlined />}
loading={reauthMutation.isPending}
onClick={() => confirmReauth(task)}
>
重发授权
</Button>
) : null}
<Button
size="small"
type="primary"
icon={<SendOutlined />}
loading={reauthMutation.isPending}
onClick={() => confirmReauth(task)}
icon={<KeyOutlined />}
disabled={!task.credentialReady}
loading={credentialExportMutation.isPending}
onClick={() => confirmCopyCredential(task)}
>
重发授权
复制 CK
</Button>
) : null}
<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 />}
icon={<ChromeOutlined />}
disabled={!taskCanUseCredential(task)}
onClick={() => openComplete(task)}
loading={injectionMutation.isPending}
onClick={() => injectionMutation.mutate(task)}
>
完成
注入浏览器
</Button>
) : null}
<Dropdown menu={taskMenu(task)} trigger={['click']}>
<Button size="small" icon={<MoreOutlined />}>
更多
</Space>
<Space wrap>
<Button
size="small"
icon={<CopyOutlined />}
onClick={() => void copyCustomerLink(task)}
>
客户链接
</Button>
</Dropdown>
<Button
size="small"
icon={<ReloadOutlined />}
loading={syncMutation.isPending}
onClick={() => syncMutation.mutate(task.id)}
>
刷新
</Button>
{!task.assignedAdminUserId ? (
<Button
size="small"
icon={<UserSwitchOutlined />}
loading={claimMutation.isPending}
onClick={() => claimMutation.mutate(task.id)}
>
领取
</Button>
) : null}
{task.executionStatus !== 'completed' &&
task.executionStatus !== 'cancelled' &&
!taskNeedsCustomerReauth(task) ? (
<Button
size="small"
type="primary"
icon={<CheckCircleOutlined />}
disabled={!taskCanUseCredential(task)}
onClick={() => openComplete(task)}
>
完成
</Button>
) : null}
{task.executionStatus !== 'completed' && task.executionStatus !== 'cancelled' ? (
<Button size="small" danger onClick={() => confirmCancel(task)}>
取消
</Button>
) : null}
</Space>
</Space>
),
},
@@ -690,11 +658,22 @@ export default function AdminSemiAutoTasksPage() {
>
刷新客户状态
</Button>
<Dropdown menu={credentialMenu(detail.task)} trigger={['click']}>
<Button icon={<KeyOutlined />}>
CK 操作 <DownOutlined />
</Button>
</Dropdown>
<Button
icon={<KeyOutlined />}
disabled={!detail.task.credentialReady}
loading={credentialExportMutation.isPending}
onClick={() => confirmCopyCredential(detail.task)}
>
复制 CK
</Button>
<Button
icon={<ChromeOutlined />}
disabled={!taskCanUseCredential(detail.task)}
loading={injectionMutation.isPending}
onClick={() => injectionMutation.mutate(detail.task)}
>
注入浏览器
</Button>
</Space>
</Card>
<Card size="small" title="时间线">
@@ -251,14 +251,13 @@ export default function CustomerFulfillmentPage() {
}
const config = selectedTask?.semiAutoConfig || {}
const title = config.pageTitle || (isWeibo ? '微博账号授权' : '账号授权与角色绑定')
const displayTitle = isWeibo ? '微博账号登录授权' : title
const displayTitle = isWeibo ? '微博账号登录授权' : '虎牙账号登录授权'
const loginQrVisible =
selectedTask && ['qr_pending', 'scanned'].includes(selectedTask.loginStatus)
if (loading) {
return (
<CustomerAuthFrame eyebrow="账号授权" title="正在读取订单信息">
<CustomerAuthFrame title="正在读取订单信息">
<div className="customer-auth-loading">
<Spin indicator={<LoadingOutlined spin />} />
<span>正在读取订单履约信息...</span>
@@ -269,7 +268,7 @@ export default function CustomerFulfillmentPage() {
if (loadError || tasks.length === 0) {
return (
<CustomerAuthFrame eyebrow="账号授权" title="暂时无法打开订单">
<CustomerAuthFrame title="暂时无法打开订单">
<div className="customer-auth-state is-error">
<h2>订单信息读取失败</h2>
<p>{loadError || '该订单没有需要客户授权或绑定的任务。'}</p>
@@ -280,11 +279,7 @@ export default function CustomerFulfillmentPage() {
if (selectedTask?.executionStatus === 'completed') {
return (
<CustomerAuthFrame
platform={selectedTask.platform}
eyebrow={isWeibo ? '微博账号授权' : '虎牙账号授权'}
title={displayTitle}
>
<CustomerAuthFrame platform={selectedTask.platform} title={displayTitle}>
<CustomerTaskFacts task={selectedTask} />
<div className="customer-auth-state is-success">
<div className="customer-auth-state-icon">
@@ -309,11 +304,7 @@ export default function CustomerFulfillmentPage() {
}
return (
<CustomerAuthFrame
platform={selectedTask?.platform}
eyebrow={isWeibo ? '微博账号授权' : '虎牙账号授权'}
title={displayTitle}
>
<CustomerAuthFrame platform={selectedTask?.platform} title={displayTitle}>
<div className="customer-auth-content-stack">
{tasks.length > 1 ? (
<div className="customer-auth-task-switcher">
@@ -503,7 +494,6 @@ export default function CustomerFulfillmentPage() {
function CustomerAuthFrame(input: {
platform?: CustomerSemiAutoTask['platform']
eyebrow: string
title: string
children: ReactNode
}) {
@@ -515,9 +505,6 @@ function CustomerAuthFrame(input: {
<header className="customer-auth-hero">
<div className="customer-auth-brand">
<div>
{input.platform !== 'weibo' ? (
<span className="customer-auth-eyebrow">{input.eyebrow}</span>
) : null}
<h1>{input.title}</h1>
</div>
</div>
+5 -16
View File
@@ -1502,21 +1502,14 @@
box-shadow: inset 0 0 24px rgba(255, 138, 0, 0.06);
}
.customer-auth-eyebrow {
display: block;
margin-bottom: 4px;
color: rgba(255, 255, 255, 0.78);
font-size: 15px;
font-weight: 500;
}
.customer-auth-brand h1 {
margin: 0;
color: #fff;
font-size: clamp(26px, 4.2vw, 38px);
line-height: 1.25;
font-weight: 700;
font-size: clamp(18px, 2.3vw, 22px);
line-height: 1.3;
font-weight: 600;
letter-spacing: 0;
white-space: nowrap;
overflow-wrap: anywhere;
}
@@ -2087,12 +2080,8 @@
background-position: 58% center;
}
.customer-auth-eyebrow {
font-size: 13px;
}
.customer-auth-brand h1 {
font-size: 26px;
font-size: 18px;
}
.customer-auth-warning {