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

@@ -0,0 +1,67 @@
## Context
导出任务能力已经具备统一 API、通用列表页面、详情页面和创建弹窗。现有列表页面通过 `scene` 搜索项区分设备、IOT 卡和订单任务,但新的产品方案要求用三个固定场景页面替代该筛选方式,并让三个页面按钮权限相互独立。
## Goals / Non-Goals
- Goals: 在导出管理下提供“导出设备”“导出IOT卡”“导出订单”三个页面。
- Goals: 三个页面固定传入各自 `scene`,去掉页面中的“导出场景”筛选。
- Goals: 三个页面及其按钮/操作使用场景级独立权限编码。
- Goals: 尽量复用现有导出任务列表和详情实现,避免复制三份完整页面逻辑。
- Non-Goals: 不新增导出任务后端 API。
- Non-Goals: 不改变创建导出任务的 `scene` 值和 `query` 快照语义。
- Non-Goals: 不调整导出任务状态流转、文件生成或下载地址生成逻辑。
## Decisions
- Decision: 使用一个可复用导出任务列表实现,通过路由配置或组件 props 注入固定场景、页面标题和权限映射。
- Rationale: 三个页面除固定 `scene` 和权限外,分页、状态筛选、时间筛选、下载、取消逻辑一致,复用可以减少重复代码和后续维护成本。
- Decision: 页面查询参数由页面固定注入 `scene`,不从搜索表单读取“导出场景”。
- Rationale: 页面已经按场景拆分,继续暴露场景筛选会造成入口语义冲突,也可能让用户在“导出设备”页面查到 IOT 卡或订单任务。
- Decision: 权限编码按场景和操作拆分,默认建议如下。
- Rationale: 用户明确要求三个界面的按钮权限编码不同;列表访问、详情、下载、取消均需要避免跨场景复用。
| 场景 | 页面访问 | 详情 | 下载 | 取消 |
| --- | --- | --- | --- | --- |
| 导出设备 | `export_task:device:list` | `export_task:device:detail` | `export_task:device:download` | `export_task:device:cancel` |
| 导出IOT卡 | `export_task:iot_card:list` | `export_task:iot_card:detail` | `export_task:iot_card:download` | `export_task:iot_card:cancel` |
| 导出订单 | `export_task:order:list` | `export_task:order:detail` | `export_task:order:download` | `export_task:order:cancel` |
- Decision: 业务列表创建导出任务按钮继续使用来源业务权限,默认建议如下。
- Rationale: 创建导出任务属于来源业务列表能力,不应因为拥有某个导出任务管理页面权限就获得业务列表导出入口。
| 入口 | 创建导出任务权限 | scene |
| --- | --- | --- |
| 设备管理 | `devices:export` | `device` |
| IOT 卡管理 | `iot_card:export` | `iot_card` |
| 订单列表 | `orders:export` | `order` |
- Decision: 详情页可继续复用同一组件,但必须根据任务场景或来源页面使用对应场景的详情、下载、取消权限。
- Rationale: 详情页面本身也包含下载和取消按钮,若继续使用通用权限会绕过三个列表页面的按钮隔离要求。
## Risks / Trade-offs
- Risk: 后端或权限配置已经发放了通用 `export_task:*` 权限,切换为场景级权限后用户可能暂时看不到页面或按钮。
- Mitigation: 实施时同步更新权限种子/菜单配置,并在上线前完成角色权限迁移或重新授权。
- Risk: 订单导出场景提案正在独立推进,可能继续给旧“导出列表”增加订单筛选。
- Mitigation: 实施本变更前先合并范围,保留 `scene=order` 和订单创建入口,移除旧列表中的订单场景筛选实现。
- Risk: 详情页直接通过 URL 打开时缺少来源页面上下文。
- Mitigation: 详情页读取任务详情后按返回的 `scene` 计算对应权限,缺少该场景详情权限时展示无权限提示并阻止后续按钮操作。
## Migration Plan
1. 增加三条导出任务列表路由和菜单项,删除或隐藏旧通用“导出列表”入口。
2. 将导出任务列表改造成固定场景列表,移除“导出场景”搜索项和冗余场景列。
3. 为三页分别注入固定 `scene`、标题和场景级权限映射。
4. 调整导出任务详情页和行操作的权限判断,改用任务场景对应权限。
5. 同步订单导出入口,确保订单导出任务进入“导出订单”页面查看。
6. 更新后端/菜单权限配置并完成角色授权迁移。
## Open Questions
- 最终权限编码是否采用本设计建议,还是需要对齐后端已有权限命名规范。
- 旧通用导出列表路由是否直接移除,还是短期隐藏并重定向到用户有权限的第一个场景页面。

