feat: 七月迭代公共状态与异步任务交互
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m59s

This commit is contained in:
luo
2026-07-24 11:48:02 +08:00
parent 15122e225a
commit a0c44ff538
14 changed files with 677 additions and 63 deletions

View File

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

View File

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

View File

@@ -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()