重构:迁移3个列表视图到useAdminListPage + 提取useAdminAction组合式函数
This commit is contained in:
@@ -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%;
|
||||||
|
|||||||
Reference in New Issue
Block a user