View File

@@ -0,0 +1,39 @@
# Change: 拆分导出任务列表为固定场景页面
## Why
当前导出管理只有一个“导出列表”页面需要用户通过“导出场景”筛选设备、IOT 卡和订单导出任务。产品希望把该页面拆成三个固定场景页面,入口更直接,并且三个页面及页面按钮使用不同权限编码。
## What Changes
- 将导出管理下的单个“导出列表”替换为三个页面:
- 导出设备
- 导出IOT卡
- 导出订单
- 三个页面分别固定查询 `scene=device``scene=iot_card``scene=order`,页面内不再展示或提交用户可选的“导出场景”筛选项。
- 导出列表中的“场景”列可移除或默认不展示,因为页面本身已经代表固定场景。
- 三个页面的菜单访问、详情、下载、取消等按钮/操作均使用场景级独立权限编码,不再复用同一组通用导出任务按钮权限。
- 业务列表发起导出的按钮权限继续按来源业务隔离设备导出、IOT 卡导出、订单导出使用不同权限编码。
- 订单导出任务仍使用 `scene=order`,并与现有订单导出场景提案合并实施或调整其导出列表筛选相关内容。
## Impact
- Affected specs:
- `export-task-management`
- Affected code:
- `src/router/routesAlias.ts`
- `src/router/routes/asyncRoutes.ts`
- `src/views/asset-management/export-task-management/export-task-list/index.vue`
- `src/views/asset-management/export-task-management/export-task-detail/index.vue`
- `src/types/api/exportTask.ts`
- `src/components/business/ExportTaskCreateDialog.vue`
- `src/views/asset-management/device-list/index.vue`
- `src/views/asset-management/iot-card-management/index.vue`
- `src/views/order-management/order-list/index.vue`
- 菜单、权限和国际化文案配置
- Dependencies:
- 后端导出任务列表接口继续支持 `scene=device``scene=iot_card``scene=order` 过滤。
- 后端/权限配置需要提供三套场景级导出任务权限编码。
- 现有 `add-order-export-scene` 提案与本提案存在重叠:其“在导出列表场景筛选中新增订单”应被本提案的三个固定页面方案替代。
- Breaking changes:
- 原通用“导出列表”菜单入口和 `export_task:list/detail/download/cancel` 这组通用页面操作权限将被场景级页面/按钮权限取代。

View File

