添加快手 Cloud 换绑角色功能
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user