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:
@@ -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