@@ -0,0 +1,111 @@
## ADDED Requirements
### Requirement: Scene-Specific Export Task Pages
The admin frontend SHALL replace the single generic export task list entry with three scene-specific export task pages: 导出设备, 导出IOT卡, and 导出订单.
#### Scenario: Render scene-specific export task navigation
- **GIVEN** 用户打开资产管理下的导出管理菜单
- **WHEN** 系统渲染导出任务导航
- **THEN** 系统 MUST provide separate entries for 导出设备, 导出IOT卡, and 导出订单
- **AND** 系统 MUST NOT expose the old generic 导出列表 menu entry as the primary navigation entry
#### Scenario: Query device export tasks
- **GIVEN** 用户打开导出设备页面
- **WHEN** 页面查询导出任务列表
- **THEN** 系统 MUST call `GET /api/admin/export-tasks` with `scene=device`
- **AND** 用户 MUST NOT be able to change this page to query `iot_card` or `order` tasks through a scene selector
#### Scenario: Query IOT card export tasks
- **GIVEN** 用户打开导出IOT卡页面
- **WHEN** 页面查询导出任务列表
- **THEN** 系统 MUST call `GET /api/admin/export-tasks` with `scene=iot_card`
- **AND** 用户 MUST NOT be able to change this page to query `device` or `order` tasks through a scene selector
#### Scenario: Query order export tasks
- **GIVEN** 用户打开导出订单页面
- **WHEN** 页面查询导出任务列表
- **THEN** 系统 MUST call `GET /api/admin/export-tasks` with `scene=order`
- **AND** 用户 MUST NOT be able to change this page to query `device` or `iot_card` tasks through a scene selector
### Requirement: Remove Export Scene Filtering From Scene Pages
Scene-specific export task pages SHALL remove user-facing export scene filtering because each page has exactly one fixed scene.
#### Scenario: Hide scene search field
- **GIVEN** 用户打开任一导出任务场景页面
- **WHEN** 页面渲染搜索表单
- **THEN** 页面 MUST NOT display a 导出场景 search item
- **AND** reset and search actions MUST keep the page's fixed `scene` unchanged
#### Scenario: Avoid redundant scene column
- **GIVEN** 用户打开任一导出任务场景页面
- **WHEN** 页面渲染任务表格
- **THEN** 页面 SHOULD remove or hide the 场景 column by default
- **AND** 页面 MUST continue to display task number, status, progress, format, row counts, shard progress, error message, and created time
### Requirement: Scene-Specific Export Task Permissions
The admin frontend MUST gate each export task page and page action with scene-specific permission codes so that device, IOT card, and order export task permissions are independent.
#### Scenario: Gate scene pages independently
- **GIVEN** 用户 only has `export_task:device:list`
- **WHEN** 系统渲染导出管理菜单
- **THEN** 系统 MUST show 导出设备
- **AND** 系统 MUST NOT show 导出IOT卡 or 导出订单 unless the user also has `export_task:iot_card:list` or `export_task:order:list`
#### Scenario: Gate device export task actions independently
- **GIVEN** 用户打开导出设备页面
- **WHEN** 页面渲染详情、下载 or 取消操作
- **THEN** 系统 MUST use `export_task:device:detail`, `export_task:device:download`, and `export_task:device:cancel` respectively
- **AND** 系统 MUST NOT treat IOT card or order export task action permissions as sufficient for device actions
#### Scenario: Gate IOT card export task actions independently
- **GIVEN** 用户打开导出IOT卡页面
- **WHEN** 页面渲染详情、下载 or 取消操作
- **THEN** 系统 MUST use `export_task:iot_card:detail`, `export_task:iot_card:download`, and `export_task:iot_card:cancel` respectively
- **AND** 系统 MUST NOT treat device or order export task action permissions as sufficient for IOT card actions
#### Scenario: Gate order export task actions independently
- **GIVEN** 用户打开导出订单页面
- **WHEN** 页面渲染详情、下载 or 取消操作
- **THEN** 系统 MUST use `export_task:order:detail`, `export_task:order:download`, and `export_task:order:cancel` respectively
- **AND** 系统 MUST NOT treat device or IOT card export task action permissions as sufficient for order actions
#### Scenario: Keep business export buttons independent
- **GIVEN** 用户分别访问设备管理、IOT 卡管理 and 订单列表
- **WHEN** 页面渲染创建导出任务按钮
- **THEN** 设备管理 MUST use `devices:export`
- **AND** IOT 卡管理 MUST use `iot_card:export`
- **AND** 订单列表 MUST use `orders:export`
- **AND** possessing one of these permissions MUST NOT reveal or enable the other two export buttons
### Requirement: Scene-Aware Export Task Detail Permissions
The export task detail page SHALL apply detail, download, and cancel permissions according to the task's scene.
#### Scenario: Enforce scene permissions on direct detail access
- **GIVEN** 用户直接打开某个导出任务详情 URL
- **WHEN** 系统加载任务详情 and reads the returned `scene`
- **THEN** 系统 MUST require the matching scene detail permission before showing the detail content
- **AND** 系统 MUST use the matching scene download and cancel permissions before showing detail-page buttons
#### Scenario: Prevent cross-scene action leakage
- **GIVEN** 用户 has only order export task download permission
- **AND** 用户打开 a completed device export task detail
- **WHEN** 页面渲染详情操作区
- **THEN** 页面 MUST NOT show the device task download button
- **AND** 页面 MUST NOT allow downloading that device task through the order download permission

