fix: 将导出列表分成三个模块
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m40s

This commit is contained in:
luo
2026-07-09 15:20:28 +08:00
parent f27bbacb1e
commit c1a7118d35
19 changed files with 479 additions and 86 deletions

View File

@@ -37,6 +37,7 @@
import { ElMessage } from 'element-plus'
import { ExportTaskService } from '@/api/modules'
import { useAuth } from '@/composables/useAuth'
import { getExportTaskSceneName } from '@/config/constants'
import type { CreateExportTaskResponse, ExportTaskFormat, ExportTaskScene } from '@/types/api'
const props = withDefaults(
@@ -73,15 +74,7 @@
set: (value: boolean) => emit('update:modelValue', value)
})
const sceneName = computed(() => {
const sceneMap: Record<ExportTaskScene, string> = {
device: '设备管理',
iot_card: 'IoT卡管理',
order: '订单管理'
}
return sceneMap[props.scene]
})
const sceneName = computed(() => getExportTaskSceneName(props.scene))
watch(
() => props.modelValue,

View File

@@ -0,0 +1,55 @@
import type { ExportTaskScene } from '@/types/api'
export interface ExportTaskScenePermissions {
detail: string
download: string
cancel: string
}
export interface ExportTaskSceneConfig {
scene: ExportTaskScene
sceneName: string
pageTitle: string
permissions: ExportTaskScenePermissions
}
export const EXPORT_TASK_SCENE_CONFIG: Record<ExportTaskScene, ExportTaskSceneConfig> = {
device: {
scene: 'device',
sceneName: '设备管理',
pageTitle: '导出设备',
permissions: {
detail: 'export_task:device_detail',
download: 'export_task:device_download',
cancel: 'export_task:device_cancel'
}
},
iot_card: {
scene: 'iot_card',
sceneName: 'IOT卡管理',
pageTitle: '导出IOT卡',
permissions: {
detail: 'export_task:iot_card_detail',
download: 'export_task:iot_card_download',
cancel: 'export_task:iot_card_cancel'
}
},
order: {
scene: 'order',
sceneName: '订单管理',
pageTitle: '导出订单',
permissions: {
detail: 'export_task:order_detail',
download: 'export_task:order_download',
cancel: 'export_task:order_cancel'
}
}
}
export const getExportTaskSceneConfig = (scene?: ExportTaskScene) => {
return scene ? EXPORT_TASK_SCENE_CONFIG[scene] : undefined
}
export const getExportTaskSceneName = (scene?: ExportTaskScene) => {
return getExportTaskSceneConfig(scene)?.sceneName || '-'
}

View File

@@ -31,3 +31,6 @@ export * from './roleTypes'
// 启用状态相关
export * from './enableStatus'
// 导出任务相关
export * from './exportTask'

View File

@@ -449,6 +449,9 @@
"taskManagement": "Task Management",
"exportTaskManagement": "Export Management",
"exportTaskList": "Export List",
"exportDevice": "Export Devices",
"exportIotCard": "Export IOT Cards",
"exportOrder": "Export Orders",
"exportTaskDetail": "Export Task Detail",
"exchangeManagement": "Exchange Management",
"exchangeDetail": "Exchange Order Detail"

View File

@@ -383,6 +383,9 @@
"taskManagement": "任务管理",
"exportTaskManagement": "导出管理",
"exportTaskList": "导出列表",
"exportDevice": "导出设备",
"exportIotCard": "导出IOT卡",
"exportOrder": "导出订单",
"exportTaskDetail": "导出任务详情",
"exchangeManagement": "换货管理",
"exchangeDetail": "换货单详情"

View File

@@ -331,8 +331,7 @@ function buildRouteMap(routes: AppRouteRecord[], parentPath = ''): Map<string, A
*/
function convertBackendMenuToRoute(
menu: any,
routeMap: Map<string, AppRouteRecord>,
parentPath = ''
routeMap: Map<string, AppRouteRecord>
): AppRouteRecord | null {
const menuUrl = menu.url || '/'
const matchedRoute = routeMap.get(menuUrl)
@@ -343,7 +342,7 @@ function convertBackendMenuToRoute(
// 如果当前菜单没有匹配的路由,但有 children尝试递归处理 children
if (menu.children && menu.children.length > 0) {
const children = menu.children
.map((child: any) => convertBackendMenuToRoute(child, routeMap, menuUrl))
.map((child: any) => convertBackendMenuToRoute(child, routeMap))
.filter((child: AppRouteRecord | null) => child !== null)
// 如果子菜单有有效的路由,返回一个包含子菜单的占位路由
@@ -387,7 +386,7 @@ function convertBackendMenuToRoute(
// 递归处理后端返回的 children
if (menu.children && menu.children.length > 0) {
const children = menu.children
.map((child: any) => convertBackendMenuToRoute(child, routeMap, menuUrl))
.map((child: any) => convertBackendMenuToRoute(child, routeMap))
.filter((child: AppRouteRecord | null) => child !== null)
if (children.length > 0) {

View File

@@ -451,12 +451,32 @@ export const asyncRoutes: AppRouteRecord[] = [
},
children: [
{
path: 'export-task-list',
name: 'ExportTaskList',
component: RoutesAlias.ExportTaskList,
path: 'export-device',
name: 'ExportDeviceTaskList',
component: RoutesAlias.ExportDeviceTaskList,
meta: {
title: 'menus.assetManagement.exportTaskList',
permissions: ['export_task:list'],
title: 'menus.assetManagement.exportDevice',
exportTaskScene: 'device',
keepAlive: true
}
},
{
path: 'export-iot-card',
name: 'ExportIotCardTaskList',
component: RoutesAlias.ExportIotCardTaskList,
meta: {
title: 'menus.assetManagement.exportIotCard',
exportTaskScene: 'iot_card',
keepAlive: true
}
},
{
path: 'export-order',
name: 'ExportOrderTaskList',
component: RoutesAlias.ExportOrderTaskList,
meta: {
title: 'menus.assetManagement.exportOrder',
exportTaskScene: 'order',
keepAlive: true
}
},
@@ -466,7 +486,6 @@ export const asyncRoutes: AppRouteRecord[] = [
component: RoutesAlias.ExportTaskDetail,
meta: {
title: 'menus.assetManagement.exportTaskDetail',
permissions: ['export_task:detail'],
isHide: true,
keepAlive: false
}

View File

@@ -60,7 +60,9 @@ export enum RoutesAlias {
OrderPackageInvalidateTask = '/asset-management/task-management/order-package-invalidate-task', // 订单套餐批量作废任务
OrderPackageInvalidateTaskDetail = '/asset-management/task-management/order-package-invalidate-task/detail', // 订单套餐作废任务详情
TaskDetail = '/asset-management/task-management/task-detail', // 任务详情IoT卡/设备任务详情)
ExportTaskList = '/asset-management/export-task-management/export-task-list', // 导出列表
ExportDeviceTaskList = '/asset-management/export-task-management/export-device', // 导出设备
ExportIotCardTaskList = '/asset-management/export-task-management/export-iot-card', // 导出IOT卡
ExportOrderTaskList = '/asset-management/export-task-management/export-order', // 导出订单
ExportTaskDetail = '/asset-management/export-task-management/export-task-detail', // 导出任务详情
// 订单管理

View File

@@ -3,6 +3,7 @@
*/
import { RouteRecordRaw } from 'vue-router'
import type { ExportTaskScene } from '@/types/api'
// 路由元数据
export interface RouteMeta extends Record<string | number | symbol, unknown> {
@@ -33,6 +34,8 @@ export interface RouteMeta extends Record<string | number | symbol, unknown> {
isFirstLevel?: boolean
/** 角色权限 */
roles?: string[]
/** 导出任务固定场景 */
exportTaskScene?: ExportTaskScene
/** 是否固定标签页 */
fixedTab?: boolean
}

View File

@@ -0,0 +1,9 @@
<template>
<ExportTaskList />
</template>
<script setup lang="ts">
import ExportTaskList from '../export-task-list/index.vue'
defineOptions({ name: 'ExportDeviceTaskList' })
</script>

View File

@@ -0,0 +1,9 @@
<template>
<ExportTaskList />
</template>
<script setup lang="ts">
import ExportTaskList from '../export-task-list/index.vue'
defineOptions({ name: 'ExportIotCardTaskList' })
</script>

View File

@@ -0,0 +1,9 @@
<template>
<ExportTaskList />
</template>
<script setup lang="ts">
import ExportTaskList from '../export-task-list/index.vue'
defineOptions({ name: 'ExportOrderTaskList' })
</script>

View File

@@ -10,23 +10,11 @@
返回
</ElButton>
<h2 class="detail-title">导出任务详情</h2>
<div class="detail-actions" v-if="taskDetail">
<ElButton
v-if="
taskDetail.status === ExportTaskStatus.COMPLETED && hasAuth('export_task:download')
"
type="primary"
@click="downloadTask"
>
<div class="detail-actions" v-if="taskDetail && canViewDetail">
<ElButton v-if="canDownloadTask" type="primary" @click="downloadTask">
下载文件
</ElButton>
<ElButton
v-if="canCancelTask && hasAuth('export_task:cancel')"
type="danger"
@click="cancelTask"
>
取消任务
</ElButton>
<ElButton v-if="canCancelTask" type="danger" @click="cancelTask"> 取消任务 </ElButton>
</div>
</div>
@@ -36,8 +24,12 @@
</ElIcon>
<div>加载中...</div>
</div>
<DetailPage v-else-if="taskDetail" :sections="detailSections" :data="taskDetail" />
<ElEmpty v-else description="暂无详情" />
<DetailPage
v-else-if="taskDetail && canViewDetail"
:sections="detailSections"
:data="taskDetail"
/>
<ElEmpty v-else :description="forbidden ? '暂无权限查看该导出任务详情' : '暂无详情'" />
</ElCard>
</div>
</ArtTableFullScreen>
@@ -52,8 +44,9 @@
import type { DetailSection } from '@/components/common/DetailPage.vue'
import { ExportTaskService } from '@/api/modules'
import { useAuth } from '@/composables/useAuth'
import { getExportTaskSceneConfig, getExportTaskSceneName } from '@/config/constants'
import { formatDateTime } from '@/utils/business/format'
import type { ExportTaskDetail, ExportTaskScene } from '@/types/api'
import type { ExportTaskDetail } from '@/types/api'
import { ExportTaskStatus } from '@/types/api'
defineOptions({ name: 'ExportTaskDetail' })
@@ -62,17 +55,22 @@
const router = useRouter()
const { hasAuth } = useAuth()
const loading = ref(false)
const forbidden = ref(false)
const taskDetail = ref<ExportTaskDetail | null>(null)
const getSceneName = (scene?: ExportTaskScene) => {
const sceneMap: Record<ExportTaskScene, string> = {
device: '设备管理',
iot_card: 'IoT卡管理',
order: '订单管理'
}
return scene ? sceneMap[scene] : '-'
}
const taskPermissions = computed(
() => getExportTaskSceneConfig(taskDetail.value?.scene)?.permissions
)
const canViewDetail = computed(
() => !!taskPermissions.value && hasAuth(taskPermissions.value.detail)
)
const canDownloadTask = computed(
() =>
canViewDetail.value &&
taskDetail.value?.status === ExportTaskStatus.COMPLETED &&
!!taskPermissions.value &&
hasAuth(taskPermissions.value.download)
)
const getStatusTagType = (status?: ExportTaskStatus) => {
const statusTypeMap: Record<
@@ -89,10 +87,14 @@
return status ? statusTypeMap[status] : 'info'
}
const canCancelTask = computed(() =>
[ExportTaskStatus.PENDING, ExportTaskStatus.PROCESSING].includes(
taskDetail.value?.status as ExportTaskStatus
)
const canCancelTask = computed(
() =>
canViewDetail.value &&
[ExportTaskStatus.PENDING, ExportTaskStatus.PROCESSING].includes(
taskDetail.value?.status as ExportTaskStatus
) &&
!!taskPermissions.value &&
hasAuth(taskPermissions.value.cancel)
)
const detailSections = computed((): DetailSection[] => [
@@ -102,7 +104,7 @@
{ label: '任务编号', prop: 'task_no', formatter: (value: string) => value || '-' },
{
label: '导出场景',
render: (data: ExportTaskDetail) => h(ElTag, {}, () => getSceneName(data.scene))
render: (data: ExportTaskDetail) => h(ElTag, {}, () => getExportTaskSceneName(data.scene))
},
{
label: '任务状态',
@@ -174,12 +176,13 @@
fullWidth: true,
render: (data: ExportTaskDetail) => {
if (!data.download_url) return h('span', '-')
if (!canDownloadTask.value) return h('span', '-')
return h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => window.open(data.download_url, '_blank')
onClick: () => downloadTask()
},
() => '打开下载地址'
)
@@ -209,10 +212,15 @@
}
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 || '获取导出任务详情失败')
}
@@ -225,6 +233,11 @@
}
const downloadTask = () => {
if (!canDownloadTask.value) {
ElMessage.warning('您没有下载该导出任务的权限')
return
}
if (!taskDetail.value?.download_url) {
ElMessage.warning('当前任务暂无可用下载地址')
return
@@ -235,6 +248,10 @@
const cancelTask = () => {
if (!taskDetail.value) return
if (!canCancelTask.value) {
ElMessage.warning('您没有取消该导出任务的权限')
return
}
ElMessageBox.confirm(`确定取消导出任务 ${taskDetail.value.task_no} 吗?`, '取消确认', {
confirmButtonText: '确定',

View File

@@ -5,6 +5,7 @@
v-model:filter="searchForm"
:items="searchFormItems"
label-width="110"
:show-expand="false"
@reset="handleReset"
@search="handleSearch"
/>
@@ -38,27 +39,36 @@
</template>
<script setup lang="ts">
import { h, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { computed, h, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox, ElProgress, ElTag } from 'element-plus'
import { ExportTaskService } from '@/api/modules'
import { useAuth } from '@/composables/useAuth'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { formatDateTime } from '@/utils/business/format'
import { RoutesAlias } from '@/router/routesAlias'
import { getExportTaskSceneConfig } from '@/config/constants'
import type { SearchFormItem } from '@/types'
import type { ExportTaskItem, ExportTaskScene } from '@/types/api'
import { ExportTaskStatus } from '@/types/api'
defineOptions({ name: 'ExportTaskList' })
const route = useRoute()
const router = useRouter()
const { hasAuth } = useAuth()
const loading = ref(false)
const taskList = ref<ExportTaskItem[]>([])
const defaultSceneConfig = getExportTaskSceneConfig('device')!
const routeScene = computed(() => route.meta.exportTaskScene as ExportTaskScene | undefined)
const sceneConfig = computed(
() => getExportTaskSceneConfig(routeScene.value) || defaultSceneConfig
)
const currentScene = computed(() => sceneConfig.value.scene)
const currentPermissions = computed(() => sceneConfig.value.permissions)
const initialSearchState = {
scene: undefined as ExportTaskScene | undefined,
status: undefined as ExportTaskStatus | undefined,
dateRange: [] as string[],
start_time: '',
@@ -72,12 +82,6 @@
total: 0
})
const sceneOptions = [
{ label: '设备管理', value: 'device' },
{ label: 'IoT卡管理', value: 'iot_card' },
{ label: '订单管理', value: 'order' }
]
const statusOptions = [
{ label: '待处理', value: ExportTaskStatus.PENDING },
{ label: '处理中', value: ExportTaskStatus.PROCESSING },
@@ -87,13 +91,6 @@
]
const searchFormItems: SearchFormItem[] = [
{
label: '导出场景',
prop: 'scene',
type: 'select',
config: { clearable: true, placeholder: '请选择导出场景' },
options: () => sceneOptions
},
{
label: '任务状态',
prop: 'status',
@@ -117,7 +114,6 @@
const columnOptions = [
{ label: '任务编号', prop: 'task_no' },
{ label: '场景', prop: 'scene' },
{ label: '状态', prop: 'status_name' },
{ label: '进度', prop: 'progress' },
{ label: '格式', prop: 'format' },
@@ -144,12 +140,6 @@
row.task_no || '-'
)
},
{
prop: 'scene',
label: '场景',
width: 120,
formatter: (row: ExportTaskItem) => getSceneName(row.scene)
},
{
prop: 'status_name',
label: '状态',
@@ -181,11 +171,6 @@
}
])
const getSceneName = (scene?: ExportTaskScene) => {
const option = sceneOptions.find((item) => item.value === scene)
return option?.label || '-'
}
const getStatusTagType = (status: ExportTaskStatus) => {
const statusTypeMap: Record<
ExportTaskStatus,
@@ -218,7 +203,7 @@
const res = await ExportTaskService.getExportTasks({
page: pagination.page,
page_size: pagination.pageSize,
scene: searchForm.scene,
scene: currentScene.value,
status: searchForm.status,
start_time: searchForm.start_time || undefined,
end_time: searchForm.end_time || undefined
@@ -272,8 +257,12 @@
return res.data
}
const getRowPermissions = (row: ExportTaskItem) => {
return getExportTaskSceneConfig(row.scene)?.permissions || currentPermissions.value
}
const goDetail = (row: ExportTaskItem) => {
if (!hasAuth('export_task:detail')) {
if (!hasAuth(getRowPermissions(row).detail)) {
ElMessage.warning('您没有查看导出任务详情的权限')
return
}
@@ -285,6 +274,11 @@
}
const downloadTask = async (row: ExportTaskItem) => {
if (!hasAuth(getRowPermissions(row).download)) {
ElMessage.warning('您没有下载导出任务的权限')
return
}
try {
const detail = await getTaskDetail(row.id)
if (!detail.download_url) {
@@ -300,6 +294,11 @@
}
const cancelTask = (row: ExportTaskItem) => {
if (!hasAuth(getRowPermissions(row).cancel)) {
ElMessage.warning('您没有取消导出任务的权限')
return
}
ElMessageBox.confirm(`确定取消导出任务 ${row.task_no} 吗?`, '取消确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
@@ -322,14 +321,15 @@
const getActions = (row: ExportTaskItem) => {
const actions: any[] = []
const permissions = getRowPermissions(row)
if (row.status === ExportTaskStatus.COMPLETED && hasAuth('export_task:download')) {
if (row.status === ExportTaskStatus.COMPLETED && hasAuth(permissions.download)) {
actions.push({ label: '下载', handler: () => downloadTask(row), type: 'primary' })
}
if (
[ExportTaskStatus.PENDING, ExportTaskStatus.PROCESSING].includes(row.status) &&
hasAuth('export_task:cancel')
hasAuth(permissions.cancel)
) {
actions.push({ label: '取消', handler: () => cancelTask(row), type: 'danger' })
}
@@ -340,6 +340,12 @@
onMounted(() => {
getTableData()
})
watch(currentScene, () => {
Object.assign(searchForm, { ...initialSearchState })
pagination.page = 1
getTableData()
})
</script>
<style scoped lang="scss">

View File

@@ -997,7 +997,7 @@
scene="iot_card"
:query="exportQuery"
confirm-permission="iot_card:export"
title="导出IoT卡"
title="导出IOT卡"
/>
</ElCard>
</div>