init
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
|
||||
import {
|
||||
closeAdminTask,
|
||||
fetchAdminTaskScreenshot,
|
||||
fetchAdminTaskDetail,
|
||||
markAdminTaskManualReview,
|
||||
regenerateAdminTaskClaimLink,
|
||||
releaseAdminTaskCdk,
|
||||
retryAdminTask,
|
||||
} from '@/services/admin'
|
||||
import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
const route = useRoute()
|
||||
const loading = ref(true)
|
||||
const actionLoading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const detail = ref<AdminTaskDetail | null>(null)
|
||||
const lastClaimUrl = ref('')
|
||||
const screenshotPreviewUrl = ref('')
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchAdminTaskDetail(String(route.params.taskId || ''))
|
||||
detail.value = response.data
|
||||
await loadScreenshotPreview(response.data)
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取任务详情失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runAction(
|
||||
action: () => Promise<{ data: AdminTaskActionResponse }>,
|
||||
successMessage: string,
|
||||
confirmText: string,
|
||||
) {
|
||||
try {
|
||||
await ElMessageBox.confirm(confirmText, '确认操作', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '继续执行',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
actionLoading.value = true
|
||||
|
||||
try {
|
||||
const response = await action()
|
||||
if (response.data.claimUrl) {
|
||||
lastClaimUrl.value = response.data.claimUrl
|
||||
}
|
||||
ElMessage.success(successMessage)
|
||||
await loadDetail()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '操作失败')
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadScreenshotPreview(taskDetail: AdminTaskDetail) {
|
||||
clearScreenshotPreview()
|
||||
|
||||
if (!taskDetail.screenshotUrl) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = await fetchAdminTaskScreenshot(taskDetail.task.taskId)
|
||||
screenshotPreviewUrl.value = URL.createObjectURL(blob)
|
||||
} catch {
|
||||
screenshotPreviewUrl.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function clearScreenshotPreview() {
|
||||
if (screenshotPreviewUrl.value) {
|
||||
URL.revokeObjectURL(screenshotPreviewUrl.value)
|
||||
screenshotPreviewUrl.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadDetail)
|
||||
onBeforeUnmount(clearScreenshotPreview)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="admin-panel">
|
||||
<header class="panel-header">
|
||||
<div>
|
||||
<h1>任务详情</h1>
|
||||
<p>查看任务全量信息并执行人工操作。</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
|
||||
<div v-if="loading" class="empty-block">任务详情加载中</div>
|
||||
|
||||
<template v-else-if="detail">
|
||||
<section class="info-card">
|
||||
<h2>{{ detail.task.taskNo }}</h2>
|
||||
<p>状态:<AdminStatusTag :status="detail.task.status" /> · 订单:{{ detail.order?.platformOrderId || '-' }}</p>
|
||||
<p>系统绑定:<AdminStatusTag :status="detail.task.systemBindingStatus" /> · 完整绑定:<AdminStatusTag :status="detail.task.userBindingStatus" /></p>
|
||||
<p>角色:{{ detail.task.roleName || '-' }} / {{ detail.task.roleId || '-' }}</p>
|
||||
<p>最后错误:{{ detail.task.lastError || '-' }}</p>
|
||||
<p v-if="lastClaimUrl">新领取链接:{{ lastClaimUrl }}</p>
|
||||
</section>
|
||||
|
||||
<section class="action-row">
|
||||
<el-button
|
||||
:disabled="!detail.operations.canRetry"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="primary"
|
||||
@click="runAction(() => retryAdminTask(detail!.task.taskId), '任务已重试', `确认重试任务 ${detail!.task.taskNo} 吗?`)"
|
||||
>
|
||||
重试任务
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAdminRole('admin')"
|
||||
:disabled="!detail.operations.canReleaseCdk"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
@click="runAction(() => releaseAdminTaskCdk(detail!.task.taskId), 'CDK 已释放', `确认释放任务 ${detail!.task.taskNo} 当前预占的 CDK 吗?`)"
|
||||
>
|
||||
释放 CDK
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAdminRole('admin')"
|
||||
:disabled="!detail.operations.canRegenerateClaimLink"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
@click="runAction(() => regenerateAdminTaskClaimLink(detail!.task.taskId), '领取链接已重新生成', `确认重新生成任务 ${detail!.task.taskNo} 的领取链接吗?旧链接会失效。`)"
|
||||
>
|
||||
重发链接
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAdminRole('admin')"
|
||||
:disabled="!detail.operations.canMarkManualReview"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
@click="runAction(() => markAdminTaskManualReview(detail!.task.taskId), '任务已转人工处理', `确认将任务 ${detail!.task.taskNo} 转为人工处理吗?`)"
|
||||
>
|
||||
转人工
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAdminRole('admin')"
|
||||
:disabled="!detail.operations.canClose"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="danger"
|
||||
@click="runAction(() => closeAdminTask(detail!.task.taskId), '任务已关闭', `确认关闭任务 ${detail!.task.taskNo} 吗?关闭后不会自动继续推进。`)"
|
||||
>
|
||||
关闭任务
|
||||
</el-button>
|
||||
</section>
|
||||
|
||||
<section class="table-card">
|
||||
<h3>任务信息</h3>
|
||||
<table class="data-table">
|
||||
<tbody>
|
||||
<tr><th>商品</th><td>{{ detail.orderItem?.skuName || '-' }}</td></tr>
|
||||
<tr><th>SKU</th><td>{{ detail.orderItem?.skuCode || '-' }}</td></tr>
|
||||
<tr><th>预占 CDK</th><td>{{ detail.cdk?.cdkCode || '-' }}</td></tr>
|
||||
<tr><th>CDK 状态</th><td><AdminStatusTag :status="detail.cdk?.status || ''" /></td></tr>
|
||||
<tr><th>Claim Token</th><td>{{ detail.claimToken?.token || '-' }}</td></tr>
|
||||
<tr><th>Token 状态</th><td><AdminStatusTag :status="detail.claimToken?.status || ''" /></td></tr>
|
||||
<tr><th>Token 过期</th><td>{{ formatAdminDateTime(detail.claimToken?.expiredAt) }}</td></tr>
|
||||
<tr><th>浏览器会话</th><td>{{ detail.task.browserSessionId || '-' }}</td></tr>
|
||||
<tr><th>用户打开链接</th><td>{{ formatAdminDateTime(detail.task.claimedAt) }}</td></tr>
|
||||
<tr><th>用户提交绑定</th><td>{{ formatAdminDateTime(detail.task.roleConfirmedAt) }}</td></tr>
|
||||
<tr><th>完整绑定成功</th><td>{{ formatAdminDateTime(detail.task.redeemedAt) }}</td></tr>
|
||||
<tr><th>截图路径</th><td>{{ detail.task.screenshotPath || '-' }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section v-if="detail.screenshotUrl" class="table-card">
|
||||
<h3>结果截图</h3>
|
||||
<p v-if="!screenshotPreviewUrl" class="screenshot-empty">截图加载中或当前不可读取。</p>
|
||||
<a v-else class="screenshot-link" :href="screenshotPreviewUrl" target="_blank" rel="noreferrer">打开原图</a>
|
||||
<img v-if="screenshotPreviewUrl" class="screenshot-preview" :src="screenshotPreviewUrl" alt="任务兑换截图" />
|
||||
</section>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-panel { display: grid; gap: 16px; }
|
||||
.panel-header h1 { margin: 0; color: #1d3555; }
|
||||
.panel-header p { margin: 8px 0 0; color: #64748b; }
|
||||
.info-card,.table-card,.empty-block,.error-copy {
|
||||
padding: 18px; border-radius: 20px; background: rgba(255,255,255,.94); border: 1px solid rgba(86,108,138,.1);
|
||||
}
|
||||
.error-copy { color: #b42318; }
|
||||
.action-row { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||
.table-card h3 { margin: 0 0 12px; color: #1d3555; }
|
||||
.data-table { width: 100%; border-collapse: collapse; }
|
||||
.data-table th,.data-table td { padding: 12px 10px; text-align: left; border-bottom: 1px solid rgba(86,108,138,.08); }
|
||||
.data-table th { width: 160px; color: #64748b; }
|
||||
.screenshot-link { display: inline-block; margin-bottom: 12px; color: #175cd3; text-decoration: none; }
|
||||
.screenshot-empty { margin: 0 0 12px; color: #64748b; }
|
||||
.screenshot-preview {
|
||||
display: block;
|
||||
width: min(100%, 720px);
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(86,108,138,.12);
|
||||
box-shadow: 0 20px 48px rgba(15,23,42,.08);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user