Files
order_site/apps/frontend/src/views/admin/tasks/AdminTasksView.vue
T

512 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref } from "vue";
import { useAdminAction } from "@/composables/useAdminAction";
import { useAdminListPage } from "@/composables/useAdminListPage";
import AdminPageHeader from "@/components/admin/AdminPageHeader.vue";
import AdminStatusTag from "@/components/admin/AdminStatusTag.vue";
import {
closeAdminTask,
fetchAdminTasks,
markAdminTaskManualReview,
regenerateAdminTaskClaimLink,
retryAdminTask,
} from "@/services/admin";
import type { AdminTaskListItem } from "@/types/admin";
import { hasAdminRole } from "@/utils/admin-auth";
import {
formatBindingRoleSummary,
formatBindingCountSummary,
} from "@/utils/admin-display";
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 canManageTaskLifecycle = hasAdminRole("operator");
const canCloseTasks = hasAdminRole("support");
type AdminTaskActionKey =
| "retry"
| "regenerate_claim_link"
| "manual_review"
| "close";
const {
loading,
errorMessage,
items,
pagination,
loadPage: loadTasks,
} = useAdminListPage<AdminTaskListItem>({
defaultErrorMessage: "读取任务列表失败",
fetchPage: (page, pageSize) =>
fetchAdminTasks({
page,
pageSize,
status: status.value.trim(),
taskNo: taskNo.value.trim(),
platformOrderId: platformOrderId.value.trim(),
roleId: roleId.value.trim(),
skuCode: skuCode.value.trim(),
dateFrom: dateFrom.value.trim(),
dateTo: dateTo.value.trim(),
}),
});
const { actionLoadingId, runAction } = useAdminAction();
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;
},
successMessage: "任务已重试",
confirmText: `确认重试任务 ${item.taskNo} 吗?`,
onAfterAction: async () => {
await loadTasks();
},
});
return;
}
if (actionKey === "regenerate_claim_link") {
void runAction({
id: item.taskId,
action: async () => {
const response = await regenerateAdminTaskClaimLink(item.taskId);
if (response.data.claimUrl) lastClaimUrl.value = response.data.claimUrl;
},
successMessage: "领取链接已重新生成",
confirmText: `确认重新生成任务 ${item.taskNo} 的领取链接吗?旧链接会失效。`,
onAfterAction: async () => {
await loadTasks();
},
});
return;
}
if (actionKey === "manual_review") {
void runAction({
id: item.taskId,
action: () => markAdminTaskManualReview(item.taskId),
successMessage: "任务已转人工处理",
confirmText: `确认将任务 ${item.taskNo} 转为人工处理吗?`,
onAfterAction: async () => {
await loadTasks();
},
});
return;
}
void runAction({
id: item.taskId,
action: () => closeAdminTask(item.taskId),
successMessage: "任务已关闭",
confirmText: `确认关闭任务 ${item.taskNo} 吗?关闭后不会自动继续推进。`,
onAfterAction: async () => {
await loadTasks();
},
});
}
function resetFilters() {
status.value = "";
taskNo.value = "";
platformOrderId.value = "";
roleId.value = "";
skuCode.value = "";
dateFrom.value = "";
dateTo.value = "";
void loadTasks(1);
}
</script>
<template>
<div class="tasks-page list-page">
<AdminPageHeader
title="交付任务"
description="更紧凑地查看任务状态、角色绑定、库存凭据和异常信息。"
>
<template #extra>
<span class="total-badge">共 {{ pagination.total }} 条任务</span>
</template>
</AdminPageHeader>
<el-card shadow="never" class="section-card">
<template #header>
<div class="card-header">
<span class="card-title">查询任务</span>
<span class="card-desc"
>按状态、任务号、订单号、角色标识、SKU 和日期筛选。</span
>
</div>
</template>
<div class="filter-grid">
<el-select v-model="status" placeholder="状态" clearable>
<el-option
v-for="opt in adminTaskStatusOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-input
v-model="taskNo"
placeholder="任务号(模糊匹配)"
clearable
@keyup.enter="loadTasks(1)"
/>
<el-input
v-model="platformOrderId"
placeholder="平台订单号"
clearable
@keyup.enter="loadTasks(1)"
/>
<el-input
v-model="roleId"
placeholder="角色唯一标识"
clearable
@keyup.enter="loadTasks(1)"
/>
<el-input
v-model="skuCode"
placeholder="商品 SKU"
clearable
@keyup.enter="loadTasks(1)"
/>
<el-date-picker
v-model="dateFrom"
type="date"
placeholder="开始日期"
value-format="YYYY-MM-DD"
/>
<el-date-picker
v-model="dateTo"
type="date"
placeholder="结束日期"
value-format="YYYY-MM-DD"
/>
<div class="filter-buttons">
<el-button round @click="resetFilters">重置</el-button>
<el-button round type="primary" @click="loadTasks(1)">查询</el-button>
</div>
</div>
</el-card>
<el-alert
v-if="errorMessage"
:title="errorMessage"
type="error"
show-icon
:closable="false"
class="mt-4"
/>
<el-alert
v-if="lastClaimUrl"
:title="`最近生成的领取链接:${lastClaimUrl}`"
type="info"
show-icon
:closable="false"
class="mt-4"
/>
<el-card
shadow="never"
class="section-card"
v-loading="loading"
element-loading-text="任务列表加载中"
>
<template v-if="items.length === 0 && !loading">
<el-empty description="当前筛选条件下暂无任务" :image-size="60" />
</template>
<template v-else>
<div class="tasks-toolbar table-toolbar">
<strong>任务列表</strong>
<span
>第 {{ pagination.page }} 页,当前展示 {{ items.length }} 条</span
>
</div>
<el-table
:data="items"
stripe
size="small"
class="tasks-table data-table"
>
<el-table-column label="任务 / 商品" min-width="220">
<template #default="{ row }">
<div class="cell-stack">
<RouterLink
:to="`/admin/tasks/${row.taskId}`"
class="task-no id-link"
:title="row.taskNo"
>
{{ row.taskNo }}
</RouterLink>
<div class="cell-meta-row">
<span class="compact-chip" :title="row.platformOrderId"
>订单 {{ row.platformOrderId }}</span
>
<span class="compact-chip compact-chip--muted"
>重试 {{ row.retryCount }} 次</span
>
</div>
<span class="cell-title" :title="row.skuName || row.skuCode">{{
row.skuName || row.skuCode
}}</span>
<span class="cell-subline">SKU {{ row.skuCode || "-" }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="进度" min-width="160">
<template #default="{ row }">
<div class="cell-stack">
<div class="progress-row">
<span class="progress-label">任务</span>
<AdminStatusTag :status="row.status" />
</div>
<div class="progress-row">
<span class="progress-label">系统</span>
<AdminStatusTag :status="row.systemBindingStatus" />
</div>
<div class="progress-row">
<span class="progress-label">完整</span>
<AdminStatusTag :status="row.userBindingStatus" />
</div>
</div>
</template>
</el-table-column>
<el-table-column label="角色 / 凭据" min-width="180">
<template #default="{ row }">
<div class="cell-stack">
<span
class="cell-title"
:title="row.roleName || row.roleId || '-'"
>{{ row.roleName || "-" }}</span
>
<span class="cell-subline" :title="row.roleId || '-'"
>角色ID {{ row.roleId || "-" }}</span
>
<span class="cell-subline"
>登录 {{ row.loginType || "-" }}</span
>
<span
class="cell-subline"
:title="row.inventoryDisplayMasked || '-'"
>凭据 {{ row.inventoryDisplayMasked || "-" }}</span
>
<span
class="cell-subline"
:title="
formatBindingRoleSummary(
row.bindingSummary.roleKeys,
'无绑定角色'
)
"
>绑定
{{
formatBindingRoleSummary(
row.bindingSummary.roleKeys,
"无绑定角色"
)
}}</span
>
<span
class="cell-subline cell-subline--strong"
:title="formatBindingCountSummary(row.bindingSummary)"
>{{ formatBindingCountSummary(row.bindingSummary) }}</span
>
</div>
</template>
</el-table-column>
<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 v-if="row.claimedAt" class="cell-subline"
>领取 {{ formatAdminDateTime(row.claimedAt) }}</span
>
<span v-if="row.roleConfirmedAt" class="cell-subline"
>确认 {{ formatAdminDateTime(row.roleConfirmedAt) }}</span
>
<span v-if="row.redeemedAt" class="cell-subline"
>兑换 {{ formatAdminDateTime(row.redeemedAt) }}</span
>
</div>
</template>
</el-table-column>
<el-table-column label="异常 / 操作" min-width="220">
<template #default="{ row }">
<div class="cell-stack">
<span class="cell-error" :title="row.lastError || '-'">{{
row.lastError || "当前无异常"
}}</span>
<div class="action-stack">
<el-button
v-if="canManageTaskLifecycle"
:disabled="
row.executorKey === 'manual_dispatch' ||
![
'retry_pending',
'manual_review',
'waiting_inventory',
].includes(row.status)
"
:loading="actionLoadingId === row.taskId"
size="small"
type="danger"
plain
@click="
runAction({
id: row.taskId,
action: async () => {
const response = await retryAdminTask(row.taskId);
if (response.data.claimUrl)
lastClaimUrl = response.data.claimUrl;
},
successMessage: '任务已重试',
confirmText: `确认重试任务 ${row.taskNo} 吗?`,
onAfterAction: async () => {
await loadTasks();
},
})
"
>
重试
</el-button>
<el-button
v-if="
canManageTaskLifecycle &&
row.executorKey !== 'manual_dispatch'
"
size="small"
@click="handleActionCommand('regenerate_claim_link', row)"
>
重发链接
</el-button>
<el-button
v-if="canManageTaskLifecycle"
size="small"
@click="handleActionCommand('manual_review', row)"
>
转人工
</el-button>
<el-button
v-if="
canCloseTasks &&
!['redeemed', 'closed'].includes(row.status)
"
size="small"
type="danger"
@click="handleActionCommand('close', row)"
>
关闭任务
</el-button>
</div>
</div>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="pagination.total > 0"
v-model:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
layout="total, prev, pager, next"
class="pagination-center"
@current-change="loadTasks"
/>
</template>
</el-card>
</div>
</template>
<style scoped>
@import "../../../styles/admin-list-pages.css";
.filter-grid {
grid-template-columns:
minmax(150px, 0.75fr) minmax(220px, 1fr) minmax(220px, 1fr)
minmax(210px, 0.95fr);
}
.filter-grid :deep(.el-date-editor) {
width: 100%;
}
.progress-row {
display: flex;
align-items: center;
gap: var(--space-2);
min-width: 0;
}
.progress-row :deep(.el-tag) {
min-width: 0;
max-width: calc(100% - 42px);
}
.progress-row :deep(.el-tag__content) {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.progress-label {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 34px;
min-height: 22px;
padding: 0 7px;
border-radius: var(--radius-full);
background: #f7f9fc;
border: 1px solid var(--border-default);
color: var(--text-secondary);
font-size: 11px;
font-weight: 700;
flex: 0 0 auto;
}
.cell-error {
display: block;
min-width: 0;
line-height: 1.55;
color: #475467;
word-break: break-word;
font-size: var(--text-xs);
}
.cell-subline--strong {
color: var(--text-primary);
font-weight: 600;
}
.action-stack {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
align-items: center;
}
.action-stack :deep(.el-button) {
flex: 0 0 auto;
margin-left: 0;
}
</style>