重构:迁移3个列表视图到useAdminListPage + 提取useAdminAction组合式函数

This commit is contained in:
yml
2026-05-19 19:27:43 +08:00
parent 110d4b9d2b
commit 671b1640aa
4 changed files with 306 additions and 266 deletions
@@ -0,0 +1,42 @@
import { ref } from 'vue'
import { isFeedbackDismissed, showConfirm, showError, showSuccess } from '@/lib/feedback'
export interface RunActionOptions {
id: number | string
action: () => Promise<unknown>
successMessage: string
confirmText: string
onAfterAction?: () => Promise<unknown> | void
}
export function useAdminAction() {
const actionLoadingId = ref<number | string | null>(null)
async function runAction(options: RunActionOptions) {
try {
await showConfirm(options.confirmText, '确认操作', {
type: 'warning',
confirmButtonText: '继续执行',
cancelButtonText: '取消',
})
} catch (error) {
if (isFeedbackDismissed(error)) return
throw error
}
actionLoadingId.value = options.id
try {
await options.action()
showSuccess(options.successMessage)
if (options.onAfterAction) await options.onAfterAction()
} catch (error) {
showError(error instanceof Error ? error.message : '操作失败')
} finally {
actionLoadingId.value = null
}
}
return { actionLoadingId, runAction }
}
@@ -1,62 +1,52 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, ref } from 'vue'
import { useAdminListPage } from '@/composables/useAdminListPage'
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue' import AdminPageHeader from '@/components/admin/AdminPageHeader.vue'
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue' import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
import { fetchAdminAuditLogs } from '@/services/admin' import { fetchAdminAuditLogs } from '@/services/admin'
import type { AdminAuditLogItem, AdminPagination } from '@/types/admin' import type { AdminAuditLogItem } from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth' import { hasAdminRole } from '@/utils/admin-auth'
import { formatAuditAction, formatAuditTargetType } from '@/utils/admin-display' import { formatAuditAction, formatAuditTargetType } from '@/utils/admin-display'
import { adminAuditActionOptions, adminAuditTargetTypeOptions } from '@/utils/admin-options' import { adminAuditActionOptions, adminAuditTargetTypeOptions } from '@/utils/admin-options'
import { formatAdminDateTime } from '@/utils/admin-time' import { formatAdminDateTime } from '@/utils/admin-time'
import { stringifyDisplayJson } from '@/utils/date-time' import { stringifyDisplayJson } from '@/utils/date-time'
const loading = ref(true)
const errorMessage = ref('')
const actorUsername = ref('') const actorUsername = ref('')
const action = ref('') const action = ref('')
const targetType = ref('') const targetType = ref('')
const dateFrom = ref('') const dateFrom = ref('')
const dateTo = ref('') const dateTo = ref('')
const items = ref<AdminAuditLogItem[]>([])
const pagination = ref<AdminPagination>({ const isAdmin = hasAdminRole('admin')
page: 1,
pageSize: 20, const {
total: 0, loading,
errorMessage,
items,
pagination,
loadPage: loadAuditLogs,
} = useAdminListPage<AdminAuditLogItem>({
defaultErrorMessage: '读取审计日志失败',
fetchPage: isAdmin
? (page, pageSize) =>
fetchAdminAuditLogs({
page,
pageSize,
actorUsername: actorUsername.value.trim(),
action: action.value.trim(),
targetType: targetType.value.trim(),
dateFrom: dateFrom.value.trim(),
dateTo: dateTo.value.trim(),
})
: () =>
Promise.resolve({
code: 0,
msg: '',
data: { items: [], pagination: { page: 1, pageSize: 20, total: 0 } },
}),
}) })
async function loadAuditLogs(page = 1) {
if (!hasAdminRole('admin')) {
loading.value = false
return
}
loading.value = true
errorMessage.value = ''
try {
const response = await fetchAdminAuditLogs({
page,
pageSize: pagination.value.pageSize,
actorUsername: actorUsername.value.trim(),
action: action.value.trim(),
targetType: targetType.value.trim(),
dateFrom: dateFrom.value.trim(),
dateTo: dateTo.value.trim(),
})
items.value = response.data.items
pagination.value = response.data.pagination
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '读取审计日志失败'
} finally {
loading.value = false
}
}
function handlePageChange() {
loadAuditLogs(pagination.value.page)
}
function formatPayload(payload: Record<string, unknown>) { function formatPayload(payload: Record<string, unknown>) {
const text = stringifyDisplayJson(payload, 0) const text = stringifyDisplayJson(payload, 0)
return text.length > 120 ? `${text.slice(0, 120)}...` : text return text.length > 120 ? `${text.slice(0, 120)}...` : text
@@ -68,7 +58,14 @@ function resolveTargetLabel(targetType: string, targetId: string) {
const totalLabel = computed(() => `${pagination.value.total} 条审计记录`) const totalLabel = computed(() => `${pagination.value.total} 条审计记录`)
onMounted(loadAuditLogs) function resetFilters() {
actorUsername.value = ''
action.value = ''
targetType.value = ''
dateFrom.value = ''
dateTo.value = ''
void loadAuditLogs(1)
}
</script> </script>
<template> <template>
@@ -78,25 +75,31 @@ onMounted(loadAuditLogs)
description="集中查看高风险后台动作,便于排查谁在什么时间改了什么。" description="集中查看高风险后台动作,便于排查谁在什么时间改了什么。"
> >
<template #extra> <template #extra>
<span class="total-label">{{ totalLabel }}</span> <span class="total-badge">{{ totalLabel }}</span>
</template> </template>
</AdminPageHeader> </AdminPageHeader>
<!-- 权限检查 --> <!-- 权限检查 -->
<el-result v-if="!hasAdminRole('admin')" icon="warning" title="仅管理员可以访问操作审计" /> <el-result v-if="!isAdmin" icon="warning" title="仅管理员可以访问操作审计" />
<template v-else> <template v-else>
<!-- 筛选栏 --> <!-- 筛选栏 -->
<el-card shadow="never" class="filter-card"> <el-card shadow="never" class="section-card">
<div class="filter-row"> <template #header>
<div class="card-header">
<span class="card-title">筛选审计日志</span>
<span class="card-desc">按操作人动作类型目标类型和日期筛选</span>
</div>
</template>
<div class="filter-grid">
<el-input <el-input
v-model="actorUsername" v-model="actorUsername"
placeholder="操作账号" placeholder="操作账号"
clearable clearable
class="filter-col-160" class="filter-control"
@keyup.enter="handlePageChange" @keyup.enter="loadAuditLogs()"
/> />
<el-select v-model="action" placeholder="动作" clearable class="filter-col-160"> <el-select v-model="action" placeholder="动作" clearable class="filter-control">
<el-option <el-option
v-for="opt in adminAuditActionOptions" v-for="opt in adminAuditActionOptions"
:key="opt.value" :key="opt.value"
@@ -104,7 +107,7 @@ onMounted(loadAuditLogs)
:value="opt.value" :value="opt.value"
/> />
</el-select> </el-select>
<el-select v-model="targetType" placeholder="目标类型" clearable class="filter-col-160"> <el-select v-model="targetType" placeholder="目标类型" clearable class="filter-control">
<el-option <el-option
v-for="opt in adminAuditTargetTypeOptions" v-for="opt in adminAuditTargetTypeOptions"
:key="opt.value" :key="opt.value"
@@ -117,16 +120,19 @@ onMounted(loadAuditLogs)
type="date" type="date"
placeholder="开始日期" placeholder="开始日期"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
class="filter-col-160" class="filter-control"
/> />
<el-date-picker <el-date-picker
v-model="dateTo" v-model="dateTo"
type="date" type="date"
placeholder="结束日期" placeholder="结束日期"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
class="filter-col-160" class="filter-control"
/> />
<el-button type="primary" @click="handlePageChange">查询</el-button> <div class="filter-buttons">
<el-button round @click="resetFilters">重置</el-button>
<el-button round type="primary" @click="loadAuditLogs()">查询</el-button>
</div>
</div> </div>
</el-card> </el-card>
@@ -141,13 +147,14 @@ onMounted(loadAuditLogs)
/> />
<!-- 表格 --> <!-- 表格 -->
<el-card shadow="never" class="table-card mt-4"> <el-card shadow="never" class="section-card">
<el-table <el-table
:data="items" :data="items"
stripe stripe
size="small" size="small"
v-loading="loading" v-loading="loading"
element-loading-text="审计日志加载中" element-loading-text="审计日志加载中"
class="data-table"
> >
<el-table-column label="时间" width="180"> <el-table-column label="时间" width="180">
<template #default="{ row }">{{ formatAdminDateTime(row.createdAt) }}</template> <template #default="{ row }">{{ formatAdminDateTime(row.createdAt) }}</template>
@@ -183,7 +190,8 @@ onMounted(loadAuditLogs)
:page-size="pagination.pageSize" :page-size="pagination.pageSize"
:total="pagination.total" :total="pagination.total"
layout="total, prev, pager, next" layout="total, prev, pager, next"
@current-change="handlePageChange" class="pagination-center"
@current-change="loadAuditLogs"
/> />
</el-card> </el-card>
</template> </template>
@@ -193,26 +201,6 @@ onMounted(loadAuditLogs)
<style scoped> <style scoped>
@import '../../../styles/admin-list-pages.css'; @import '../../../styles/admin-list-pages.css';
.total-label {
color: var(--text-secondary);
font-size: var(--text-sm);
}
.filter-card {
margin-top: var(--space-4);
}
.filter-row {
display: flex;
gap: var(--space-3);
align-items: center;
flex-wrap: wrap;
}
.table-card {
overflow: visible;
}
.payload-text { .payload-text {
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: var(--text-xs); font-size: var(--text-xs);
@@ -1,17 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, ref } from 'vue'
import { useAdminListPage } from '@/composables/useAdminListPage'
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue' import AdminPageHeader from '@/components/admin/AdminPageHeader.vue'
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue' import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
import { fetchAdminMessageDeliveries } from '@/services/admin' import { fetchAdminMessageDeliveries } from '@/services/admin'
import type { AdminMessageDeliveryListItem, AdminPagination } from '@/types/admin' import type { AdminMessageDeliveryListItem } from '@/types/admin'
import { formatAdminDateTime } from '@/utils/admin-time' import { formatAdminDateTime } from '@/utils/admin-time'
import { adminMessageDeliveryStatusOptions } from '@/utils/admin-options' import { adminMessageDeliveryStatusOptions } from '@/utils/admin-options'
const loading = ref(true)
const errorMessage = ref('')
const items = ref<AdminMessageDeliveryListItem[]>([])
const provider = ref('agiso') const provider = ref('agiso')
const platform = ref('xianyu') const platform = ref('xianyu')
const status = ref('') const status = ref('')
@@ -21,26 +18,18 @@ const taskNo = ref('')
const dateFrom = ref('') const dateFrom = ref('')
const dateTo = ref('') const dateTo = ref('')
const pagination = ref<AdminPagination>({ const {
page: 1, loading,
pageSize: 20, errorMessage,
total: 0, items,
}) pagination,
loadPage: loadMessageDeliveries,
const summary = computed(() => ({ } = useAdminListPage<AdminMessageDeliveryListItem>({
successCount: items.value.filter((item) => item.status === 'success').length, defaultErrorMessage: '读取消息发送记录失败',
failedCount: items.value.filter((item) => item.status === 'failed').length, fetchPage: (page, pageSize) =>
pendingCount: items.value.filter((item) => item.status === 'pending').length, fetchAdminMessageDeliveries({
}))
async function loadMessageDeliveries(page = pagination.value.page) {
loading.value = true
errorMessage.value = ''
try {
const response = await fetchAdminMessageDeliveries({
page, page,
pageSize: pagination.value.pageSize, pageSize,
provider: provider.value.trim(), provider: provider.value.trim(),
platform: platform.value.trim(), platform: platform.value.trim(),
status: status.value.trim(), status: status.value.trim(),
@@ -49,15 +38,14 @@ async function loadMessageDeliveries(page = pagination.value.page) {
taskNo: taskNo.value.trim(), taskNo: taskNo.value.trim(),
dateFrom: dateFrom.value.trim(), dateFrom: dateFrom.value.trim(),
dateTo: dateTo.value.trim(), dateTo: dateTo.value.trim(),
}) }),
items.value = response.data.items })
pagination.value = response.data.pagination
} catch (error) { const summary = computed(() => ({
errorMessage.value = error instanceof Error ? error.message : '读取消息发送记录失败' successCount: items.value.filter((item) => item.status === 'success').length,
} finally { failedCount: items.value.filter((item) => item.status === 'failed').length,
loading.value = false pendingCount: items.value.filter((item) => item.status === 'pending').length,
} }))
}
function resetFilters() { function resetFilters() {
provider.value = 'agiso' provider.value = 'agiso'
@@ -70,8 +58,6 @@ function resetFilters() {
dateTo.value = '' dateTo.value = ''
void loadMessageDeliveries(1) void loadMessageDeliveries(1)
} }
onMounted(loadMessageDeliveries)
</script> </script>
<template> <template>
@@ -1,51 +1,56 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { ref } from "vue";
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue' import { useAdminAction } from "@/composables/useAdminAction";
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue' import { useAdminListPage } from "@/composables/useAdminListPage";
import { showConfirm, showError, showSuccess } from '@/lib/feedback' import AdminPageHeader from "@/components/admin/AdminPageHeader.vue";
import AdminStatusTag from "@/components/admin/AdminStatusTag.vue";
import { import {
closeAdminTask, closeAdminTask,
fetchAdminTasks, fetchAdminTasks,
markAdminTaskManualReview, markAdminTaskManualReview,
regenerateAdminTaskClaimLink, regenerateAdminTaskClaimLink,
retryAdminTask, retryAdminTask,
} from '@/services/admin' } from "@/services/admin";
import type { AdminPagination, AdminTaskActionResponse, AdminTaskListItem } from '@/types/admin' import type { AdminTaskActionResponse, AdminTaskListItem } from "@/types/admin";
import { hasAdminRole } from '@/utils/admin-auth' import { hasAdminRole } from "@/utils/admin-auth";
import { adminTaskStatusOptions } from '@/utils/admin-options' import {
import { formatAdminDateTime } from '@/utils/admin-time' formatBindingCountSummary,
formatBindingRoleSummary,
} from "@/utils/admin-display";
import { adminTaskStatusOptions } from "@/utils/admin-options";
import { formatAdminDateTime } from "@/utils/admin-time";
const loading = ref(true) const status = ref("");
const actionLoadingId = ref<number | null>(null) const taskNo = ref("");
const errorMessage = ref('') const platformOrderId = ref("");
const status = ref('') const roleId = ref("");
const taskNo = ref('') const skuCode = ref("");
const platformOrderId = ref('') const dateFrom = ref("");
const roleId = ref('') const dateTo = ref("");
const skuCode = ref('') const lastClaimUrl = ref("");
const dateFrom = ref('')
const dateTo = ref('')
const items = ref<AdminTaskListItem[]>([])
const lastClaimUrl = ref('')
const pagination = ref<AdminPagination>({
page: 1,
pageSize: 20,
total: 0,
})
const canManageTaskLifecycle = hasAdminRole('operator')
const canCloseTasks = hasAdminRole('support')
type AdminTaskActionKey = 'retry' | 'regenerate_claim_link' | 'manual_review' | 'close' const canManageTaskLifecycle = hasAdminRole("operator");
const canCloseTasks = hasAdminRole("support");
async function loadTasks(page = pagination.value.page) { type AdminTaskActionKey =
loading.value = true | "retry"
errorMessage.value = '' | "regenerate_claim_link"
| "manual_review"
| "close";
try { const {
const response = await fetchAdminTasks({ loading,
errorMessage,
items,
pagination,
loadPage: loadTasks,
} = useAdminListPage<AdminTaskListItem>({
defaultErrorMessage: "读取任务列表失败",
fetchPage: (page, pageSize) =>
fetchAdminTasks({
page, page,
pageSize: pagination.value.pageSize, pageSize,
status: status.value.trim(), status: status.value.trim(),
taskNo: taskNo.value.trim(), taskNo: taskNo.value.trim(),
platformOrderId: platformOrderId.value.trim(), platformOrderId: platformOrderId.value.trim(),
@@ -53,105 +58,82 @@ async function loadTasks(page = pagination.value.page) {
skuCode: skuCode.value.trim(), skuCode: skuCode.value.trim(),
dateFrom: dateFrom.value.trim(), dateFrom: dateFrom.value.trim(),
dateTo: dateTo.value.trim(), dateTo: dateTo.value.trim(),
}) }),
items.value = response.data.items });
pagination.value = response.data.pagination
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '读取任务列表失败'
} finally {
loading.value = false
}
}
function formatBindingRoles(item: AdminTaskListItem) { const { actionLoadingId, runAction } = useAdminAction();
const roles = item.bindingSummary.roleKeys
if (!Array.isArray(roles) || roles.length === 0) return '无绑定角色'
return roles.join(' / ')
}
function formatBindingCountSummary(item: AdminTaskListItem) { import {
const summary = item.bindingSummary formatBindingCountSummary,
return `${summary.totalBindingCount} 条 · 预占 ${summary.reservedBindingCount} · 已消费 ${summary.consumedBindingCount} · 已释放 ${summary.releasedBindingCount}` formatBindingRoleSummary,
} } from "@/utils/admin-display";
onMounted(() => { function handleActionCommand(
void loadTasks() actionKey: AdminTaskActionKey,
}) item: AdminTaskListItem
async function runAction(
taskId: number,
action: () => Promise<{ data: AdminTaskActionResponse }>,
successMessage: string,
confirmText: string,
) { ) {
try { if (actionKey === "retry") {
await showConfirm(confirmText, '确认操作', { void runAction({
type: 'warning', id: item.taskId,
confirmButtonText: '继续执行', action: async () => {
cancelButtonText: '取消', const response = await retryAdminTask(item.taskId);
}) if (response.data.claimUrl) lastClaimUrl.value = response.data.claimUrl;
} catch { },
return successMessage: "任务已重试",
confirmText: `确认重试任务 ${item.taskNo} 吗?`,
onAfterAction: async () => {
await loadTasks();
},
});
return;
} }
if (actionKey === "regenerate_claim_link") {
actionLoadingId.value = taskId void runAction({
try { id: item.taskId,
const response = await action() action: async () => {
if (response.data.claimUrl) lastClaimUrl.value = response.data.claimUrl const response = await regenerateAdminTaskClaimLink(item.taskId);
showSuccess(successMessage) if (response.data.claimUrl) lastClaimUrl.value = response.data.claimUrl;
await loadTasks() },
} catch (error) { successMessage: "领取链接已重新生成",
showError(error instanceof Error ? error.message : '操作失败') confirmText: `确认重新生成任务 ${item.taskNo} 的领取链接吗?旧链接会失效。`,
} finally { onAfterAction: async () => {
actionLoadingId.value = null await loadTasks();
},
});
return;
} }
} if (actionKey === "manual_review") {
void runAction({
function handleActionCommand(actionKey: AdminTaskActionKey, item: AdminTaskListItem) { id: item.taskId,
if (actionKey === 'retry') { action: () => markAdminTaskManualReview(item.taskId),
void runAction( successMessage: "任务已转人工处理",
item.taskId, confirmText: `确认将任务 ${item.taskNo} 转为人工处理吗?`,
() => retryAdminTask(item.taskId), onAfterAction: async () => {
'任务已重试', await loadTasks();
`确认重试任务 ${item.taskNo} 吗?`, },
) });
return return;
} }
if (actionKey === 'regenerate_claim_link') { void runAction({
void runAction( id: item.taskId,
item.taskId, action: () => closeAdminTask(item.taskId),
() => regenerateAdminTaskClaimLink(item.taskId), successMessage: "任务已关闭",
'领取链接已重新生成', confirmText: `确认关闭任务 ${item.taskNo} 吗?关闭后不会自动继续推进。`,
`确认重新生成任务 ${item.taskNo} 的领取链接吗?旧链接会失效。`, onAfterAction: async () => {
) await loadTasks();
return },
} });
if (actionKey === 'manual_review') {
void runAction(
item.taskId,
() => markAdminTaskManualReview(item.taskId),
'任务已转人工处理',
`确认将任务 ${item.taskNo} 转为人工处理吗?`,
)
return
}
void runAction(
item.taskId,
() => closeAdminTask(item.taskId),
'任务已关闭',
`确认关闭任务 ${item.taskNo} 吗?关闭后不会自动继续推进。`,
)
} }
function resetFilters() { function resetFilters() {
status.value = '' status.value = "";
taskNo.value = '' taskNo.value = "";
platformOrderId.value = '' platformOrderId.value = "";
roleId.value = '' roleId.value = "";
skuCode.value = '' skuCode.value = "";
dateFrom.value = '' dateFrom.value = "";
dateTo.value = '' dateTo.value = "";
void loadTasks(1) void loadTasks(1);
} }
</script> </script>
@@ -170,7 +152,9 @@ function resetFilters() {
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span class="card-title">查询任务</span> <span class="card-title">查询任务</span>
<span class="card-desc">按状态任务号订单号角色标识SKU 和日期筛选</span> <span class="card-desc"
>按状态任务号订单号角色标识SKU 和日期筛选</span
>
</div> </div>
</template> </template>
<div class="filter-grid"> <div class="filter-grid">
@@ -200,7 +184,12 @@ function resetFilters() {
clearable clearable
@keyup.enter="loadTasks(1)" @keyup.enter="loadTasks(1)"
/> />
<el-input v-model="skuCode" placeholder="商品 SKU" clearable @keyup.enter="loadTasks(1)" /> <el-input
v-model="skuCode"
placeholder="商品 SKU"
clearable
@keyup.enter="loadTasks(1)"
/>
<el-date-picker <el-date-picker
v-model="dateFrom" v-model="dateFrom"
type="date" type="date"
@@ -250,10 +239,17 @@ function resetFilters() {
<template v-else> <template v-else>
<div class="tasks-toolbar table-toolbar"> <div class="tasks-toolbar table-toolbar">
<strong>任务列表</strong> <strong>任务列表</strong>
<span> {{ pagination.page }} 当前展示 {{ items.length }} </span> <span
> {{ pagination.page }} 当前展示 {{ items.length }} </span
>
</div> </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="220"> <el-table-column label="任务 / 商品" min-width="220">
<template #default="{ row }"> <template #default="{ row }">
<div class="cell-stack"> <div class="cell-stack">
@@ -268,12 +264,14 @@ function resetFilters() {
<span class="compact-chip" :title="row.platformOrderId" <span class="compact-chip" :title="row.platformOrderId"
>订单 {{ row.platformOrderId }}</span >订单 {{ 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>
<span class="cell-title" :title="row.skuName || row.skuCode">{{ <span class="cell-title" :title="row.skuName || row.skuCode">{{
row.skuName || row.skuCode row.skuName || row.skuCode
}}</span> }}</span>
<span class="cell-subline">SKU {{ row.skuCode || '-' }}</span> <span class="cell-subline">SKU {{ row.skuCode || "-" }}</span>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
@@ -298,15 +296,21 @@ function resetFilters() {
<el-table-column label="角色 / 凭据" min-width="180"> <el-table-column label="角色 / 凭据" min-width="180">
<template #default="{ row }"> <template #default="{ row }">
<div class="cell-stack"> <div class="cell-stack">
<span class="cell-title" :title="row.roleName || row.roleId || '-'">{{ <span
row.roleName || '-' class="cell-title"
}}</span> :title="row.roleName || row.roleId || '-'"
<span class="cell-subline" :title="row.roleId || '-'" >{{ row.roleName || "-" }}</span
>角色ID {{ row.roleId || '-' }}</span
> >
<span class="cell-subline">登录 {{ row.loginType || '-' }}</span> <span class="cell-subline" :title="row.roleId || '-'"
<span class="cell-subline" :title="row.inventoryDisplayMasked || '-'" >角色ID {{ row.roleId || "-" }}</span
>凭据 {{ row.inventoryDisplayMasked || '-' }}</span >
<span class="cell-subline"
>登录 {{ row.loginType || "-" }}</span
>
<span
class="cell-subline"
:title="row.inventoryDisplayMasked || '-'"
>凭据 {{ row.inventoryDisplayMasked || "-" }}</span
> >
<span class="cell-subline" :title="formatBindingRoles(row)" <span class="cell-subline" :title="formatBindingRoles(row)"
>绑定 {{ formatBindingRoles(row) }}</span >绑定 {{ formatBindingRoles(row) }}</span
@@ -322,8 +326,12 @@ function resetFilters() {
<el-table-column label="时间" min-width="180"> <el-table-column label="时间" min-width="180">
<template #default="{ row }"> <template #default="{ row }">
<div class="cell-stack"> <div class="cell-stack">
<span class="cell-subline">创建 {{ formatAdminDateTime(row.createdAt) }}</span> <span class="cell-subline"
<span class="cell-subline">更新 {{ formatAdminDateTime(row.updatedAt) }}</span> >创建 {{ formatAdminDateTime(row.createdAt) }}</span
>
<span class="cell-subline"
>更新 {{ formatAdminDateTime(row.updatedAt) }}</span
>
<span v-if="row.claimedAt" class="cell-subline" <span v-if="row.claimedAt" class="cell-subline"
>领取 {{ formatAdminDateTime(row.claimedAt) }}</span >领取 {{ formatAdminDateTime(row.claimedAt) }}</span
> >
@@ -340,32 +348,46 @@ function resetFilters() {
<template #default="{ row }"> <template #default="{ row }">
<div class="cell-stack"> <div class="cell-stack">
<span class="cell-error" :title="row.lastError || '-'">{{ <span class="cell-error" :title="row.lastError || '-'">{{
row.lastError || '当前无异常' row.lastError || "当前无异常"
}}</span> }}</span>
<div class="action-stack"> <div class="action-stack">
<el-button <el-button
v-if="canManageTaskLifecycle" v-if="canManageTaskLifecycle"
:disabled=" :disabled="
row.executorKey === 'manual_dispatch' || row.executorKey === 'manual_dispatch' ||
!['retry_pending', 'manual_review', 'waiting_inventory'].includes(row.status) ![
'retry_pending',
'manual_review',
'waiting_inventory',
].includes(row.status)
" "
:loading="actionLoadingId === row.taskId" :loading="actionLoadingId === row.taskId"
size="small" size="small"
type="danger" type="danger"
plain plain
@click=" @click="
runAction( runAction({
row.taskId, id: row.taskId,
() => retryAdminTask(row.taskId), action: async () => {
'任务已重试', const response = await retryAdminTask(row.taskId);
`确认重试任务 ${row.taskNo} 吗?`, if (response.data.claimUrl)
) lastClaimUrl.value = response.data.claimUrl;
},
successMessage: '任务已重试',
confirmText: `确认重试任务 ${row.taskNo} 吗?`,
onAfterAction: async () => {
await loadTasks();
},
})
" "
> >
重试 重试
</el-button> </el-button>
<el-button <el-button
v-if="canManageTaskLifecycle && row.executorKey !== 'manual_dispatch'" v-if="
canManageTaskLifecycle &&
row.executorKey !== 'manual_dispatch'
"
size="small" size="small"
@click="handleActionCommand('regenerate_claim_link', row)" @click="handleActionCommand('regenerate_claim_link', row)"
> >
@@ -379,7 +401,10 @@ function resetFilters() {
转人工 转人工
</el-button> </el-button>
<el-button <el-button
v-if="canCloseTasks && !['redeemed', 'closed'].includes(row.status)" v-if="
canCloseTasks &&
!['redeemed', 'closed'].includes(row.status)
"
size="small" size="small"
type="danger" type="danger"
@click="handleActionCommand('close', row)" @click="handleActionCommand('close', row)"
@@ -407,13 +432,12 @@ function resetFilters() {
</template> </template>
<style scoped> <style scoped>
@import '../../../styles/admin-list-pages.css'; @import "../../../styles/admin-list-pages.css";
.filter-grid { .filter-grid {
grid-template-columns: minmax(150px, 0.75fr) minmax(220px, 1fr) minmax(220px, 1fr) minmax( grid-template-columns:
210px, minmax(150px, 0.75fr) minmax(220px, 1fr) minmax(220px, 1fr)
0.95fr minmax(210px, 0.95fr);
);
} }
.filter-grid :deep(.el-date-editor) { .filter-grid :deep(.el-date-editor) {
width: 100%; width: 100%;