添加快手 Cloud 换绑角色功能

This commit is contained in:
yml
2026-05-30 11:23:52 +08:00
parent 681ff44cbc
commit 1c50569ea9
22 changed files with 1537 additions and 953 deletions
@@ -1,38 +1,34 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { onMounted, ref } from 'vue'
import { useAdminAction } from "@/composables/useAdminAction";
import { useAdminListPage } from "@/composables/useAdminListPage";
import AdminPageHeader from "@/components/admin/AdminPageHeader.vue";
import { useAdminAction } from '@/composables/useAdminAction'
import { useAdminListPage } from '@/composables/useAdminListPage'
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue'
import {
closeAdminTask,
fetchAdminTasks,
markAdminTaskManualReview,
regenerateAdminTaskClaimLink,
rebindAdminTaskKuaishouCloudRole,
retryAdminTask,
} from "@/services/admin";
import type { AdminTaskListItem } from "@/types/admin";
import { hasAdminRole } from "@/utils/admin-auth";
import { adminTaskStatusOptions } from "@/utils/admin-options";
import { formatAdminDateTime } from "@/utils/admin-time";
} from '@/services/admin'
import type { AdminTaskListItem } from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth'
import { adminTaskStatusOptions } from '@/utils/admin-options'
import { formatAdminDateTime } from '@/utils/admin-time'
const status = ref("");
const taskNo = ref("");
const platformOrderId = ref("");
const roleId = ref("");
const skuCode = ref("");
const dateFrom = ref("");
const dateTo = ref("");
const lastClaimUrl = ref("");
const status = ref('')
const taskNo = ref('')
const platformOrderId = ref('')
const roleId = ref('')
const skuCode = ref('')
const dateFrom = ref('')
const dateTo = ref('')
const lastClaimUrl = ref('')
const canManageTaskLifecycle = hasAdminRole("operator");
const canCloseTasks = hasAdminRole("support");
const canManageTaskLifecycle = hasAdminRole('operator')
const canCloseTasks = hasAdminRole('support')
type AdminTaskActionKey =
| "retry"
| "regenerate_claim_link"
| "manual_review"
| "close";
type AdminTaskActionKey = 'retry' | 'rebind_role' | 'manual_review' | 'close'
const {
loading,
@@ -41,7 +37,7 @@ const {
pagination,
loadPage: loadTasks,
} = useAdminListPage<AdminTaskListItem>({
defaultErrorMessage: "读取任务列表失败",
defaultErrorMessage: '读取任务列表失败',
fetchPage: (page, pageSize) =>
fetchAdminTasks({
page,
@@ -54,141 +50,144 @@ const {
dateFrom: dateFrom.value.trim(),
dateTo: dateTo.value.trim(),
}),
});
})
const { actionLoadingId, runAction } = useAdminAction();
const { actionLoadingId, runAction } = useAdminAction()
function handleActionCommand(
actionKey: AdminTaskActionKey,
item: AdminTaskListItem
) {
if (actionKey === "retry") {
function handleActionCommand(actionKey: AdminTaskActionKey, item: AdminTaskListItem) {
if (actionKey === 'retry') {
void runAction({
id: item.taskId,
action: async () => {
const response = await retryAdminTask(item.taskId);
if (response.data.claimUrl) lastClaimUrl.value = response.data.claimUrl;
const response = await retryAdminTask(item.taskId)
if (response.data.claimUrl) lastClaimUrl.value = response.data.claimUrl
},
successMessage: "任务已重试",
successMessage: '任务已重试',
confirmText: `确认重试任务 ${item.taskNo} 吗?`,
onAfterAction: async () => {
await loadTasks();
await loadTasks()
},
});
return;
})
return
}
if (actionKey === "regenerate_claim_link") {
if (actionKey === 'rebind_role') {
void runAction({
id: item.taskId,
action: async () => {
const response = await regenerateAdminTaskClaimLink(item.taskId);
if (response.data.claimUrl) lastClaimUrl.value = response.data.claimUrl;
const response = await rebindAdminTaskKuaishouCloudRole(item.taskId)
if (response.data.claimUrl) lastClaimUrl.value = response.data.claimUrl
},
successMessage: "领取链接已重新生成",
confirmText: `确认重新生成任务 ${item.taskNo} 的领取链接吗?旧链接会失效`,
successMessage: '角色换绑资源已准备完成',
confirmText: `确认任务 ${item.taskNo} 换绑角色吗?当前虚拟号会退还,并生成新的绑定二维码`,
onAfterAction: async () => {
await loadTasks();
await loadTasks()
},
});
return;
})
return
}
if (actionKey === "manual_review") {
if (actionKey === 'manual_review') {
void runAction({
id: item.taskId,
action: () => markAdminTaskManualReview(item.taskId),
successMessage: "任务已转人工处理",
successMessage: '任务已转人工处理',
confirmText: `确认将任务 ${item.taskNo} 转为人工处理吗?`,
onAfterAction: async () => {
await loadTasks();
await loadTasks()
},
});
return;
})
return
}
void runAction({
id: item.taskId,
action: () => closeAdminTask(item.taskId),
successMessage: "任务已关闭",
successMessage: '任务已关闭',
confirmText: `确认关闭任务 ${item.taskNo} 吗?关闭后不会自动继续推进。`,
onAfterAction: async () => {
await loadTasks();
await loadTasks()
},
});
})
}
function resetFilters() {
status.value = "";
taskNo.value = "";
platformOrderId.value = "";
roleId.value = "";
skuCode.value = "";
dateFrom.value = "";
dateTo.value = "";
void loadTasks(1);
status.value = ''
taskNo.value = ''
platformOrderId.value = ''
roleId.value = ''
skuCode.value = ''
dateFrom.value = ''
dateTo.value = ''
void loadTasks(1)
}
function resolveResourceStageLabel(status: string) {
const normalized = String(status || "").trim();
const normalized = String(status || '').trim()
const labelMap: Record<string, string> = {
resource_ready: "资源已准备",
pending_prepare: "待准备资源",
pending_payment: "待支付",
pending_binding_prepare: "待准备资源",
retry_pending: "待重试",
manual_review: "人工处理",
resource_exception: "资源异常",
closed: "已关闭",
expired: "已过期",
completed: "已完成",
not_started: "未开始",
};
resource_ready: '资源已准备',
pending_prepare: '待准备资源',
pending_payment: '待支付',
pending_binding_prepare: '待准备资源',
retry_pending: '待重试',
manual_review: '人工处理',
resource_exception: '资源异常',
closed: '已关闭',
expired: '已过期',
completed: '已完成',
not_started: '未开始',
}
return labelMap[normalized] || normalized || "-";
return labelMap[normalized] || normalized || '-'
}
function resolveCustomerStageLabel(status: string) {
const normalized = String(status || "").trim();
const normalized = String(status || '').trim()
const labelMap: Record<string, string> = {
not_started: "待打开链接",
waiting_customer: "待客户绑定",
link_opened: "已打开链接",
customer_processing: "处理中",
customer_confirmed: "已提交",
customer_completed: "客户已完成",
customer_exception: "客户步骤异常",
closed: "已关闭",
expired: "已过期",
};
not_started: '待打开链接',
waiting_customer: '待客户绑定',
link_opened: '已打开链接',
customer_processing: '处理中',
customer_confirmed: '已提交',
customer_completed: '客户已完成',
customer_exception: '客户步骤异常',
closed: '已关闭',
expired: '已过期',
}
return labelMap[normalized] || normalized || "-";
return labelMap[normalized] || normalized || '-'
}
function resolveTaskMainText(item: AdminTaskListItem) {
if (item.lastError) return "需要关注";
if (["failed", "manual_review", "retry_pending"].includes(item.status)) return "待处理";
if (["redeemed", "completed", "closed"].includes(item.status)) return "已完成";
if (item.customerStatus === "waiting_customer") return "等待客户绑定";
if (item.resourceStatus === "resource_ready") return "资源已准备";
if (item.status === "pending_binding_prepare") return "待准备资源";
return item.status || "处理中";
if (item.lastError) return '需要关注'
if (['failed', 'manual_review', 'retry_pending'].includes(item.status)) return '待处理'
if (['redeemed', 'completed', 'closed'].includes(item.status)) return '已完成'
if (item.customerStatus === 'waiting_customer') return '等待客户绑定'
if (item.resourceStatus === 'resource_ready') return '资源已准备'
if (item.status === 'pending_binding_prepare') return '待准备资源'
return item.status || '处理中'
}
function resolveTaskTone(item: AdminTaskListItem) {
if (item.lastError || ["failed", "manual_review", "retry_pending"].includes(item.status)) return "danger";
if (["redeemed", "completed", "closed"].includes(item.status)) return "success";
return "warning";
if (item.lastError || ['failed', 'manual_review', 'retry_pending'].includes(item.status))
return 'danger'
if (['redeemed', 'completed', 'closed'].includes(item.status)) return 'success'
return 'warning'
}
function canRebindListTask(item: AdminTaskListItem) {
return (
canManageTaskLifecycle &&
item.executorKey === 'kuaishou_ct_assisted' &&
['waiting_binding', 'role_confirmed', 'manual_review', 'retry_pending'].includes(item.status)
)
}
onMounted(() => {
void loadTasks(1);
});
void loadTasks(1)
})
</script>
<template>
<div class="tasks-page list-page">
<AdminPageHeader
title="交付任务"
description="查看自动履约任务、客户步骤和异常处理。"
>
<AdminPageHeader title="交付任务" description="查看自动履约任务、客户步骤和异常处理。">
<template #extra>
<span class="total-badge"> {{ pagination.total }} 条任务</span>
</template>
@@ -198,9 +197,7 @@ onMounted(() => {
<template #header>
<div class="card-header">
<span class="card-title">查询任务</span>
<span class="card-desc"
>按状态任务号订单号商品和日期筛选</span
>
<span class="card-desc">按状态任务号订单号商品和日期筛选</span>
</div>
</template>
<div class="filter-grid">
@@ -224,12 +221,7 @@ onMounted(() => {
clearable
@keyup.enter="loadTasks(1)"
/>
<el-input
v-model="roleId"
placeholder="角色 ID"
clearable
@keyup.enter="loadTasks(1)"
/>
<el-input v-model="roleId" placeholder="角色 ID" clearable @keyup.enter="loadTasks(1)" />
<el-input
v-model="skuCode"
placeholder="商品名 / SKU"
@@ -265,7 +257,7 @@ onMounted(() => {
/>
<el-alert
v-if="lastClaimUrl"
:title="`最近生成的领取链接:${lastClaimUrl}`"
:title="`最近可用领取链接:${lastClaimUrl}`"
type="info"
show-icon
:closable="false"
@@ -285,17 +277,10 @@ onMounted(() => {
<template v-else>
<div class="tasks-toolbar table-toolbar">
<strong>任务列表</strong>
<span
> {{ pagination.page }} 当前展示 {{ items.length }} </span
>
<span> {{ pagination.page }} 当前展示 {{ items.length }} </span>
</div>
<el-table
:data="items"
stripe
size="small"
class="tasks-table data-table"
>
<el-table :data="items" stripe size="small" class="tasks-table data-table">
<el-table-column label="任务 / 订单" min-width="230">
<template #default="{ row }">
<div class="cell-stack">
@@ -310,9 +295,7 @@ onMounted(() => {
<span class="compact-chip" :title="row.platformOrderId"
>订单 {{ row.platformOrderId }}</span
>
<span class="compact-chip compact-chip--muted"
>重试 {{ row.retryCount }} </span
>
<span class="compact-chip compact-chip--muted">重试 {{ row.retryCount }} </span>
</div>
</div>
</template>
@@ -323,7 +306,7 @@ onMounted(() => {
<span class="cell-title" :title="row.skuName || row.skuCode">{{
row.skuName || row.skuCode
}}</span>
<span class="cell-subline">SKU {{ row.skuCode || "-" }}</span>
<span class="cell-subline">SKU {{ row.skuCode || '-' }}</span>
</div>
</template>
</el-table-column>
@@ -334,11 +317,11 @@ onMounted(() => {
{{ resolveTaskMainText(row) }}
</span>
<p class="task-status-note">
客户 {{ resolveCustomerStageLabel(row.customerStatus) }}
· 资源 {{ resolveResourceStageLabel(row.resourceStatus) }}
客户 {{ resolveCustomerStageLabel(row.customerStatus) }} · 资源
{{ resolveResourceStageLabel(row.resourceStatus) }}
</p>
<p v-if="row.roleName || row.roleId" class="task-status-note">
角色 {{ row.roleName || "-" }} / {{ row.roleId || "-" }}
角色 {{ row.roleName || '-' }} / {{ row.roleId || '-' }}
</p>
</div>
</template>
@@ -346,12 +329,8 @@ onMounted(() => {
<el-table-column label="时间" min-width="180">
<template #default="{ row }">
<div class="cell-stack">
<span class="cell-subline"
>创建 {{ formatAdminDateTime(row.createdAt) }}</span
>
<span class="cell-subline"
>更新 {{ formatAdminDateTime(row.updatedAt) }}</span
>
<span class="cell-subline">创建 {{ formatAdminDateTime(row.createdAt) }}</span>
<span class="cell-subline">更新 {{ formatAdminDateTime(row.updatedAt) }}</span>
<span v-if="row.claimedAt" class="cell-subline"
>领取 {{ formatAdminDateTime(row.claimedAt) }}</span
>
@@ -368,7 +347,7 @@ onMounted(() => {
<template #default="{ row }">
<div class="cell-stack">
<span class="cell-error" :title="row.lastError || '-'">{{
row.lastError || "当前无异常"
row.lastError || '当前无异常'
}}</span>
<div class="action-stack">
<el-button
@@ -386,14 +365,11 @@ onMounted(() => {
重试
</el-button>
<el-button
v-if="
canManageTaskLifecycle &&
row.executorKey !== 'manual_dispatch'
"
v-if="canRebindListTask(row)"
size="small"
@click="handleActionCommand('regenerate_claim_link', row)"
@click="handleActionCommand('rebind_role', row)"
>
重发链接
换绑角色
</el-button>
<el-button
v-if="canManageTaskLifecycle"
@@ -403,10 +379,7 @@ onMounted(() => {
转人工
</el-button>
<el-button
v-if="
canCloseTasks &&
!['redeemed', 'closed'].includes(row.status)
"
v-if="canCloseTasks && !['redeemed', 'closed'].includes(row.status)"
size="small"
type="danger"
@click="handleActionCommand('close', row)"
@@ -434,7 +407,7 @@ onMounted(() => {
</template>
<style scoped>
@import "../../../styles/admin-list-pages.css";
@import '../../../styles/admin-list-pages.css';
.filter-grid {
grid-template-columns:
@@ -465,19 +438,19 @@ onMounted(() => {
white-space: nowrap;
}
.task-main-chip[data-tone="success"] {
.task-main-chip[data-tone='success'] {
color: #0f766e;
background: var(--color-success-light);
border-color: #a6f4c5;
}
.task-main-chip[data-tone="warning"] {
.task-main-chip[data-tone='warning'] {
color: #b54708;
background: var(--color-warning-light);
border-color: #fedf89;
}
.task-main-chip[data-tone="danger"] {
.task-main-chip[data-tone='danger'] {
color: var(--color-danger-dark);
background: var(--color-danger-light);
border-color: #fecaca;