feat: 七月迭代公共状态与异步任务交互
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m59s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m59s
This commit is contained in:
@@ -113,6 +113,9 @@
|
||||
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(res.data?.message || '导出任务已创建')
|
||||
if (res.data?.task_id) {
|
||||
localStorage.setItem(`export-task-active:${props.scene}`, String(res.data.task_id))
|
||||
}
|
||||
emit('success', res.data)
|
||||
visible.value = false
|
||||
} else {
|
||||
|
||||
178
src/composables/useAsyncTaskPolling.ts
Normal file
178
src/composables/useAsyncTaskPolling.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
computed,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
shallowRef,
|
||||
type Ref,
|
||||
type ShallowRef
|
||||
} from 'vue'
|
||||
import { isAsyncTaskActive, isAsyncTaskTerminal, type AsyncTaskProgress } from '@/types/api'
|
||||
|
||||
const POLL_DELAYS = [2000, 3000, 5000]
|
||||
const MAX_POLL_DELAY = 10000
|
||||
|
||||
export interface AsyncTaskPollingOptions<T extends AsyncTaskProgress> {
|
||||
storageKey: string
|
||||
fetchTask: (taskId: number) => Promise<T>
|
||||
isForbidden?: (error: unknown) => boolean
|
||||
autoRestore?: boolean
|
||||
}
|
||||
|
||||
export interface AsyncTaskPollingState<T extends AsyncTaskProgress> {
|
||||
taskId: Ref<number | null>
|
||||
task: ShallowRef<T | null>
|
||||
loading: Ref<boolean>
|
||||
forbidden: Ref<boolean>
|
||||
error: Ref<string | null>
|
||||
isActive: Readonly<Ref<boolean>>
|
||||
start: (taskId: number) => Promise<void>
|
||||
retry: () => Promise<void>
|
||||
stop: () => void
|
||||
clear: () => void
|
||||
}
|
||||
|
||||
const readStoredTaskId = (storageKey: string): number | null => {
|
||||
try {
|
||||
const value = Number(localStorage.getItem(storageKey))
|
||||
return Number.isInteger(value) && value > 0 ? value : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const writeStoredTaskId = (storageKey: string, taskId: number | null) => {
|
||||
try {
|
||||
if (taskId) localStorage.setItem(storageKey, String(taskId))
|
||||
else localStorage.removeItem(storageKey)
|
||||
} catch {
|
||||
// Storage may be unavailable in private browsing or restricted webviews.
|
||||
}
|
||||
}
|
||||
|
||||
const getErrorMessage = (error: any) =>
|
||||
error?.response?.data?.msg || error?.message || '获取导出任务详情失败'
|
||||
|
||||
export function useAsyncTaskPolling<T extends AsyncTaskProgress>(
|
||||
options: AsyncTaskPollingOptions<T>
|
||||
): AsyncTaskPollingState<T> {
|
||||
const taskId = ref<number | null>(readStoredTaskId(options.storageKey))
|
||||
const task = shallowRef<T | null>(null)
|
||||
const loading = ref(false)
|
||||
const forbidden = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const isActive = computed(() => isAsyncTaskActive(task.value?.status))
|
||||
|
||||
let timer: number | undefined
|
||||
let pollIndex = 0
|
||||
let requestInFlight = false
|
||||
|
||||
const clearTimer = () => {
|
||||
if (timer !== undefined) {
|
||||
window.clearTimeout(timer)
|
||||
timer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
clearTimer()
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
stop()
|
||||
taskId.value = null
|
||||
task.value = null
|
||||
error.value = null
|
||||
forbidden.value = false
|
||||
writeStoredTaskId(options.storageKey, null)
|
||||
}
|
||||
|
||||
const schedule = () => {
|
||||
clearTimer()
|
||||
if (document.hidden || !isActive.value || !taskId.value) return
|
||||
const delay = POLL_DELAYS[pollIndex] ?? MAX_POLL_DELAY
|
||||
pollIndex += 1
|
||||
timer = window.setTimeout(() => {
|
||||
timer = undefined
|
||||
void refresh()
|
||||
}, delay)
|
||||
}
|
||||
|
||||
const refresh = async () => {
|
||||
if (!taskId.value || requestInFlight || document.hidden) return
|
||||
|
||||
requestInFlight = true
|
||||
loading.value = true
|
||||
forbidden.value = false
|
||||
error.value = null
|
||||
try {
|
||||
const nextTask = await options.fetchTask(taskId.value)
|
||||
task.value = nextTask
|
||||
if (isAsyncTaskTerminal(nextTask.status)) {
|
||||
stop()
|
||||
writeStoredTaskId(options.storageKey, null)
|
||||
} else {
|
||||
schedule()
|
||||
}
|
||||
} catch (requestError) {
|
||||
if (options.isForbidden?.(requestError) ?? false) {
|
||||
forbidden.value = true
|
||||
stop()
|
||||
} else {
|
||||
error.value = getErrorMessage(requestError)
|
||||
stop()
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
requestInFlight = false
|
||||
}
|
||||
}
|
||||
|
||||
const start = async (nextTaskId: number) => {
|
||||
if (!Number.isInteger(nextTaskId) || nextTaskId <= 0) return
|
||||
taskId.value = nextTaskId
|
||||
task.value = null
|
||||
pollIndex = 0
|
||||
writeStoredTaskId(options.storageKey, nextTaskId)
|
||||
await refresh()
|
||||
}
|
||||
|
||||
const retry = async () => {
|
||||
if (!taskId.value) return
|
||||
await refresh()
|
||||
}
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
if (taskId.value && isActive.value) {
|
||||
pollIndex = 0
|
||||
void refresh()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
if (options.autoRestore !== false && taskId.value) void refresh()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stop()
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
})
|
||||
|
||||
return {
|
||||
taskId,
|
||||
task: task as ShallowRef<T | null>,
|
||||
loading,
|
||||
forbidden,
|
||||
error,
|
||||
isActive,
|
||||
start,
|
||||
retry,
|
||||
stop,
|
||||
clear
|
||||
}
|
||||
}
|
||||
28
src/types/api/asyncTask.ts
Normal file
28
src/types/api/asyncTask.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export enum AsyncTaskStatus {
|
||||
PENDING = 1,
|
||||
PROCESSING = 2,
|
||||
COMPLETED = 3,
|
||||
FAILED = 4,
|
||||
CANCELED = 5
|
||||
}
|
||||
|
||||
export interface AsyncTaskProgress {
|
||||
task_id?: number
|
||||
status: number
|
||||
status_name?: string
|
||||
total_count?: number
|
||||
success_count?: number
|
||||
failed_count?: number
|
||||
failure_details?: unknown[] | null
|
||||
error_code?: string
|
||||
error_summary?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export const isAsyncTaskActive = (status?: AsyncTaskStatus) =>
|
||||
status === AsyncTaskStatus.PENDING || status === AsyncTaskStatus.PROCESSING
|
||||
|
||||
export const isAsyncTaskTerminal = (status?: AsyncTaskStatus) =>
|
||||
status === AsyncTaskStatus.COMPLETED ||
|
||||
status === AsyncTaskStatus.FAILED ||
|
||||
status === AsyncTaskStatus.CANCELED
|
||||
@@ -21,23 +21,30 @@ export interface ExportTaskQueryParams extends PaginationParams {
|
||||
|
||||
export interface ExportTaskItem {
|
||||
id: number
|
||||
task_id?: number
|
||||
task_no: string
|
||||
scene: ExportTaskScene
|
||||
status: ExportTaskStatus
|
||||
status_name: string
|
||||
progress: number
|
||||
status_name?: string
|
||||
progress?: number
|
||||
format: ExportTaskFormat
|
||||
total_rows: number
|
||||
processed_rows: number
|
||||
total_shards: number
|
||||
success_shards: number
|
||||
failed_shards: number
|
||||
file_key: string
|
||||
error_message: string
|
||||
cancel_requested: boolean
|
||||
total_rows?: number
|
||||
processed_rows?: number
|
||||
total_shards?: number
|
||||
success_shards?: number
|
||||
failed_shards?: number
|
||||
file_key?: string
|
||||
error_message?: string
|
||||
cancel_requested?: boolean
|
||||
total_count?: number
|
||||
success_count?: number
|
||||
failed_count?: number
|
||||
error_code?: string
|
||||
error_summary?: string
|
||||
updated_at?: string
|
||||
created_at: string
|
||||
started_at: string
|
||||
completed_at: string
|
||||
started_at?: string
|
||||
completed_at?: string
|
||||
}
|
||||
|
||||
export interface ExportTaskListResponse {
|
||||
|
||||
@@ -111,5 +111,8 @@ export * from './pollingMonitor'
|
||||
// 导出任务相关
|
||||
export * from './exportTask'
|
||||
|
||||
// 通用异步任务相关
|
||||
export * from './asyncTask'
|
||||
|
||||
// 订单套餐批量作废任务相关
|
||||
export * from './orderPackageInvalidateTask'
|
||||
|
||||
6
src/types/components.d.ts
vendored
6
src/types/components.d.ts
vendored
@@ -87,8 +87,10 @@ declare module 'vue' {
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCalendar: typeof import('element-plus/es')['ElCalendar']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
@@ -111,7 +113,9 @@ declare module 'vue' {
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
@@ -123,6 +127,8 @@ declare module 'vue' {
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
|
||||
@@ -992,7 +992,6 @@
|
||||
try {
|
||||
const res = await CardService.getStandaloneIotCards({
|
||||
is_standalone: true,
|
||||
status: 1,
|
||||
page: 1,
|
||||
page_size: 10
|
||||
})
|
||||
@@ -1015,7 +1014,6 @@
|
||||
oldDeviceSearchLoading.value = true
|
||||
try {
|
||||
const res = await DeviceService.getDevices({
|
||||
status: 1,
|
||||
page: 1,
|
||||
page_size: 10
|
||||
})
|
||||
@@ -1045,7 +1043,6 @@
|
||||
try {
|
||||
const res = await CardService.getStandaloneIotCards({
|
||||
is_standalone: true,
|
||||
status: 1,
|
||||
keyword: query,
|
||||
page: 1,
|
||||
page_size: 20
|
||||
@@ -1075,7 +1072,6 @@
|
||||
oldDeviceSearchLoading.value = true
|
||||
try {
|
||||
const res = await DeviceService.getDevices({
|
||||
status: 1,
|
||||
keyword: query,
|
||||
page: 1,
|
||||
page_size: 20
|
||||
@@ -1113,8 +1109,6 @@
|
||||
try {
|
||||
const res = await CardService.getStandaloneIotCards({
|
||||
is_standalone: true,
|
||||
status: 1,
|
||||
shop_id: shopId,
|
||||
keyword: query,
|
||||
page: 1,
|
||||
page_size: 20
|
||||
@@ -1174,13 +1168,14 @@
|
||||
try {
|
||||
const res = await CardService.getStandaloneIotCards({
|
||||
is_standalone: true,
|
||||
status: 1,
|
||||
shop_id: shopId,
|
||||
page: 1,
|
||||
page_size: 10
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
newIotCardOptions.value = res.data.items || []
|
||||
if (newIotCardOptions.value.length === 0) {
|
||||
ElMessage.info('暂无同店铺可用的新资产')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载IoT卡列表失败:', error)
|
||||
@@ -1228,8 +1223,6 @@
|
||||
newDeviceSearchLoading.value = true
|
||||
try {
|
||||
const res = await DeviceService.getDevices({
|
||||
status: 1,
|
||||
shop_id: shopId,
|
||||
keyword: query,
|
||||
page: 1,
|
||||
page_size: 20
|
||||
@@ -1287,13 +1280,14 @@
|
||||
newDeviceSearchLoading.value = true
|
||||
try {
|
||||
const res = await DeviceService.getDevices({
|
||||
status: 1,
|
||||
shop_id: shopId,
|
||||
page: 1,
|
||||
page_size: 10
|
||||
})
|
||||
if (res.code === 0 && res.data) {
|
||||
newDeviceOptions.value = res.data.items || []
|
||||
if (newDeviceOptions.value.length === 0) {
|
||||
ElMessage.info('暂无同店铺可用的新资产')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载设备列表失败:', error)
|
||||
|
||||
@@ -18,25 +18,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="detail-loading">
|
||||
<div v-if="polling.loading && !taskDetail" class="detail-loading">
|
||||
<ElIcon class="is-loading" :size="36">
|
||||
<Loading />
|
||||
</ElIcon>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
<DetailPage
|
||||
v-else-if="taskDetail && canViewDetail"
|
||||
v-if="taskDetail && canViewDetail"
|
||||
:sections="detailSections"
|
||||
:data="taskDetail"
|
||||
/>
|
||||
<ElEmpty v-else :description="forbidden ? '暂无权限查看该导出任务详情' : '暂无详情'" />
|
||||
<ElEmpty
|
||||
v-else
|
||||
:description="polling.forbidden ? '暂无权限查看该导出任务详情' : '暂无详情'"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, ref } from 'vue'
|
||||
import { computed, h, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElButton, ElCard, ElEmpty, ElIcon, ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
@@ -48,15 +51,29 @@
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import type { ExportTaskDetail } from '@/types/api'
|
||||
import { ExportTaskStatus } from '@/types/api'
|
||||
import { useAsyncTaskPolling } from '@/composables/useAsyncTaskPolling'
|
||||
|
||||
defineOptions({ name: 'ExportTaskDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
const loading = ref(false)
|
||||
const forbidden = ref(false)
|
||||
const taskDetail = ref<ExportTaskDetail | null>(null)
|
||||
const polling = useAsyncTaskPolling<ExportTaskDetail>({
|
||||
storageKey: `export-task-detail:${String(route.query.id || '')}`,
|
||||
autoRestore: false,
|
||||
fetchTask: async (taskId) => {
|
||||
const res = await ExportTaskService.getExportTaskDetail(taskId)
|
||||
if (res.code === 403) {
|
||||
const error = new Error('您没有查看该导出任务详情的权限') as Error & { status?: number }
|
||||
error.status = 403
|
||||
throw error
|
||||
}
|
||||
if (res.code !== 0 || !res.data) throw new Error(res.msg || '获取导出任务详情失败')
|
||||
return res.data
|
||||
},
|
||||
isForbidden: (error: any) => error?.status === 403 || error?.response?.status === 403
|
||||
})
|
||||
const taskDetail = polling.task
|
||||
|
||||
const taskPermissions = computed(
|
||||
() => getExportTaskSceneConfig(taskDetail.value?.scene)?.permissions
|
||||
@@ -102,6 +119,7 @@
|
||||
title: '任务基本信息',
|
||||
fields: [
|
||||
{ label: '任务编号', prop: 'task_no', formatter: (value: string) => value || '-' },
|
||||
{ label: '任务ID', prop: 'task_id', formatter: (value: number) => String(value ?? '-') },
|
||||
{
|
||||
label: '导出场景',
|
||||
render: (data: ExportTaskDetail) => h(ElTag, {}, () => getExportTaskSceneName(data.scene))
|
||||
@@ -132,11 +150,17 @@
|
||||
prop: 'completed_at',
|
||||
formatter: (value: string) => formatDateTime(value) || '-'
|
||||
},
|
||||
{
|
||||
label: '更新时间',
|
||||
prop: 'updated_at',
|
||||
formatter: (value: string) => formatDateTime(value) || '-'
|
||||
},
|
||||
{
|
||||
label: '错误信息',
|
||||
prop: 'error_message',
|
||||
prop: 'error_summary',
|
||||
fullWidth: true,
|
||||
formatter: (value: string) => value || '-'
|
||||
formatter: (value: string, data: ExportTaskDetail) =>
|
||||
data.error_summary || data.error_message || value || '-'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -144,6 +168,22 @@
|
||||
title: '处理进度',
|
||||
fields: [
|
||||
{ label: '进度', prop: 'progress', formatter: (value: number) => `${value ?? 0}%` },
|
||||
{
|
||||
label: '总数',
|
||||
prop: 'total_count',
|
||||
formatter: (value: number, data: ExportTaskDetail) =>
|
||||
String(value ?? data.total_rows ?? 0)
|
||||
},
|
||||
{
|
||||
label: '成功数',
|
||||
prop: 'success_count',
|
||||
formatter: (value: number) => String(value ?? 0)
|
||||
},
|
||||
{
|
||||
label: '失败数',
|
||||
prop: 'failed_count',
|
||||
formatter: (value: number) => String(value ?? 0)
|
||||
},
|
||||
{ label: '总行数', prop: 'total_rows', formatter: (value: number) => String(value ?? 0) },
|
||||
{
|
||||
label: '已处理行数',
|
||||
@@ -203,33 +243,16 @@
|
||||
|
||||
const getTaskId = () => Number(route.query.id)
|
||||
|
||||
const getTaskDetail = async () => {
|
||||
const loadTaskDetail = async () => {
|
||||
const taskId = getTaskId()
|
||||
if (!taskId) {
|
||||
ElMessage.error('缺少任务ID参数')
|
||||
goBack()
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
forbidden.value = false
|
||||
try {
|
||||
const res = await ExportTaskService.getExportTaskDetail(taskId)
|
||||
if (res.code === 0) {
|
||||
taskDetail.value = res.data
|
||||
if (!canViewDetail.value) {
|
||||
forbidden.value = true
|
||||
ElMessage.warning('您没有查看该导出任务详情的权限')
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取导出任务详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取导出任务详情失败:', error)
|
||||
ElMessage.error('获取导出任务详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
await polling.start(taskId)
|
||||
if (polling.error.value) ElMessage.error(polling.error.value)
|
||||
if (polling.forbidden.value) ElMessage.warning('您没有查看该导出任务详情的权限')
|
||||
}
|
||||
|
||||
const downloadTask = () => {
|
||||
@@ -259,10 +282,12 @@
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await ExportTaskService.cancelExportTask(taskDetail.value!.id)
|
||||
const res = await ExportTaskService.cancelExportTask(
|
||||
taskDetail.value!.task_id ?? taskDetail.value!.id
|
||||
)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(res.data?.message || '任务取消成功')
|
||||
getTaskDetail()
|
||||
polling.retry()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '取消导出任务失败')
|
||||
}
|
||||
@@ -274,7 +299,7 @@
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTaskDetail()
|
||||
loadTaskDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -49,8 +49,9 @@
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { getExportTaskSceneConfig } from '@/config/constants'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import type { ExportTaskItem, ExportTaskScene } from '@/types/api'
|
||||
import type { ExportTaskDetail, ExportTaskItem, ExportTaskScene } from '@/types/api'
|
||||
import { ExportTaskStatus } from '@/types/api'
|
||||
import { useAsyncTaskPolling } from '@/composables/useAsyncTaskPolling'
|
||||
|
||||
defineOptions({ name: 'ExportTaskList' })
|
||||
|
||||
@@ -116,11 +117,15 @@
|
||||
{ label: '任务编号', prop: 'task_no' },
|
||||
{ label: '状态', prop: 'status_name' },
|
||||
{ label: '进度', prop: 'progress' },
|
||||
{ label: '总数', prop: 'total_count' },
|
||||
{ label: '成功数', prop: 'success_count' },
|
||||
{ label: '失败数', prop: 'failed_count' },
|
||||
{ label: '格式', prop: 'format' },
|
||||
{ label: '总行数', prop: 'total_rows' },
|
||||
{ label: '已处理行数', prop: 'processed_rows' },
|
||||
{ label: '分片', prop: 'total_shards' },
|
||||
{ label: '错误信息', prop: 'error_message' },
|
||||
{ label: '错误信息', prop: 'error_summary' },
|
||||
{ label: '更新时间', prop: 'updated_at' },
|
||||
{ label: '创建时间', prop: 'created_at' }
|
||||
]
|
||||
|
||||
@@ -153,6 +158,24 @@
|
||||
width: 150,
|
||||
formatter: (row: ExportTaskItem) => h(ElProgress, { percentage: row.progress || 0 })
|
||||
},
|
||||
{
|
||||
prop: 'total_count',
|
||||
label: '总数',
|
||||
width: 100,
|
||||
formatter: (row: ExportTaskItem) => row.total_count ?? row.total_rows ?? 0
|
||||
},
|
||||
{
|
||||
prop: 'success_count',
|
||||
label: '成功数',
|
||||
width: 100,
|
||||
formatter: (row: ExportTaskItem) => row.success_count ?? 0
|
||||
},
|
||||
{
|
||||
prop: 'failed_count',
|
||||
label: '失败数',
|
||||
width: 100,
|
||||
formatter: (row: ExportTaskItem) => row.failed_count ?? 0
|
||||
},
|
||||
{ prop: 'format', label: '格式', width: 90 },
|
||||
{ prop: 'total_rows', label: '总行数', width: 110 },
|
||||
{ prop: 'processed_rows', label: '已处理行数', width: 120 },
|
||||
@@ -162,12 +185,24 @@
|
||||
width: 130,
|
||||
formatter: (row: ExportTaskItem) => `${row.success_shards || 0}/${row.total_shards || 0}`
|
||||
},
|
||||
{ prop: 'error_message', label: '错误信息', minWidth: 180, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'error_summary',
|
||||
label: '错误信息',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: ExportTaskItem) => row.error_summary || row.error_message || '-'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: ExportTaskItem) => formatDateTime(row.created_at) || '-'
|
||||
},
|
||||
{
|
||||
prop: 'updated_at',
|
||||
label: '更新时间',
|
||||
width: 180,
|
||||
formatter: (row: ExportTaskItem) => formatDateTime(row.updated_at) || '-'
|
||||
}
|
||||
])
|
||||
|
||||
@@ -257,6 +292,25 @@
|
||||
return res.data
|
||||
}
|
||||
|
||||
const activeTaskPolling = useAsyncTaskPolling<ExportTaskDetail>({
|
||||
storageKey: computed(() => `export-task-active:${currentScene.value}`).value,
|
||||
fetchTask: async (taskId) => {
|
||||
const res = await ExportTaskService.getExportTaskDetail(taskId)
|
||||
if (res.code === 403) {
|
||||
const error = new Error('暂无权限查看导出任务') as Error & { status?: number }
|
||||
error.status = 403
|
||||
throw error
|
||||
}
|
||||
if (res.code !== 0 || !res.data) throw new Error(res.msg || '获取导出任务详情失败')
|
||||
return res.data
|
||||
},
|
||||
isForbidden: (error: any) => error?.status === 403 || error?.response?.status === 403
|
||||
})
|
||||
|
||||
watch(activeTaskPolling.task, () => {
|
||||
if (activeTaskPolling.task.value) getTableData()
|
||||
})
|
||||
|
||||
const getRowPermissions = (row: ExportTaskItem) => {
|
||||
return getExportTaskSceneConfig(row.scene)?.permissions || currentPermissions.value
|
||||
}
|
||||
@@ -269,7 +323,7 @@
|
||||
|
||||
router.push({
|
||||
path: RoutesAlias.ExportTaskDetail,
|
||||
query: { id: row.id }
|
||||
query: { id: row.task_id ?? row.id }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -280,7 +334,7 @@
|
||||
}
|
||||
|
||||
try {
|
||||
const detail = await getTaskDetail(row.id)
|
||||
const detail = await getTaskDetail(row.task_id ?? row.id)
|
||||
if (!detail.download_url) {
|
||||
ElMessage.warning('当前任务暂无可用下载地址')
|
||||
return
|
||||
@@ -305,7 +359,7 @@
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const res = await ExportTaskService.cancelExportTask(row.id)
|
||||
const res = await ExportTaskService.cancelExportTask(row.task_id ?? row.id)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(res.data?.message || '任务取消成功')
|
||||
getTableData()
|
||||
|
||||
Reference in New Issue
Block a user