View File

@@ -0,0 +1,46 @@
## 1. Proposal Review
- [x] 1.1 确认三个页面名称为“导出设备”“导出IOT卡”“导出订单”。
- [x] 1.2 确认固定场景值分别为 `device``iot_card``order`
- [x] 1.3 确认三套页面和按钮权限编码,或按后端权限命名规范替换设计中的默认建议。
- [x] 1.4 确认本提案与 `add-order-export-scene` 合并实施,订单场景不再通过旧导出场景筛选进入。
## 2. Routing And Menu
- [x] 2.1 新增三条导出任务列表路由别名和路由记录。
- [x] 2.2 在导出管理菜单下新增“导出设备”“导出IOT卡”“导出订单”。
- [x] 2.3 删除或隐藏旧“导出列表”菜单入口。
- [x] 2.4 为三条路由分别配置独立页面访问权限。
- [x] 2.5 更新菜单国际化文案和权限配置。
## 3. Export Task List Pages
- [x] 3.1 将现有导出任务列表改造成可复用固定场景列表,支持传入页面标题、固定 `scene` 和权限映射。
- [x] 3.2 移除搜索表单中的“导出场景”筛选项。
- [x] 3.3 列表查询始终携带当前页面固定 `scene`
- [x] 3.4 移除或默认隐藏列表中的“场景”列。
- [x] 3.5 保留任务状态、创建时间、分页、刷新等现有列表能力。
## 4. Scene-Specific Actions
- [x] 4.1 详情入口按当前页面场景检查对应 `detail` 权限。
- [x] 4.2 下载按钮按当前页面场景检查对应 `download` 权限。
- [x] 4.3 取消按钮按当前页面场景检查对应 `cancel` 权限。
- [x] 4.4 导出任务详情页根据任务 `scene` 使用对应详情、下载、取消权限。
- [x] 4.5 验证只有某一场景权限时,不会看到其他场景页面或按钮。
## 5. Business Export Entrypoints
- [x] 5.1 设备管理导出按钮继续使用 `devices:export` 并创建 `scene=device` 任务。
- [x] 5.2 IOT 卡管理导出按钮继续使用 `iot_card:export` 并创建 `scene=iot_card` 任务。
- [x] 5.3 订单列表导出按钮使用 `orders:export` 并创建 `scene=order` 任务。
- [x] 5.4 确认三个业务导出按钮权限编码互不复用。
## 6. Verification
- [x] 6.1 验证“导出设备”页面请求 `GET /api/admin/export-tasks` 时固定携带 `scene=device`
- [x] 6.2 验证“导出IOT卡”页面请求固定携带 `scene=iot_card`
- [x] 6.3 验证“导出订单”页面请求固定携带 `scene=order`
- [x] 6.4 验证三个页面均不展示“导出场景”筛选项。
- [x] 6.5 验证三页详情、下载、取消按钮按各自权限显示和拦截。
- [x] 6.6 运行类型检查、lint 和相关页面手动回归。

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>