This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="bulk-purchase-detail-page" id="table-full-screen">
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<div class="detail-header">
|
||||
<ElButton @click="goBack">
|
||||
<template #icon>
|
||||
<ElIcon><ArrowLeft /></ElIcon>
|
||||
</template>
|
||||
返回
|
||||
</ElButton>
|
||||
<h2 class="detail-title">批量订购任务详情</h2>
|
||||
<ElTag v-if="taskDetail" :type="getStatusType(taskDetail.status)">
|
||||
{{ taskDetail.status_name || getStatusName(taskDetail.status) }}
|
||||
</ElTag>
|
||||
</div>
|
||||
|
||||
<div v-if="loading && !taskDetail" class="detail-loading">
|
||||
<ElIcon class="is-loading" :size="36"><Loading /></ElIcon>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
|
||||
<template v-else-if="taskDetail">
|
||||
<ElDescriptions :column="4" border>
|
||||
<ElDescriptionsItem label="任务号">{{ taskDetail.task_no || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="订单文件" :span="2">
|
||||
<span class="ellipsis-value" :title="taskDetail.file_name || '-'">
|
||||
{{ taskDetail.file_name || '-' }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="套餐名称">
|
||||
<span class="ellipsis-value" :title="taskDetail.package_name || '-'">
|
||||
{{ taskDetail.package_name || '-' }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="套餐编码">
|
||||
<span class="ellipsis-value" :title="taskDetail.package_code || '-'">
|
||||
{{ taskDetail.package_code || '-' }}
|
||||
</span>
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="支付方式">
|
||||
{{ getPaymentMethodName(taskDetail.payment_method) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="总数">{{ taskDetail.total_count ?? 0 }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成功数">{{
|
||||
taskDetail.success_count ?? 0
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="失败数">{{ taskDetail.fail_count ?? 0 }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建人">{{
|
||||
taskDetail.creator_name || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="支付凭证">
|
||||
{{
|
||||
taskDetail.voucher_keys?.length
|
||||
? `已上传 ${taskDetail.voucher_keys.length} 个`
|
||||
: '未上传'
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="创建时间">{{
|
||||
formatDateTime(taskDetail.created_at)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="开始时间">{{
|
||||
formatDateTime(taskDetail.started_at)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="完成时间">{{
|
||||
formatDateTime(taskDetail.completed_at)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="更新时间">{{
|
||||
formatDateTime(taskDetail.updated_at)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="taskDetail.error_message" label="错误信息" :span="4">
|
||||
{{ taskDetail.error_message }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElAlert
|
||||
v-if="taskDetail.error_summary"
|
||||
class="task-error"
|
||||
type="error"
|
||||
:closable="false"
|
||||
:title="taskDetail.error_summary"
|
||||
/>
|
||||
|
||||
<template v-if="hasAuth(BULK_PURCHASE_PERMISSIONS.items)">
|
||||
<div class="items-toolbar">
|
||||
<ElSelect
|
||||
v-model="itemStatus"
|
||||
clearable
|
||||
placeholder="筛选行状态"
|
||||
style="width: 140px"
|
||||
>
|
||||
<ElOption label="成功" :value="BulkPurchaseTaskStatus.COMPLETED" />
|
||||
<ElOption label="失败" :value="BulkPurchaseTaskStatus.FAILED" />
|
||||
</ElSelect>
|
||||
<ElButton :loading="loading" @click="refreshTask">刷新结果</ElButton>
|
||||
</div>
|
||||
|
||||
<ElTable v-loading="loading" :data="paginatedItems" border>
|
||||
<ElTableColumn label="行号" width="90">
|
||||
<template #default="scope">{{ scope.row.line ?? '-' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="资产" min-width="180">
|
||||
<template #default="scope">{{ getAssetIdentifier(scope.row) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="110">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getItemStatusType(scope.row.status)">
|
||||
{{ scope.row.status_name || getItemStatusName(scope.row.status) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="order_no" label="订单号" min-width="180" show-overflow-tooltip />
|
||||
<ElTableColumn label="金额" width="120">
|
||||
<template #default="scope">{{ formatMoney(scope.row.amount) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="处理结果" min-width="240" show-overflow-tooltip>
|
||||
<template #default="scope">{{ getItemReason(scope.row) }}</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<ElPagination
|
||||
v-model:current-page="itemsPage"
|
||||
v-model:page-size="itemsSize"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="filteredItems.length"
|
||||
@size-change="handleItemSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<ElEmpty v-else :description="forbidden ? '暂无查看权限' : '暂无任务详情'" />
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem,
|
||||
ElEmpty,
|
||||
ElIcon,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElPagination,
|
||||
ElSelect,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag
|
||||
} from 'element-plus'
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
import { BulkPurchaseService } from '@/api/modules'
|
||||
import type { BulkPurchaseItem, BulkPurchasePaymentMethod, BulkPurchaseTask } from '@/types/api'
|
||||
import { BulkPurchaseTaskStatus } from '@/types/api'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useAsyncTaskPolling } from '@/composables/useAsyncTaskPolling'
|
||||
import { formatDateTime, formatMoney } from '@/utils/business/format'
|
||||
import { BULK_PURCHASE_PERMISSIONS } from '@/config/constants/bulkPurchase'
|
||||
|
||||
defineOptions({ name: 'BulkPurchaseDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
const itemStatus = ref<BulkPurchaseTaskStatus | undefined>()
|
||||
const itemsPage = ref(1)
|
||||
const itemsSize = ref(20)
|
||||
const taskId = Number(route.params.id)
|
||||
|
||||
const polling = useAsyncTaskPolling<BulkPurchaseTask>({
|
||||
storageKey: `bulk-purchase-detail:${taskId}`,
|
||||
autoRestore: false,
|
||||
fetchTask: async (id) => {
|
||||
const res = await BulkPurchaseService.getTask(id)
|
||||
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 loading = polling.loading
|
||||
const forbidden = polling.forbidden
|
||||
|
||||
const loadTaskDetail = async () => {
|
||||
if (!taskId) {
|
||||
ElMessage.error('缺少任务 ID 参数')
|
||||
goBack()
|
||||
return
|
||||
}
|
||||
|
||||
await polling.start(taskId)
|
||||
if (polling.error.value) ElMessage.error(polling.error.value)
|
||||
if (polling.forbidden.value) ElMessage.warning('暂无查看该批量订购任务详情的权限')
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const refreshTask = () => {
|
||||
void polling.retry()
|
||||
}
|
||||
|
||||
const getStatusName = (status?: BulkPurchaseTaskStatus) => {
|
||||
const names: Record<BulkPurchaseTaskStatus, string> = {
|
||||
[BulkPurchaseTaskStatus.PENDING]: '待处理',
|
||||
[BulkPurchaseTaskStatus.PROCESSING]: '处理中',
|
||||
[BulkPurchaseTaskStatus.COMPLETED]: '已完成',
|
||||
[BulkPurchaseTaskStatus.FAILED]: '已失败',
|
||||
[BulkPurchaseTaskStatus.CANCELED]: '已取消'
|
||||
}
|
||||
return status ? names[status] || '-' : '-'
|
||||
}
|
||||
|
||||
const getStatusType = (status?: BulkPurchaseTaskStatus) => {
|
||||
if (status === BulkPurchaseTaskStatus.COMPLETED) return 'success'
|
||||
if (status === BulkPurchaseTaskStatus.FAILED) return 'danger'
|
||||
if (status === BulkPurchaseTaskStatus.PROCESSING) return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
const getPaymentMethodName = (method?: BulkPurchasePaymentMethod) => {
|
||||
return method === 'offline' ? '线下支付' : method === 'wallet' ? '代理钱包' : '-'
|
||||
}
|
||||
|
||||
const getAssetIdentifier = (item: BulkPurchaseItem) =>
|
||||
item.asset_identifier || item.iccid || item.virtual_no || '-'
|
||||
|
||||
const getItemStatusName = (status?: BulkPurchaseTaskStatus) => {
|
||||
if (status === BulkPurchaseTaskStatus.COMPLETED) return '成功'
|
||||
if (status === BulkPurchaseTaskStatus.FAILED) return '失败'
|
||||
return '-'
|
||||
}
|
||||
|
||||
const getItemStatusType = (status?: BulkPurchaseTaskStatus) => {
|
||||
if (status === BulkPurchaseTaskStatus.COMPLETED) return 'success'
|
||||
if (status === BulkPurchaseTaskStatus.FAILED) return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
const getItemReason = (item: BulkPurchaseItem) =>
|
||||
item.reason || item.error_summary || item.error_reason || '-'
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
const items = taskDetail.value?.items || []
|
||||
if (itemStatus.value === undefined) return items
|
||||
return items.filter((item) => item.status === itemStatus.value)
|
||||
})
|
||||
|
||||
const paginatedItems = computed(() => {
|
||||
const start = (itemsPage.value - 1) * itemsSize.value
|
||||
return filteredItems.value.slice(start, start + itemsSize.value)
|
||||
})
|
||||
|
||||
const handleItemSizeChange = () => {
|
||||
itemsPage.value = 1
|
||||
}
|
||||
|
||||
watch(itemStatus, () => {
|
||||
itemsPage.value = 1
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void loadTaskDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bulk-purchase-detail-page {
|
||||
.detail-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 220px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.ellipsis-value {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: bottom;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-error {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.items-toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin: 20px 0 12px;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,506 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="bulk-purchase-page" id="table-full-screen">
|
||||
<ElDrawer
|
||||
v-model="createDrawerVisible"
|
||||
title="批量订购套餐"
|
||||
direction="rtl"
|
||||
size="520px"
|
||||
:close-on-click-modal="!createDrawerBusy"
|
||||
:close-on-press-escape="!createDrawerBusy"
|
||||
@closed="handleCreateDrawerClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="110px" class="create-form">
|
||||
<ElFormItem label="套餐" prop="package_id">
|
||||
<ElSelect
|
||||
v-model="form.package_id"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:remote-method="handlePackageSearch"
|
||||
:loading="packageLoading"
|
||||
placeholder="请输入套餐名称搜索"
|
||||
style="width: 100%"
|
||||
@visible-change="handlePackageVisibleChange"
|
||||
>
|
||||
<ElOption
|
||||
v-for="pkg in packageOptions"
|
||||
:key="pkg.id"
|
||||
:label="pkg.package_name"
|
||||
:value="pkg.id"
|
||||
>
|
||||
<span class="package-option">{{ pkg.package_name }}</span>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="支付方式" prop="payment_method">
|
||||
<ElRadioGroup v-model="form.payment_method" @change="handlePaymentMethodChange">
|
||||
<ElRadio value="wallet">代理钱包</ElRadio>
|
||||
<ElRadio value="offline">线下支付</ElRadio>
|
||||
</ElRadioGroup>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="订单文件" prop="orderFile">
|
||||
<VoucherUpload
|
||||
ref="orderUploadRef"
|
||||
v-model="orderFileKeys"
|
||||
voucher-name="订单文件"
|
||||
:max-count="1"
|
||||
purpose="batch_purchase"
|
||||
:max-size-mb="10"
|
||||
single-column-csv
|
||||
:max-csv-rows="1000"
|
||||
accept=".csv"
|
||||
tip="仅支持 UTF-8 单列 CSV,最多 1000 行,最大 10MB"
|
||||
@uploading-change="orderFileUploading = $event"
|
||||
@change="formRef?.validateField('orderFile')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="form.payment_method === 'offline'"
|
||||
label="整批支付凭证"
|
||||
prop="voucherFile"
|
||||
>
|
||||
<VoucherUpload
|
||||
ref="voucherUploadRef"
|
||||
v-model="voucherFileKeys"
|
||||
voucher-name="整批支付凭证"
|
||||
@uploading-change="voucherUploading = $event"
|
||||
@change="formRef?.validateField('voucherFile')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElAlert
|
||||
v-if="createDrawerBusy"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="
|
||||
voucherUploading
|
||||
? '支付凭证上传中,请稍候'
|
||||
: orderFileUploading
|
||||
? '订单文件上传中,请稍候'
|
||||
: '批量订购文件上传及任务创建中,请勿重复提交'
|
||||
"
|
||||
/>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<ElButton :disabled="createDrawerBusy" @click="createDrawerVisible = false">
|
||||
取消
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="createDrawerBusy"
|
||||
:disabled="!hasAuth(BULK_PURCHASE_PERMISSIONS.create)"
|
||||
@click="submitTask"
|
||||
>
|
||||
创建批量订购任务
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
|
||||
<ElCard
|
||||
v-if="hasAuth(BULK_PURCHASE_PERMISSIONS.detail)"
|
||||
shadow="never"
|
||||
class="art-table-card task-list-card"
|
||||
>
|
||||
<template #header>
|
||||
<div class="task-list-header">
|
||||
<span>批量订购任务</span>
|
||||
<div class="task-list-toolbar">
|
||||
<ElButton
|
||||
v-if="hasAuth(BULK_PURCHASE_PERMISSIONS.template)"
|
||||
tag="a"
|
||||
href="/templates/bulk-purchase-template.csv"
|
||||
download="批量订购套餐模板.csv"
|
||||
>
|
||||
下载模板
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="hasAuth(BULK_PURCHASE_PERMISSIONS.create)"
|
||||
type="primary"
|
||||
@click="openCreateDrawer"
|
||||
>
|
||||
批量订购套餐
|
||||
</ElButton>
|
||||
<ElSelect
|
||||
v-model="taskListStatus"
|
||||
clearable
|
||||
placeholder="筛选任务状态"
|
||||
style="width: 150px"
|
||||
@change="handleTaskListStatusChange"
|
||||
>
|
||||
<ElOption label="待处理" :value="BulkPurchaseTaskStatus.PENDING" />
|
||||
<ElOption label="处理中" :value="BulkPurchaseTaskStatus.PROCESSING" />
|
||||
<ElOption label="已完成" :value="BulkPurchaseTaskStatus.COMPLETED" />
|
||||
<ElOption label="已失败" :value="BulkPurchaseTaskStatus.FAILED" />
|
||||
<ElOption label="已取消" :value="BulkPurchaseTaskStatus.CANCELED" />
|
||||
</ElSelect>
|
||||
<ElButton :loading="taskListLoading" @click="loadTaskList">刷新</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElTable v-loading="taskListLoading" :data="taskList" border>
|
||||
<ElTableColumn label="任务号" min-width="200" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<ElButton type="primary" link @click="showTask(scope.row)">
|
||||
{{ scope.row.task_no || '-' }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="file_name" label="订单文件" min-width="180" show-overflow-tooltip />
|
||||
<ElTableColumn
|
||||
prop="package_name"
|
||||
label="套餐名称"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="package_code"
|
||||
label="套餐编码"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<ElTableColumn label="支付方式" width="110">
|
||||
<template #default="scope">{{
|
||||
getPaymentMethodName(scope.row.payment_method)
|
||||
}}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="110">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getStatusType(scope.row.status)">
|
||||
{{ scope.row.status_name || getStatusName(scope.row.status) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="total_count" label="总数" width="90" />
|
||||
<ElTableColumn prop="success_count" label="成功数" width="90" />
|
||||
<ElTableColumn prop="fail_count" label="失败数" width="90" />
|
||||
<ElTableColumn label="创建时间" width="180">
|
||||
<template #default="scope">{{ formatDateTime(scope.row.created_at) }}</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<ElPagination
|
||||
v-model:current-page="taskListPage"
|
||||
v-model:page-size="taskListSize"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="taskListTotal"
|
||||
@current-change="loadTaskList"
|
||||
@size-change="handleTaskListSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElDrawer,
|
||||
ElForm,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElPagination,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag
|
||||
} from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
import { BulkPurchaseService, PackageManageService } from '@/api/modules'
|
||||
import type { BulkPurchasePaymentMethod, BulkPurchaseTask, PackageResponse } from '@/types/api'
|
||||
import { BulkPurchaseTaskStatus } from '@/types/api'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { BULK_PURCHASE_PERMISSIONS } from '@/config/constants/bulkPurchase'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
|
||||
defineOptions({ name: 'BulkPurchase' })
|
||||
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
const formRef = ref<FormInstance>()
|
||||
const orderUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const voucherUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const createDrawerVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const orderFileKeys = ref<string[]>([])
|
||||
const orderFileUploading = ref(false)
|
||||
const voucherFileKeys = ref<string[]>([])
|
||||
const voucherUploading = ref(false)
|
||||
const packageOptions = ref<PackageResponse[]>([])
|
||||
const packageLoading = ref(false)
|
||||
const taskList = ref<BulkPurchaseTask[]>([])
|
||||
const taskListLoading = ref(false)
|
||||
const taskListPage = ref(1)
|
||||
const taskListSize = ref(20)
|
||||
const taskListTotal = ref(0)
|
||||
const taskListStatus = ref<BulkPurchaseTaskStatus | undefined>()
|
||||
const createDrawerBusy = computed(
|
||||
() => submitting.value || voucherUploading.value || orderFileUploading.value
|
||||
)
|
||||
|
||||
const form = reactive<{
|
||||
package_id?: number
|
||||
payment_method: BulkPurchasePaymentMethod
|
||||
}>({
|
||||
package_id: undefined,
|
||||
payment_method: 'wallet'
|
||||
})
|
||||
|
||||
const rules = computed<FormRules>(() => ({
|
||||
package_id: [{ required: true, message: '请选择套餐', trigger: 'change' }],
|
||||
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
|
||||
orderFile: [
|
||||
{
|
||||
validator: (_rule: unknown, _value: unknown, callback: (error?: Error) => void) => {
|
||||
if (orderFileKeys.value.length === 0) callback(new Error('请上传订单文件'))
|
||||
else callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
voucherFile: [
|
||||
{
|
||||
validator: (_rule: unknown, _value: unknown, callback: (error?: Error) => void) => {
|
||||
if (form.payment_method === 'offline' && voucherFileKeys.value.length === 0) {
|
||||
callback(new Error('线下支付必须上传整批支付凭证'))
|
||||
} else callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}))
|
||||
|
||||
const loadPackages = async (query = '') => {
|
||||
packageLoading.value = true
|
||||
try {
|
||||
const res = await PackageManageService.getPackages({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
package_name: query.trim() || undefined,
|
||||
status: 1,
|
||||
shelf_status: 1
|
||||
})
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(res.msg || '获取套餐列表失败')
|
||||
return
|
||||
}
|
||||
|
||||
packageOptions.value = (res.data?.items || []).filter(
|
||||
(item) => item.status === 1 && item.shelf_status === 1
|
||||
)
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '获取套餐列表失败')
|
||||
} finally {
|
||||
packageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handlePackageSearch = (query: string) => {
|
||||
void loadPackages(query)
|
||||
}
|
||||
|
||||
const handlePackageVisibleChange = (visible: boolean) => {
|
||||
if (visible && packageOptions.value.length === 0) void loadPackages()
|
||||
}
|
||||
|
||||
const openCreateDrawer = () => {
|
||||
createDrawerVisible.value = true
|
||||
if (packageOptions.value.length === 0) void loadPackages()
|
||||
}
|
||||
|
||||
const handleCreateDrawerClosed = () => {
|
||||
formRef.value?.resetFields()
|
||||
form.package_id = undefined
|
||||
form.payment_method = 'wallet'
|
||||
orderFileKeys.value = []
|
||||
voucherFileKeys.value = []
|
||||
orderUploadRef.value?.clearFiles(false)
|
||||
voucherUploadRef.value?.clearFiles(false)
|
||||
orderFileUploading.value = false
|
||||
voucherUploading.value = false
|
||||
}
|
||||
|
||||
const handlePaymentMethodChange = (value: string | number | boolean | undefined) => {
|
||||
if (value === 'wallet') {
|
||||
voucherFileKeys.value = []
|
||||
voucherUploadRef.value?.clearFiles()
|
||||
formRef.value?.clearValidate('voucherFile')
|
||||
}
|
||||
}
|
||||
|
||||
const submitTask = async () => {
|
||||
if (
|
||||
!hasAuth(BULK_PURCHASE_PERMISSIONS.create) ||
|
||||
submitting.value ||
|
||||
voucherUploading.value ||
|
||||
orderFileUploading.value
|
||||
)
|
||||
return
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid || !form.package_id || orderFileKeys.value.length === 0) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const data = {
|
||||
file_key: orderFileKeys.value[0],
|
||||
package_id: form.package_id,
|
||||
payment_method: form.payment_method,
|
||||
...(form.payment_method === 'offline' ? { voucher_keys: voucherFileKeys.value } : {})
|
||||
}
|
||||
|
||||
const res = await BulkPurchaseService.createTask(data)
|
||||
const taskId = res.data?.task_id ?? res.data?.id
|
||||
if (res.code !== 0 || !taskId) {
|
||||
ElMessage.error(res.msg || '创建批量订购任务失败')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.success(res.data.message || '批量订购任务已创建')
|
||||
createDrawerVisible.value = false
|
||||
await router.push(`${RoutesAlias.BulkPurchaseDetail}/${taskId}`)
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '创建批量订购任务失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadTaskList = async () => {
|
||||
if (!hasAuth(BULK_PURCHASE_PERMISSIONS.detail)) return
|
||||
taskListLoading.value = true
|
||||
try {
|
||||
const res = await BulkPurchaseService.getTasks({
|
||||
page: taskListPage.value,
|
||||
page_size: taskListSize.value,
|
||||
status: taskListStatus.value
|
||||
})
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(res.msg || '获取批量订购任务列表失败')
|
||||
return
|
||||
}
|
||||
|
||||
taskList.value = res.data?.items || []
|
||||
taskListTotal.value = res.data?.total || 0
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '获取批量订购任务列表失败')
|
||||
} finally {
|
||||
taskListLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTaskListStatusChange = () => {
|
||||
taskListPage.value = 1
|
||||
void loadTaskList()
|
||||
}
|
||||
|
||||
const handleTaskListSizeChange = () => {
|
||||
taskListPage.value = 1
|
||||
void loadTaskList()
|
||||
}
|
||||
|
||||
const showTask = (task: BulkPurchaseTask) => {
|
||||
const taskId = task.task_id ?? task.id
|
||||
if (!taskId) return
|
||||
|
||||
void router.push(`${RoutesAlias.BulkPurchaseDetail}/${taskId}`)
|
||||
}
|
||||
|
||||
const getStatusName = (status: BulkPurchaseTaskStatus) => {
|
||||
const names: Record<BulkPurchaseTaskStatus, string> = {
|
||||
[BulkPurchaseTaskStatus.PENDING]: '待处理',
|
||||
[BulkPurchaseTaskStatus.PROCESSING]: '处理中',
|
||||
[BulkPurchaseTaskStatus.COMPLETED]: '已完成',
|
||||
[BulkPurchaseTaskStatus.FAILED]: '已失败',
|
||||
[BulkPurchaseTaskStatus.CANCELED]: '已取消'
|
||||
}
|
||||
return names[status] || '-'
|
||||
}
|
||||
|
||||
const getStatusType = (status: BulkPurchaseTaskStatus) => {
|
||||
if (status === BulkPurchaseTaskStatus.COMPLETED) return 'success'
|
||||
if (status === BulkPurchaseTaskStatus.FAILED) return 'danger'
|
||||
if (status === BulkPurchaseTaskStatus.PROCESSING) return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
const getPaymentMethodName = (method?: BulkPurchasePaymentMethod) => {
|
||||
return method === 'offline' ? '线下支付' : method === 'wallet' ? '代理钱包' : '-'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadTaskList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bulk-purchase-page {
|
||||
.task-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-list-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.package-option {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.create-form {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.task-list-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.task-list-toolbar {
|
||||
width: 100%;
|
||||
|
||||
.el-select {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,506 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="device-batch-allocation-page" id="table-full-screen">
|
||||
<ElDrawer
|
||||
v-model="createDrawerVisible"
|
||||
title="设备批量分配"
|
||||
direction="rtl"
|
||||
size="520px"
|
||||
:close-on-click-modal="!createDrawerBusy"
|
||||
:close-on-press-escape="!createDrawerBusy"
|
||||
@closed="handleCreateDrawerClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" label-width="110px" class="create-form">
|
||||
<ElFormItem label="操作类型" prop="operation_type">
|
||||
<ElSelect
|
||||
v-model="form.operation_type"
|
||||
style="width: 100%"
|
||||
@change="handleOperationTypeChange"
|
||||
>
|
||||
<ElOption label="分配目标代理" value="assign_shop" />
|
||||
<ElOption label="设置套餐系列" value="assign_series" />
|
||||
<ElOption label="批量回收设备" value="recall" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="requiresTargetId" label="目标 ID" prop="target_id">
|
||||
<ElSelect
|
||||
v-model="form.target_id"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:remote-method="handleTargetSearch"
|
||||
:loading="targetLoading"
|
||||
:placeholder="targetPlaceholder"
|
||||
style="width: 100%"
|
||||
@visible-change="handleTargetVisibleChange"
|
||||
>
|
||||
<template v-if="form.operation_type === 'assign_shop'">
|
||||
<ElOption
|
||||
v-for="shop in shopOptions"
|
||||
:key="shop.id"
|
||||
:label="shop.shop_name"
|
||||
:value="shop.id"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ElOption
|
||||
v-for="series in seriesOptions"
|
||||
:key="series.id"
|
||||
:label="series.series_name"
|
||||
:value="series.id"
|
||||
/>
|
||||
</template>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-else label="回收目标">
|
||||
<ElTag type="warning">回收到平台库存</ElTag>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="设备文件" prop="file_key">
|
||||
<VoucherUpload
|
||||
ref="uploadRef"
|
||||
v-model="fileKeys"
|
||||
voucher-name="设备标识文件"
|
||||
:max-count="1"
|
||||
purpose="device_batch_allocation"
|
||||
:max-size-mb="10"
|
||||
single-column-csv
|
||||
:max-csv-rows="1000"
|
||||
accept=".csv"
|
||||
tip="仅支持 UTF-8 单列 CSV,最多 1000 行,最大 10MB"
|
||||
@uploading-change="fileUploading = $event"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElAlert
|
||||
v-if="createDrawerBusy"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="fileUploading ? '设备文件上传中,请稍候' : '任务创建中,请勿重复提交'"
|
||||
/>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<ElButton :disabled="createDrawerBusy" @click="createDrawerVisible = false">
|
||||
取消
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
:disabled="!hasAuth(JULY_PERMISSIONS.deviceAllocation.create)"
|
||||
@click="submitTask"
|
||||
>
|
||||
创建分配任务
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card task-list-card">
|
||||
<template #header>
|
||||
<div class="task-list-header">
|
||||
<span>设备批量分配任务</span>
|
||||
<div class="task-list-toolbar">
|
||||
<ElButton
|
||||
v-if="hasAuth(JULY_PERMISSIONS.deviceAllocation.create)"
|
||||
tag="a"
|
||||
href="/templates/device-batch-allocation-template.csv"
|
||||
download="设备批量分配模板.csv"
|
||||
>
|
||||
下载模板
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="hasAuth(JULY_PERMISSIONS.deviceAllocation.create)"
|
||||
type="primary"
|
||||
@click="openCreateDrawer"
|
||||
>
|
||||
设备批量分配
|
||||
</ElButton>
|
||||
<ElSelect
|
||||
v-model="taskListOperationType"
|
||||
clearable
|
||||
placeholder="筛选操作类型"
|
||||
style="width: 170px"
|
||||
@change="handleTaskListFilterChange"
|
||||
>
|
||||
<ElOption label="分配目标代理" value="assign_shop" />
|
||||
<ElOption label="设置套餐系列" value="assign_series" />
|
||||
<ElOption label="批量回收设备" value="recall" />
|
||||
</ElSelect>
|
||||
<ElSelect
|
||||
v-model="taskListStatus"
|
||||
clearable
|
||||
placeholder="筛选任务状态"
|
||||
style="width: 150px"
|
||||
@change="handleTaskListFilterChange"
|
||||
>
|
||||
<ElOption label="待处理" :value="DeviceImportTaskStatus.PENDING" />
|
||||
<ElOption label="处理中" :value="DeviceImportTaskStatus.PROCESSING" />
|
||||
<ElOption label="已完成" :value="DeviceImportTaskStatus.COMPLETED" />
|
||||
<ElOption label="失败" :value="DeviceImportTaskStatus.FAILED" />
|
||||
</ElSelect>
|
||||
<ElButton :loading="taskListLoading" @click="loadTaskList">刷新</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElTable v-loading="taskListLoading" :data="taskList" border>
|
||||
<ElTableColumn label="任务号" min-width="210" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<ElButton
|
||||
v-if="hasAuth(JULY_PERMISSIONS.deviceAllocation.detail)"
|
||||
type="primary"
|
||||
link
|
||||
@click="showTask(scope.row)"
|
||||
>
|
||||
{{ scope.row.task_no || '-' }}
|
||||
</ElButton>
|
||||
<span v-else>{{ scope.row.task_no || '-' }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作类型" width="150">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getOperationTagType(scope.row.operation_type)">
|
||||
{{ getOperationLabel(scope.row.operation_type) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="operation_name"
|
||||
label="操作名称"
|
||||
min-width="170"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ scope.row.operation_name || getOperationLabel(scope.row.operation_type) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="目标 ID" width="110">
|
||||
<template #default="scope">{{ scope.row.target_id ?? '-' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="110">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getStatusTagType(scope.row.status)">
|
||||
{{ scope.row.status_name || scope.row.status_text || '-' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="total_count" label="总数" width="90" />
|
||||
<ElTableColumn prop="success_count" label="成功数" width="90" />
|
||||
<ElTableColumn prop="fail_count" label="失败数" width="90" />
|
||||
<ElTableColumn prop="skip_count" label="跳过数" width="90" />
|
||||
<ElTableColumn label="创建时间" width="180">
|
||||
<template #default="scope">{{ formatDateTime(scope.row.created_at) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="完成时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.completed_at ? formatDateTime(scope.row.completed_at) : '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<ElPagination
|
||||
v-model:current-page="taskListPage"
|
||||
v-model:page-size="taskListSize"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="taskListTotal"
|
||||
@current-change="loadTaskList"
|
||||
@size-change="handleTaskListSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElDrawer,
|
||||
ElForm,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElPagination,
|
||||
ElSelect,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag
|
||||
} from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
import { DeviceService, PackageSeriesService, ShopService } from '@/api/modules'
|
||||
import { DeviceImportTaskStatus } from '@/types/api/device'
|
||||
import type {
|
||||
DeviceBatchAllocationRequest,
|
||||
DeviceImportTask,
|
||||
DeviceImportTaskOperationType
|
||||
} from '@/types/api/device'
|
||||
import type { PackageSeriesResponse, ShopResponse } from '@/types/api'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
|
||||
defineOptions({ name: 'DeviceBatchAllocation' })
|
||||
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const createDrawerVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const fileUploading = ref(false)
|
||||
const fileKeys = ref<string[]>([])
|
||||
const taskList = ref<DeviceImportTask[]>([])
|
||||
const taskListLoading = ref(false)
|
||||
const taskListPage = ref(1)
|
||||
const taskListSize = ref(20)
|
||||
const taskListTotal = ref(0)
|
||||
const taskListStatus = ref<DeviceImportTaskStatus | undefined>()
|
||||
const taskListOperationType = ref<Exclude<DeviceImportTaskOperationType, 'import'> | undefined>()
|
||||
const shopOptions = ref<ShopResponse[]>([])
|
||||
const seriesOptions = ref<PackageSeriesResponse[]>([])
|
||||
const targetLoading = ref(false)
|
||||
const form = reactive<{
|
||||
operation_type: Exclude<DeviceImportTaskOperationType, 'import'>
|
||||
target_id?: number
|
||||
}>({
|
||||
operation_type: 'assign_shop',
|
||||
target_id: undefined
|
||||
})
|
||||
|
||||
const requiresTargetId = computed(() =>
|
||||
['assign_shop', 'assign_series'].includes(form.operation_type)
|
||||
)
|
||||
const targetPlaceholder = computed(() =>
|
||||
form.operation_type === 'assign_shop' ? '请选择启用的目标店铺' : '请选择启用的套餐系列'
|
||||
)
|
||||
const createDrawerBusy = computed(() => submitting.value || fileUploading.value)
|
||||
|
||||
const getOperationLabel = (operationType: DeviceImportTaskOperationType) => {
|
||||
switch (operationType) {
|
||||
case 'assign_shop':
|
||||
return '分配目标代理'
|
||||
case 'assign_series':
|
||||
return '设置套餐系列'
|
||||
case 'recall':
|
||||
return '批量回收设备'
|
||||
default:
|
||||
return '设备导入'
|
||||
}
|
||||
}
|
||||
|
||||
const getOperationTagType = (operationType: DeviceImportTaskOperationType) =>
|
||||
operationType === 'recall' ? 'warning' : 'primary'
|
||||
|
||||
const getStatusTagType = (status: DeviceImportTaskStatus) => {
|
||||
switch (status) {
|
||||
case DeviceImportTaskStatus.PENDING:
|
||||
return 'info'
|
||||
case DeviceImportTaskStatus.PROCESSING:
|
||||
return 'warning'
|
||||
case DeviceImportTaskStatus.COMPLETED:
|
||||
return 'success'
|
||||
case DeviceImportTaskStatus.FAILED:
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
const openCreateDrawer = () => {
|
||||
if (!hasAuth(JULY_PERMISSIONS.deviceAllocation.create)) return
|
||||
createDrawerVisible.value = true
|
||||
}
|
||||
|
||||
const loadTargetOptions = async (query = '') => {
|
||||
if (form.operation_type === 'recall') return
|
||||
targetLoading.value = true
|
||||
try {
|
||||
if (form.operation_type === 'assign_shop') {
|
||||
const res = await ShopService.getShops({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
status: 1,
|
||||
shop_name: query || undefined
|
||||
})
|
||||
if (res.code === 0) shopOptions.value = res.data.items || []
|
||||
} else {
|
||||
const res = await PackageSeriesService.getPackageSeries({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
status: 1,
|
||||
series_name: query || undefined
|
||||
})
|
||||
if (res.code === 0) seriesOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取批量分配目标失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message)
|
||||
} finally {
|
||||
targetLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTargetSearch = (query: string) => {
|
||||
void loadTargetOptions(query)
|
||||
}
|
||||
|
||||
const handleTargetVisibleChange = (visible: boolean) => {
|
||||
if (
|
||||
visible &&
|
||||
(form.operation_type === 'assign_shop'
|
||||
? !shopOptions.value.length
|
||||
: !seriesOptions.value.length)
|
||||
) {
|
||||
void loadTargetOptions()
|
||||
}
|
||||
}
|
||||
|
||||
const handleOperationTypeChange = () => {
|
||||
form.target_id = undefined
|
||||
shopOptions.value = []
|
||||
seriesOptions.value = []
|
||||
if (form.operation_type !== 'recall') void loadTargetOptions()
|
||||
}
|
||||
|
||||
const handleCreateDrawerClosed = () => {
|
||||
formRef.value?.resetFields()
|
||||
form.operation_type = 'assign_shop'
|
||||
form.target_id = undefined
|
||||
shopOptions.value = []
|
||||
seriesOptions.value = []
|
||||
fileKeys.value = []
|
||||
fileUploading.value = false
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
const handleTaskListFilterChange = () => {
|
||||
taskListPage.value = 1
|
||||
void loadTaskList()
|
||||
}
|
||||
|
||||
const loadTaskList = async () => {
|
||||
taskListLoading.value = true
|
||||
try {
|
||||
const res = await DeviceService.getImportTasks({
|
||||
page: taskListPage.value,
|
||||
page_size: taskListSize.value,
|
||||
status: taskListStatus.value,
|
||||
operation_type: taskListOperationType.value
|
||||
})
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(res.msg || '获取设备批量分配任务失败')
|
||||
return
|
||||
}
|
||||
taskList.value = (res.data.items || []).filter((item) => item.operation_type !== 'import')
|
||||
taskListTotal.value = res.data.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取设备批量分配任务失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message)
|
||||
} finally {
|
||||
taskListLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTaskListSizeChange = () => {
|
||||
taskListPage.value = 1
|
||||
void loadTaskList()
|
||||
}
|
||||
|
||||
const showTask = (row: DeviceImportTask) => {
|
||||
router.push({
|
||||
path: RoutesAlias.TaskDetail,
|
||||
query: { id: row.id, task_type: 'device' }
|
||||
})
|
||||
}
|
||||
|
||||
const submitTask = async () => {
|
||||
if (createDrawerBusy.value) return
|
||||
if (!fileKeys.value[0]) {
|
||||
ElMessage.warning('请先上传设备标识文件')
|
||||
return
|
||||
}
|
||||
if (requiresTargetId.value && !form.target_id) {
|
||||
ElMessage.warning('请选择目标店铺或套餐系列')
|
||||
return
|
||||
}
|
||||
|
||||
const request: DeviceBatchAllocationRequest = {
|
||||
file_key: fileKeys.value[0],
|
||||
operation_type: form.operation_type
|
||||
}
|
||||
if (requiresTargetId.value) request.target_id = form.target_id
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await DeviceService.createAllocationTask(request)
|
||||
if (res.code !== 0) {
|
||||
ElMessage.error(res.msg || '创建设备批量分配任务失败')
|
||||
return
|
||||
}
|
||||
createDrawerVisible.value = false
|
||||
await loadTaskList()
|
||||
ElMessage.success(`任务已创建:${res.data.task_no}`)
|
||||
} catch (error) {
|
||||
console.error('创建设备批量分配任务失败:', error)
|
||||
ElMessage.error(normalizeApiError(error).message)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadTaskList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.device-batch-allocation-page {
|
||||
.task-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.task-list-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.task-list-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.task-list-toolbar {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,13 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div v-if="isPlatformAccount" class="device-task-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
/>
|
||||
|
||||
<div v-if="pollingError || pollingForbidden" class="task-polling-error">
|
||||
<ElAlert
|
||||
@@ -21,26 +20,18 @@
|
||||
</div>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton
|
||||
v-if="isPlatformAccount && hasAuth(JULY_PERMISSIONS.deviceAllocation.page)"
|
||||
type="primary"
|
||||
:icon="Upload"
|
||||
@click="importDialogVisible = true"
|
||||
v-permission="JULY_PERMISSIONS.deviceAllocation.page"
|
||||
>
|
||||
<ElButton type="primary" :icon="Upload" @click="importDialogVisible = true">
|
||||
批量导入设备
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
@@ -62,21 +53,20 @@
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<!-- 导入对话框 -->
|
||||
<ElDialog v-model="importDialogVisible" title="批量导入设备" width="40%" align-center>
|
||||
<ElAlert type="info" :closable="false" style="margin-bottom: 20px">
|
||||
<template #title>
|
||||
<div style="line-height: 1.8">
|
||||
<p><strong>导入说明:</strong></p>
|
||||
<p>1. 设备分配使用单列 UTF-8 CSV,导入设备仍使用 Excel 模板</p>
|
||||
<p>2. CSV 单次最多 1000 条,文件不超过 10MB</p>
|
||||
<p>3. 列格式请设置为文本格式,避免长数字被转为科学计数法</p>
|
||||
<p>4. <strong>重要:列顺序固定,不可调整。</strong>系统按位置读取,不识别列名</p>
|
||||
<p style="color: var(--el-color-primary)">5. 必填列:虚拟号(第1列)</p>
|
||||
<p>1. 设备导入使用 Excel 模板,文件大小不能超过 300MB。</p>
|
||||
<p>2. 列格式请设置为文本格式,避免长数字被转为科学计数法。</p>
|
||||
<p>3. <strong>列顺序固定,不可调整。</strong>系统按位置读取,不识别列名。</p>
|
||||
<p style="color: var(--el-color-primary)">
|
||||
4. 必填列:设备标识(第 1 列,支持 VirtualNo、IMEI 或 SN)。
|
||||
</p>
|
||||
<p>
|
||||
6.
|
||||
可选列:SN、设备名称、设备型号、设备类型、IMEI、制造商、最大SIM槽数(默认4,有效范围1-4)、卡1~卡4
|
||||
ICCID
|
||||
5. 可选列:SN、设备名称、设备型号、设备类型、IMEI、制造商、最大 SIM 槽数、卡 1~卡 4
|
||||
ICCID。
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -88,23 +78,7 @@
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="任务类型">
|
||||
<ElSelect v-model="importForm.operation_type" style="width: 100%">
|
||||
<ElOption label="导入设备" value="import" />
|
||||
<ElOption label="分配目标代理" value="assign_shop" />
|
||||
<ElOption label="设置套餐系列" value="assign_series" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol v-if="importForm.operation_type !== 'import'" :span="12">
|
||||
<ElFormItem label="目标 ID">
|
||||
<ElInput v-model="importForm.target_id" placeholder="代理店铺或套餐系列 ID" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="批次号" prop="batch_no">
|
||||
<ElInput
|
||||
@@ -135,18 +109,18 @@
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElUpload
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:limit="1"
|
||||
accept=".xlsx,.csv"
|
||||
>
|
||||
accept=".xlsx"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击选择</em></div>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击选择</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">导入设备支持 .xlsx;批量分配支持单列 .csv</div>
|
||||
<div class="el-upload__tip">设备导入仅支持 .xlsx 文件</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
|
||||
@@ -166,12 +140,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { h, onMounted, reactive, ref, watch, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { DeviceService } from '@/api/modules'
|
||||
import { ElMessage, ElTag } from 'element-plus'
|
||||
import { Download, UploadFilled, Upload } from '@element-plus/icons-vue'
|
||||
import { Download, Upload, UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { UploadInstance } from 'element-plus'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
@@ -189,7 +162,6 @@
|
||||
} from '@/types/api/device'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { generatePackageCode } from '@/utils/codeGenerator'
|
||||
import { JULY_PERMISSIONS } from '@/config/constants'
|
||||
import { normalizeApiError } from '@/utils/business/apiError'
|
||||
|
||||
defineOptions({ name: 'DeviceTask' })
|
||||
@@ -208,40 +180,23 @@
|
||||
const importDialogVisible = ref(false)
|
||||
const importForm = reactive({
|
||||
batch_no: '',
|
||||
realname_policy: '' as '' | 'none' | 'before_order' | 'after_order',
|
||||
operation_type: 'import' as 'import' | 'assign_shop' | 'assign_series',
|
||||
target_id: ''
|
||||
realname_policy: '' as '' | RealnamePolicy
|
||||
})
|
||||
|
||||
// 搜索表单初始值
|
||||
const initialSearchState = {
|
||||
status: undefined,
|
||||
status: undefined as DeviceImportTaskStatus | undefined,
|
||||
batch_no: '',
|
||||
dateRange: undefined as string[] | undefined,
|
||||
start_time: '',
|
||||
end_time: ''
|
||||
dateRange: undefined as string[] | undefined
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({ ...initialSearchState })
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 搜索表单配置
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '任务状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '全部'
|
||||
},
|
||||
config: { clearable: true, placeholder: '全部' },
|
||||
options: () => [
|
||||
{ label: '待处理', value: 1 },
|
||||
{ label: '处理中', value: 2 },
|
||||
@@ -253,10 +208,7 @@
|
||||
label: '批次号',
|
||||
prop: 'batch_no',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true,
|
||||
placeholder: '请输入批次号'
|
||||
}
|
||||
config: { clearable: true, placeholder: '请输入批次号' }
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
@@ -271,7 +223,6 @@
|
||||
}
|
||||
]
|
||||
|
||||
// 列配置
|
||||
const columnOptions = [
|
||||
{ label: '任务编号', prop: 'task_no' },
|
||||
{ label: '任务状态', prop: 'status' },
|
||||
@@ -288,7 +239,6 @@
|
||||
]
|
||||
|
||||
const taskList = ref<DeviceImportTask[]>([])
|
||||
|
||||
const polling = useAsyncTaskPolling<DeviceImportTaskDetail>({
|
||||
storageKey: 'device-import-active-task',
|
||||
autoRestore: false,
|
||||
@@ -306,7 +256,6 @@
|
||||
const pollingError = polling.error
|
||||
const pollingForbidden = polling.forbidden
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status: DeviceImportTaskStatus) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
@@ -322,19 +271,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetail = (row: DeviceImportTask) => {
|
||||
if (!isPlatformAccount.value) return
|
||||
router.push({
|
||||
path: RoutesAlias.TaskDetail,
|
||||
query: {
|
||||
id: row.id,
|
||||
task_type: 'device'
|
||||
}
|
||||
query: { id: row.id, task_type: 'device' }
|
||||
})
|
||||
}
|
||||
|
||||
// 处理名称点击
|
||||
const handleNameClick = (row: DeviceImportTask) => {
|
||||
if (isPlatformAccount.value && hasAuth('device_task:view_detail')) {
|
||||
viewDetail(row)
|
||||
@@ -343,15 +287,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'task_no',
|
||||
label: '任务编号',
|
||||
width: 220,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: DeviceImportTask) => {
|
||||
return h(
|
||||
formatter: (row: DeviceImportTask) =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
@@ -362,48 +305,31 @@
|
||||
},
|
||||
row.task_no
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '任务状态',
|
||||
width: 100,
|
||||
formatter: (row: DeviceImportTask) => {
|
||||
return h(ElTag, { type: getStatusType(row.status) }, () => row.status_text)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'total_count',
|
||||
label: '总数',
|
||||
width: 80
|
||||
formatter: (row: DeviceImportTask) =>
|
||||
h(ElTag, { type: getStatusType(row.status) }, () => row.status_name || row.status_text)
|
||||
},
|
||||
{ prop: 'total_count', label: '总数', width: 80 },
|
||||
{
|
||||
prop: 'success_count',
|
||||
label: '成功数',
|
||||
width: 80,
|
||||
formatter: (row: DeviceImportTask) => {
|
||||
return h('span', { style: { color: 'var(--el-color-success)' } }, row.success_count)
|
||||
}
|
||||
formatter: (row: DeviceImportTask) =>
|
||||
h('span', { style: { color: 'var(--el-color-success)' } }, row.success_count)
|
||||
},
|
||||
{
|
||||
prop: 'fail_count',
|
||||
label: '失败数',
|
||||
width: 80,
|
||||
formatter: (row: DeviceImportTask) => {
|
||||
return h('span', { style: { color: 'var(--el-color-danger)' } }, row.fail_count)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'skip_count',
|
||||
label: '跳过数',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
prop: 'batch_no',
|
||||
label: '批次号',
|
||||
width: 180,
|
||||
showOverflowTooltip: true
|
||||
formatter: (row: DeviceImportTask) =>
|
||||
h('span', { style: { color: 'var(--el-color-danger)' } }, row.fail_count)
|
||||
},
|
||||
{ prop: 'skip_count', label: '跳过数', width: 80 },
|
||||
{ prop: 'batch_no', label: '批次号', width: 180, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'started_at',
|
||||
label: '开始时间',
|
||||
@@ -424,12 +350,7 @@
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: DeviceImportTask) => row.error_message || '-'
|
||||
},
|
||||
{
|
||||
prop: 'file_name',
|
||||
label: '文件名',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{ prop: 'file_name', label: '文件名', minWidth: 180, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'creator_name',
|
||||
label: '操作人',
|
||||
@@ -460,7 +381,6 @@
|
||||
}
|
||||
})
|
||||
|
||||
// 获取设备任务列表
|
||||
const getTableData = async () => {
|
||||
if (!isPlatformAccount.value) {
|
||||
taskList.value = []
|
||||
@@ -473,10 +393,9 @@
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
status: searchForm.status,
|
||||
operation_type: 'import',
|
||||
batch_no: searchForm.batch_no || undefined
|
||||
}
|
||||
|
||||
// 处理时间范围
|
||||
if (searchForm.dateRange && Array.isArray(searchForm.dateRange)) {
|
||||
params.start_time = searchForm.dateRange[0]
|
||||
params.end_time = searchForm.dateRange[1]
|
||||
@@ -484,9 +403,7 @@
|
||||
|
||||
const res = await DeviceService.getImportTasks(params)
|
||||
if (res.code === 0) {
|
||||
const taskItems = (res.data as typeof res.data & { items?: DeviceImportTask[] | null })
|
||||
.items
|
||||
taskList.value = taskItems || []
|
||||
taskList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -497,37 +414,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState })
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
void getTableData()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
void getTableData()
|
||||
}
|
||||
|
||||
// 刷新表格
|
||||
const handleRefresh = () => {
|
||||
getTableData()
|
||||
}
|
||||
const handleRefresh = () => void getTableData()
|
||||
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getTableData()
|
||||
void getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.page = newCurrentPage
|
||||
getTableData()
|
||||
void getTableData()
|
||||
}
|
||||
|
||||
// 下载模板
|
||||
const downloadTemplate = async () => {
|
||||
const downloadTemplate = () => {
|
||||
try {
|
||||
const link = document.createElement('a')
|
||||
link.href = new URL('@/template/设备导入模板.xlsx', import.meta.url).href
|
||||
@@ -542,107 +452,75 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 文件选择变化
|
||||
const handleFileChange = async (uploadFile: any) => {
|
||||
const isCsvAllocation = importForm.operation_type !== 'import'
|
||||
const maxSize = (isCsvAllocation ? 10 : 300) * 1024 * 1024
|
||||
if (uploadFile.raw && uploadFile.raw.size > maxSize) {
|
||||
const handleFileChange = (uploadFile: any) => {
|
||||
const file = uploadFile.raw as File | undefined
|
||||
if (!file) return
|
||||
if (file.size > 300 * 1024 * 1024) {
|
||||
ElMessage.error('文件大小不能超过 300MB')
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (uploadFile.raw && (isCsvAllocation ? !uploadFile.raw.name.endsWith('.csv') : !uploadFile.raw.name.endsWith('.xlsx'))) {
|
||||
ElMessage.error(isCsvAllocation ? '批量分配只能上传 .csv 文件' : '设备导入只能上传 .xlsx 文件')
|
||||
if (!file.name.toLowerCase().endsWith('.xlsx')) {
|
||||
ElMessage.error('设备导入只能上传 .xlsx 文件')
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (isCsvAllocation && uploadFile.raw) {
|
||||
const rows = (await uploadFile.raw.text()).replace(/^\uFEFF/, '').split(/\r?\n/).filter(Boolean)
|
||||
if (rows.length > 1001 || rows.some((row: string) => row.includes(','))) {
|
||||
ElMessage.error('设备分配 CSV 必须是单列且最多 1000 行数据')
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fileList.value = uploadFile.raw ? [uploadFile.raw] : []
|
||||
fileList.value = [file]
|
||||
}
|
||||
|
||||
// 清空文件
|
||||
const clearFiles = () => {
|
||||
uploadRef.value?.clearFiles()
|
||||
fileList.value = []
|
||||
}
|
||||
|
||||
// 生成批次号
|
||||
const handleGenerateBatchNo = () => {
|
||||
const code = generatePackageCode().replace('PKG', 'DEV')
|
||||
importForm.batch_no = code
|
||||
importForm.batch_no = generatePackageCode().replace('PKG', 'DEV')
|
||||
ElMessage.success('批次号生成成功')
|
||||
}
|
||||
|
||||
// 取消导入
|
||||
const handleCancelImport = () => {
|
||||
clearFiles()
|
||||
importForm.batch_no = ''
|
||||
importForm.realname_policy = ''
|
||||
importForm.operation_type = 'import'
|
||||
importForm.target_id = ''
|
||||
importDialogVisible.value = false
|
||||
}
|
||||
// 提交上传
|
||||
|
||||
const submitUpload = async () => {
|
||||
if (!isPlatformAccount.value || !hasAuth(JULY_PERMISSIONS.deviceAllocation.page)) return
|
||||
if (!fileList.value.length) {
|
||||
ElMessage.warning('请先选择文件')
|
||||
return
|
||||
}
|
||||
if (importForm.operation_type !== 'import' && !Number(importForm.target_id)) {
|
||||
ElMessage.warning('请输入有效的目标 ID')
|
||||
return
|
||||
}
|
||||
if (!isPlatformAccount.value || !fileList.value.length) {
|
||||
ElMessage.warning('请先选择文件')
|
||||
return
|
||||
}
|
||||
|
||||
const file = fileList.value[0]
|
||||
uploading.value = true
|
||||
|
||||
try {
|
||||
ElMessage.info('正在准备上传...')
|
||||
const isAllocation = importForm.operation_type !== 'import'
|
||||
const contentType = isAllocation ? 'text/csv' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: contentType,
|
||||
purpose: isAllocation ? 'device_batch_allocation' : 'iot_import'
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
purpose: 'iot_import'
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
return
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
|
||||
ElMessage.info('正在上传文件...')
|
||||
await StorageService.uploadFile(upload_url, file, contentType)
|
||||
|
||||
ElMessage.info(isAllocation ? '正在创建分配任务...' : '正在创建导入任务...')
|
||||
const importRes = isAllocation
|
||||
? await DeviceService.createAllocationTask({
|
||||
file_key,
|
||||
operation_type: importForm.operation_type as 'assign_shop' | 'assign_series',
|
||||
target_id: Number(importForm.target_id)
|
||||
})
|
||||
: await DeviceService.importDevices({
|
||||
file_key,
|
||||
batch_no: importForm.batch_no || undefined,
|
||||
realname_policy: (importForm.realname_policy || undefined) as RealnamePolicy | undefined
|
||||
})
|
||||
await StorageService.uploadFile(
|
||||
upload_url,
|
||||
file,
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
)
|
||||
|
||||
ElMessage.info('正在创建设备导入任务...')
|
||||
const importRes = await DeviceService.importDevices({
|
||||
file_key,
|
||||
batch_no: importForm.batch_no || undefined,
|
||||
realname_policy: (importForm.realname_policy || undefined) as RealnamePolicy | undefined
|
||||
})
|
||||
if (importRes.code !== 0) {
|
||||
ElMessage.error(importRes.msg || '创建导入任务失败')
|
||||
return
|
||||
@@ -650,12 +528,10 @@
|
||||
|
||||
const taskNo = importRes.data.task_no
|
||||
const taskId = importRes.data.task_id
|
||||
|
||||
handleCancelImport()
|
||||
await router.replace({ path: route.path, query: { task_id: String(taskId) } })
|
||||
await polling.start(taskId)
|
||||
await getTableData()
|
||||
|
||||
ElMessage.success({
|
||||
message: `导入任务已创建!任务编号:${taskNo}`,
|
||||
duration: 3000,
|
||||
@@ -669,62 +545,51 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 从行数据下载失败数据
|
||||
const downloadFailDataByRow = async (row: DeviceImportTask) => {
|
||||
if (!isPlatformAccount.value) return
|
||||
try {
|
||||
const res = await DeviceService.getImportTaskDetail(row.id)
|
||||
if (res.code === 0 && res.data) {
|
||||
const detail = res.data
|
||||
const failReasons =
|
||||
detail.failed_items?.map((item: any) => ({
|
||||
line: item.line || '-',
|
||||
deviceNo: item.virtual_no || '-',
|
||||
message: item.reason || '未知错误'
|
||||
})) || []
|
||||
|
||||
if (failReasons.length === 0) {
|
||||
ElMessage.warning('没有失败数据可下载')
|
||||
return
|
||||
}
|
||||
|
||||
const headers = ['行号', '设备编号', '失败原因']
|
||||
const csvRows = [
|
||||
headers.join(','),
|
||||
...failReasons.map((item: any) =>
|
||||
[item.line, `\t${item.deviceNo}`, `"${item.message}"`].join(',')
|
||||
)
|
||||
]
|
||||
const csvContent = csvRows.join('\n')
|
||||
|
||||
const BOM = '\uFEFF'
|
||||
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' })
|
||||
|
||||
const link = document.createElement('a')
|
||||
const url = URL.createObjectURL(blob)
|
||||
link.setAttribute('href', url)
|
||||
link.setAttribute('download', `导入失败数据_${row.batch_no}.csv`)
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('失败数据下载成功')
|
||||
if (res.code !== 0 || !res.data) return
|
||||
const failReasons =
|
||||
res.data.failed_items?.map((item) => ({
|
||||
line: item.line || '-',
|
||||
deviceNo: item.device_identifier || item.virtual_no || '-',
|
||||
message: item.reason || '未知错误'
|
||||
})) || []
|
||||
if (failReasons.length === 0) {
|
||||
ElMessage.warning('没有失败数据可下载')
|
||||
return
|
||||
}
|
||||
|
||||
const csvRows = [
|
||||
['行号', '设备编号', '失败原因'].join(','),
|
||||
...failReasons.map((item) =>
|
||||
[item.line, `\t${item.deviceNo}`, `"${item.message.replaceAll('"', '""')}"`].join(',')
|
||||
)
|
||||
]
|
||||
const blob = new Blob(['\uFEFF' + csvRows.join('\n')], {
|
||||
type: 'text/csv;charset=utf-8;'
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `导入失败数据_${row.batch_no}.csv`
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
ElMessage.success('失败数据下载成功')
|
||||
} catch (error) {
|
||||
console.error('下载失败数据失败:', error)
|
||||
ElMessage.error('下载失败数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
const downloadTaskFile = async (row: DeviceImportTask) => {
|
||||
if (!isPlatformAccount.value) return
|
||||
const fileKey = row.file_name?.trim()
|
||||
const fileKey = row.file_key || row.file_name?.trim()
|
||||
if (!fileKey) {
|
||||
ElMessage.warning('当前任务没有可下载的原始文件')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await StorageService.downloadFileByKey(fileKey)
|
||||
ElMessage.success('原始文件下载已开始')
|
||||
@@ -734,20 +599,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 获取操作按钮
|
||||
const getActions = (row: DeviceImportTask) => {
|
||||
if (!isPlatformAccount.value) return []
|
||||
const actions: any[] = []
|
||||
const showDownloadFileAction = false
|
||||
|
||||
if (showDownloadFileAction && row.file_name?.trim() && hasAuth('device_task:download_file')) {
|
||||
actions.push({
|
||||
label: '下载文件',
|
||||
handler: () => downloadTaskFile(row),
|
||||
type: 'primary'
|
||||
})
|
||||
if (row.file_key && hasAuth('device_task:download_file')) {
|
||||
actions.push({ label: '下载文件', handler: () => downloadTaskFile(row), type: 'primary' })
|
||||
}
|
||||
|
||||
if (row.fail_count > 0 && hasAuth('device_task:download_fail_data')) {
|
||||
actions.push({
|
||||
label: '失败数据',
|
||||
@@ -755,7 +612,6 @@
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<ArtTableFullScreen>
|
||||
<div class="task-detail-page" id="table-full-screen">
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 页面头部 -->
|
||||
<div class="detail-header">
|
||||
<ElButton @click="goBack">
|
||||
<template #icon>
|
||||
@@ -10,35 +9,59 @@
|
||||
</template>
|
||||
返回
|
||||
</ElButton>
|
||||
<h2 class="detail-title">任务详情</h2>
|
||||
<h2 class="detail-title">{{ taskTitle }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- 使用 DetailPage 组件显示任务信息 -->
|
||||
<DetailPage v-if="taskDetail" :sections="detailSections" :data="taskDetail" />
|
||||
|
||||
<!-- 失败记录 -->
|
||||
<div class="failure-section" v-if="taskDetail?.fail_count && taskDetail.fail_count > 0">
|
||||
<div v-if="taskDetail?.fail_count && taskDetail.fail_count > 0" class="result-section">
|
||||
<ElDivider content-position="left">
|
||||
<span class="section-title">失败记录 ({{ taskDetail.fail_count }})</span>
|
||||
<span class="section-title"
|
||||
>{{ resultAction }}失败记录 ({{ taskDetail.fail_count }})</span
|
||||
>
|
||||
</ElDivider>
|
||||
<ElTable :data="taskDetail.failed_items || []" border style="width: 100%">
|
||||
<ElTableColumn prop="line" label="行号" width="100" />
|
||||
<ElTableColumn v-if="taskType === 'card'" prop="iccid" label="ICCID" min-width="180" />
|
||||
<ElTableColumn v-else prop="virtual_no" label="设备号" min-width="180" />
|
||||
<ElTableColumn prop="reason" label="失败原因" min-width="300" />
|
||||
<ElTableColumn v-else label="设备标识" min-width="180" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ scope.row.device_identifier || scope.row.virtual_no || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="reason" :label="`${resultAction}失败原因`" min-width="300" />
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<!-- 跳过记录 -->
|
||||
<div class="skipped-section" v-if="taskDetail?.skip_count && taskDetail.skip_count > 0">
|
||||
<div v-if="taskDetail?.skip_count && taskDetail.skip_count > 0" class="result-section">
|
||||
<ElDivider content-position="left">
|
||||
<span class="section-title">跳过记录 ({{ taskDetail.skip_count }})</span>
|
||||
<span class="section-title"
|
||||
>{{ resultAction }}跳过记录 ({{ taskDetail.skip_count }})</span
|
||||
>
|
||||
</ElDivider>
|
||||
<ElTable :data="taskDetail.skipped_items || []" border style="width: 100%">
|
||||
<ElTableColumn prop="line" label="行号" width="100" />
|
||||
<ElTableColumn v-if="taskType === 'card'" prop="iccid" label="ICCID" min-width="180" />
|
||||
<ElTableColumn v-else prop="virtual_no" label="设备号" min-width="180" />
|
||||
<ElTableColumn prop="reason" label="跳过原因" min-width="300" />
|
||||
<ElTableColumn v-else label="设备标识" min-width="180" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ scope.row.device_identifier || scope.row.virtual_no || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="reason" :label="`${resultAction}跳过原因`" min-width="300" />
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<div v-if="warningCount > 0" class="result-section">
|
||||
<ElDivider content-position="left">
|
||||
<span class="section-title">{{ resultAction }}警告记录 ({{ warningCount }})</span>
|
||||
</ElDivider>
|
||||
<ElTable :data="warningItems" border style="width: 100%">
|
||||
<ElTableColumn prop="line" label="行号" width="100" />
|
||||
<ElTableColumn label="设备标识" min-width="180" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ scope.row.device_identifier || scope.row.virtual_no || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="reason" label="警告原因" min-width="300" />
|
||||
</ElTable>
|
||||
</div>
|
||||
</ElCard>
|
||||
@@ -47,16 +70,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h } from 'vue'
|
||||
import { computed, h, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { CardService, DeviceService } from '@/api/modules'
|
||||
import { ElDivider, ElIcon, ElMessage, ElTable, ElTableColumn, ElTag } from 'element-plus'
|
||||
import type { TagProps } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import type { IotCardImportTaskDetail } from '@/types/api/card'
|
||||
import type { DeviceImportTaskDetail } from '@/types/api/device'
|
||||
import type { DeviceImportTaskDetail, DeviceImportTaskOperationType } from '@/types/api/device'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
@@ -72,12 +94,53 @@
|
||||
type TaskDetail = IotCardImportTaskDetail | DeviceImportTaskDetail
|
||||
|
||||
const taskDetail = ref<TaskDetail | null>(null)
|
||||
const loading = ref(false)
|
||||
const taskType = ref<TaskType>('card')
|
||||
const loading = ref(false)
|
||||
|
||||
const getOperationLabel = (operationType?: DeviceImportTaskOperationType) => {
|
||||
switch (operationType) {
|
||||
case 'assign_shop':
|
||||
return '分配目标代理'
|
||||
case 'assign_series':
|
||||
return '设置套餐系列'
|
||||
case 'recall':
|
||||
return '批量回收设备'
|
||||
case 'import':
|
||||
return '导入设备'
|
||||
default:
|
||||
return '设备任务'
|
||||
}
|
||||
}
|
||||
|
||||
const deviceTask = computed(() =>
|
||||
taskType.value === 'device' ? (taskDetail.value as DeviceImportTaskDetail | null) : null
|
||||
)
|
||||
const taskTitle = computed(() =>
|
||||
taskType.value === 'device'
|
||||
? `${getOperationLabel(deviceTask.value?.operation_type)}任务详情`
|
||||
: 'ICCID导入任务详情'
|
||||
)
|
||||
const resultAction = computed(() => {
|
||||
if (taskType.value !== 'device') return '导入'
|
||||
switch (deviceTask.value?.operation_type) {
|
||||
case 'assign_shop':
|
||||
return '分配'
|
||||
case 'assign_series':
|
||||
return '设置套餐系列'
|
||||
case 'recall':
|
||||
return '回收'
|
||||
default:
|
||||
return '导入'
|
||||
}
|
||||
})
|
||||
const warningCount = computed(() =>
|
||||
taskType.value === 'device' ? deviceTask.value?.warning_count || 0 : 0
|
||||
)
|
||||
const warningItems = computed(() =>
|
||||
taskType.value === 'device' ? deviceTask.value?.warning_items || [] : []
|
||||
)
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status?: number): TagProps['type'] => {
|
||||
if (!status) return 'info'
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'info'
|
||||
@@ -92,8 +155,54 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 详情页配置
|
||||
const detailSections = computed((): DetailSection[] => {
|
||||
const deviceFields: DetailSection['fields'] =
|
||||
taskType.value === 'device'
|
||||
? [
|
||||
{
|
||||
label: '操作名称',
|
||||
prop: 'operation_name',
|
||||
formatter: (value: string) =>
|
||||
value || getOperationLabel(deviceTask.value?.operation_type)
|
||||
},
|
||||
{
|
||||
label: '目标 ID',
|
||||
prop: 'target_id',
|
||||
formatter: (value: number | null) => String(value ?? '-')
|
||||
}
|
||||
]
|
||||
: []
|
||||
|
||||
const cardFields: DetailSection['fields'] =
|
||||
taskType.value === 'card'
|
||||
? [
|
||||
{
|
||||
label: '运营商',
|
||||
prop: 'carrier_name',
|
||||
formatter: (value: string) => value || '-'
|
||||
},
|
||||
{
|
||||
label: '卡业务类型',
|
||||
render: (data: any) => {
|
||||
if (!data.card_category) return h('span', '-')
|
||||
return h(
|
||||
ElTag,
|
||||
{
|
||||
type: data.card_category === 'industry' ? 'warning' : 'success',
|
||||
size: 'small'
|
||||
},
|
||||
() =>
|
||||
data.card_category === 'normal'
|
||||
? '普通卡'
|
||||
: data.card_category === 'industry'
|
||||
? '行业卡'
|
||||
: data.card_category
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
: []
|
||||
|
||||
return [
|
||||
{
|
||||
title: '任务基本信息',
|
||||
@@ -101,69 +210,44 @@
|
||||
{ label: '任务编号', prop: 'task_no', formatter: (value: string) => value || '-' },
|
||||
{
|
||||
label: '任务类型',
|
||||
render: () => {
|
||||
return h(
|
||||
render: () =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: taskType.value === 'device' ? 'warning' : 'primary', size: 'small' },
|
||||
() => (taskType.value === 'device' ? '设备导入' : 'ICCID导入')
|
||||
() =>
|
||||
taskType.value === 'device'
|
||||
? getOperationLabel(deviceTask.value?.operation_type)
|
||||
: 'ICCID导入'
|
||||
)
|
||||
}
|
||||
},
|
||||
...deviceFields,
|
||||
{ label: '批次号', prop: 'batch_no', formatter: (value: string) => value || '-' },
|
||||
{ label: '文件名', prop: 'file_name', formatter: (value: string) => value || '-' },
|
||||
{
|
||||
label: '操作人',
|
||||
prop: 'creator_name',
|
||||
formatter: (value: string) => value || '-'
|
||||
},
|
||||
{ label: '操作人', prop: 'creator_name', formatter: (value: string) => value || '-' },
|
||||
{
|
||||
label: '任务状态',
|
||||
render: (data: TaskDetail) => {
|
||||
return h(ElTag, { type: getStatusType(data.status) }, () => data.status_text)
|
||||
}
|
||||
render: (data: TaskDetail) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: getStatusType(data.status) },
|
||||
() => (data as DeviceImportTaskDetail).status_name || data.status_text || '-'
|
||||
)
|
||||
},
|
||||
...(taskType.value === 'card'
|
||||
? [
|
||||
{
|
||||
label: '运营商',
|
||||
prop: 'carrier_name',
|
||||
formatter: (value: any) => value || '-'
|
||||
},
|
||||
{
|
||||
label: '卡业务类型',
|
||||
render: (data: any) => {
|
||||
if (!data.card_category) return h('span', '-')
|
||||
return h(
|
||||
ElTag,
|
||||
{
|
||||
type: data.card_category === 'industry' ? 'warning' : 'success',
|
||||
size: 'small'
|
||||
},
|
||||
() =>
|
||||
data.card_category === 'normal'
|
||||
? '普通卡'
|
||||
: data.card_category === 'industry'
|
||||
? '行业卡'
|
||||
: data.card_category
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...cardFields,
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'created_at',
|
||||
formatter: (value: Date) => (value ? formatDateTime(value) : '-')
|
||||
formatter: (value: string) => (value ? formatDateTime(value) : '-')
|
||||
},
|
||||
{
|
||||
label: '开始处理时间',
|
||||
prop: 'started_at',
|
||||
formatter: (value: Date) => (value ? formatDateTime(value) : '-')
|
||||
formatter: (value: string | null) => (value ? formatDateTime(value) : '-')
|
||||
},
|
||||
{
|
||||
label: '完成时间',
|
||||
prop: 'completed_at',
|
||||
formatter: (value: Date) => (value ? formatDateTime(value) : '-')
|
||||
formatter: (value: string | null) => (value ? formatDateTime(value) : '-')
|
||||
},
|
||||
...(taskDetail.value?.error_message
|
||||
? [
|
||||
@@ -171,13 +255,12 @@
|
||||
label: '错误信息',
|
||||
prop: 'error_message',
|
||||
fullWidth: true,
|
||||
render: (data: TaskDetail) => {
|
||||
return h(
|
||||
render: (data: TaskDetail) =>
|
||||
h(
|
||||
'span',
|
||||
{ style: { color: 'var(--el-color-danger)' } },
|
||||
data.error_message || ''
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
: [])
|
||||
@@ -189,64 +272,57 @@
|
||||
fields: [
|
||||
{
|
||||
label: '总数',
|
||||
render: (data: TaskDetail) => {
|
||||
return h(
|
||||
render: (data: TaskDetail) =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
style: {
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
color: 'var(--el-color-primary)'
|
||||
}
|
||||
style: { fontSize: '16px', fontWeight: 'bold', color: 'var(--el-color-primary)' }
|
||||
},
|
||||
String(data.total_count || 0)
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '成功数',
|
||||
render: (data: TaskDetail) => {
|
||||
return h(ElTag, { type: 'success' }, () => String(data.success_count || 0))
|
||||
}
|
||||
render: (data: TaskDetail) =>
|
||||
h(ElTag, { type: 'success' }, () => String(data.success_count || 0))
|
||||
},
|
||||
{
|
||||
label: '失败数',
|
||||
render: (data: TaskDetail) => {
|
||||
return h(ElTag, { type: 'danger' }, () => String(data.fail_count || 0))
|
||||
}
|
||||
render: (data: TaskDetail) =>
|
||||
h(ElTag, { type: 'danger' }, () => String(data.fail_count || 0))
|
||||
},
|
||||
{
|
||||
label: '跳过数',
|
||||
render: (data: TaskDetail) => {
|
||||
return h(ElTag, { type: 'warning' }, () => String(data.skip_count || 0))
|
||||
}
|
||||
}
|
||||
render: (data: TaskDetail) =>
|
||||
h(ElTag, { type: 'warning' }, () => String(data.skip_count || 0))
|
||||
},
|
||||
...(taskType.value === 'device'
|
||||
? [
|
||||
{
|
||||
label: '警告数',
|
||||
render: (data: TaskDetail) =>
|
||||
h(ElTag, { type: 'info' }, () =>
|
||||
String((data as DeviceImportTaskDetail).warning_count || 0)
|
||||
)
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 返回列表
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
const goBack = () => router.back()
|
||||
|
||||
// 获取任务详情
|
||||
const getTaskDetail = async () => {
|
||||
const taskId = route.query.id
|
||||
const queryTaskType = route.query.task_type as TaskType | undefined
|
||||
|
||||
if (!taskId) {
|
||||
ElMessage.error('缺少任务ID参数')
|
||||
ElMessage.error('缺少任务 ID 参数')
|
||||
goBack()
|
||||
return
|
||||
}
|
||||
|
||||
// 设置任务类型
|
||||
if (queryTaskType) {
|
||||
taskType.value = queryTaskType
|
||||
}
|
||||
|
||||
if (queryTaskType) taskType.value = queryTaskType
|
||||
if (taskType.value === 'device' && !isPlatformAccount.value) {
|
||||
ElMessage.error('当前账号无权查看设备任务详情')
|
||||
goBack()
|
||||
@@ -256,28 +332,22 @@
|
||||
loading.value = true
|
||||
try {
|
||||
if (taskType.value === 'device') {
|
||||
// 获取设备导入任务详情
|
||||
const res = await DeviceService.getImportTaskDetail(Number(taskId))
|
||||
if (res.code === 0) {
|
||||
taskDetail.value = res.data
|
||||
}
|
||||
if (res.code === 0) taskDetail.value = res.data
|
||||
} else {
|
||||
// 获取ICCID导入任务详情
|
||||
const res = await CardService.getIotCardImportTaskDetail(Number(taskId))
|
||||
if (res.code === 0) {
|
||||
taskDetail.value = res.data
|
||||
}
|
||||
if (res.code === 0) taskDetail.value = res.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.log('获取任务详情失败')
|
||||
console.error('获取任务详情失败:', error)
|
||||
ElMessage.error('获取任务详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTaskDetail()
|
||||
void getTaskDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -285,8 +355,8 @@
|
||||
.task-detail-page {
|
||||
.detail-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.detail-title {
|
||||
@@ -297,8 +367,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.failure-section,
|
||||
.skipped-section {
|
||||
.result-section {
|
||||
margin-top: 20px;
|
||||
|
||||
.section-title {
|
||||
|
||||
Reference in New Issue
Block a user