让凭证数组与套餐作废任务进入可联调状态
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m52s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m52s
Constraint: 后端订单套餐作废接口使用 /api/admin/order-package-invalidate-tasks,上传 purpose 只能使用 attachment。 Rejected: 继续提交逗号拼接凭证字符串 | 新提交路径必须使用 string[],历史字符串只做读取归一化。 Confidence: high Scope-risk: broad Directive: 新增订单套餐作废权限时需同时配置菜单 URL 与 order_package_invalidate_task:create/detail 按钮权限。 Tested: bun run build;development 真实后端联调上传、创建、列表、详情通过;复机接口未触发。 Not-tested: 未逐一手工覆盖所有历史凭证数据、全部角色权限组合和所有文件扩展名预览
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
style="width: 100%"
|
||||
>
|
||||
<template v-if="!props.seriesId">
|
||||
<ElOption :value="null" disabled>
|
||||
<ElOption value="__NO_PACKAGE__" disabled>
|
||||
<span style="color: var(--el-text-color-warning)">
|
||||
该设备未关联套餐系列,无法购买套餐
|
||||
</span>
|
||||
@@ -125,6 +125,7 @@
|
||||
import type { CreateOrderRequest, PackageResponse } from '@/types/api'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -154,7 +155,7 @@
|
||||
const form = reactive({
|
||||
package_id: undefined as number | undefined,
|
||||
payment_method: 'wallet' as 'wallet' | 'offline',
|
||||
payment_voucher_key: undefined as string | undefined
|
||||
payment_voucher_key: [] as string[]
|
||||
})
|
||||
|
||||
const rules = computed<FormRules>(() => {
|
||||
@@ -317,7 +318,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
if (form.payment_method === 'offline' && !form.payment_voucher_key) {
|
||||
if (form.payment_method === 'offline' && !hasVoucherKeys(form.payment_voucher_key)) {
|
||||
ElMessage.error('线下支付必须上传支付凭证')
|
||||
return
|
||||
}
|
||||
@@ -331,8 +332,8 @@
|
||||
payment_method: form.payment_method
|
||||
}
|
||||
|
||||
if (form.payment_method === 'offline' && form.payment_voucher_key) {
|
||||
data.payment_voucher_key = form.payment_voucher_key
|
||||
if (form.payment_method === 'offline' && hasVoucherKeys(form.payment_voucher_key)) {
|
||||
data.payment_voucher_key = toVoucherKeyList(form.payment_voucher_key)
|
||||
}
|
||||
|
||||
await OrderService.createOrder(data)
|
||||
@@ -360,7 +361,7 @@
|
||||
formRef.value?.resetFields()
|
||||
form.package_id = undefined
|
||||
form.payment_method = 'wallet'
|
||||
form.payment_voucher_key = undefined
|
||||
form.payment_voucher_key = []
|
||||
voucherUploading.value = false
|
||||
packageOptions.value = []
|
||||
packagesEmptyWithSeriesId.value = false
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
import { useAssetOperations } from './composables/useAssetOperations'
|
||||
|
||||
// 引入类型
|
||||
import type { AssetPackageUsageRecord } from '@/types/api'
|
||||
import type { BindingCard, PackageInfo, SwitchCardForm, WiFiForm } from './types'
|
||||
|
||||
defineOptions({ name: 'SingleCard' })
|
||||
@@ -613,10 +614,10 @@
|
||||
/**
|
||||
* 显示流量详单
|
||||
*/
|
||||
const handleShowDailyRecords = (payload: { packageRow: PackageInfo }) => {
|
||||
const handleShowDailyRecords = (payload: { packageRow: AssetPackageUsageRecord }) => {
|
||||
const { packageRow } = payload
|
||||
selectedPackageUsageId.value = packageRow.package_usage_id ?? packageRow.id
|
||||
selectedPackageRow.value = packageRow
|
||||
selectedPackageUsageId.value = packageRow.package_usage_id ?? packageRow.id ?? null
|
||||
selectedPackageRow.value = packageRow as PackageInfo
|
||||
dailyRecordsDialogVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
import { ExchangeService } from '@/api/modules'
|
||||
import type { ExchangeResponse } from '@/api/modules/exchange'
|
||||
import { ElTag, ElCard, ElButton, ElIcon } from 'element-plus'
|
||||
import type { TagProps } from 'element-plus'
|
||||
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
@@ -44,8 +45,8 @@
|
||||
const exchangeDetail = ref<ExchangeResponse | null>(null)
|
||||
const exchangeId = ref<number>(0)
|
||||
|
||||
const getStatusType = (status: number) => {
|
||||
const types: Record<number, string> = {
|
||||
const getStatusType = (status: number): TagProps['type'] => {
|
||||
const types: Record<number, TagProps['type']> = {
|
||||
1: 'warning',
|
||||
2: 'info',
|
||||
3: 'primary',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
:label-width="90"
|
||||
label-width="90px"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useTableContextMenu } from '@/composables/useTableContextMenu'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import type { AssetAllocationRecord, AllocationTypeEnum, AssetTypeEnum } from '@/types/api/card'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
@@ -347,7 +346,7 @@
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
const params: Record<string, any> = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
...formFilters
|
||||
|
||||
@@ -370,6 +370,13 @@
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'creator_name',
|
||||
label: '操作人',
|
||||
width: 140,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: DeviceImportTask) => row.creator_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
|
||||
@@ -500,6 +500,13 @@
|
||||
minWidth: 250,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'creator_name',
|
||||
label: '操作人',
|
||||
width: 140,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: IotCardImportTask) => row.creator_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="order-package-invalidate-task-detail-page">
|
||||
<ElCard shadow="never">
|
||||
<div class="detail-header">
|
||||
<ElButton @click="router.back()">返回</ElButton>
|
||||
<h2 class="detail-title">订单套餐作废任务详情</h2>
|
||||
</div>
|
||||
|
||||
<DetailPage v-if="taskDetail" :sections="detailSections" :data="taskDetail" />
|
||||
|
||||
<ElTable
|
||||
v-if="taskDetail?.failed_items?.length"
|
||||
:data="taskDetail.failed_items"
|
||||
border
|
||||
style="width: 100%; margin-top: 16px"
|
||||
>
|
||||
<ElTableColumn prop="line" label="行号" width="100" />
|
||||
<ElTableColumn prop="order_no" label="订单号" min-width="180" show-overflow-tooltip />
|
||||
<ElTableColumn prop="reason" label="失败原因" min-width="240" show-overflow-tooltip />
|
||||
</ElTable>
|
||||
<ElEmpty v-else-if="taskDetail" description="暂无失败明细" />
|
||||
</ElCard>
|
||||
|
||||
<PaymentVoucherDialog :file-keys="voucherPreviewKeys" @close="voucherPreviewKeys = []" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElButton, ElCard, ElEmpty, ElMessage, ElTable, ElTableColumn, ElTag } from 'element-plus'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import { OrderPackageInvalidateTaskService } from '@/api/modules'
|
||||
import type {
|
||||
OrderPackageInvalidateTaskDetail,
|
||||
OrderPackageInvalidateTaskStatus
|
||||
} from '@/types/api'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
|
||||
defineOptions({ name: 'OrderPackageInvalidateTaskDetail' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const taskDetail = ref<OrderPackageInvalidateTaskDetail | null>(null)
|
||||
const voucherPreviewKeys = ref<string[]>([])
|
||||
|
||||
const getStatusType = (status: OrderPackageInvalidateTaskStatus) => {
|
||||
const statusMap: Record<number, 'info' | 'warning' | 'success' | 'danger'> = {
|
||||
1: 'info',
|
||||
2: 'warning',
|
||||
3: 'success',
|
||||
4: 'danger'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
const getStatusName = (task: OrderPackageInvalidateTaskDetail) =>
|
||||
task.status_text || task.status_name || '-'
|
||||
|
||||
const detailSections = computed<DetailSection[]>(() => [
|
||||
{
|
||||
title: '任务信息',
|
||||
columns: 2,
|
||||
fields: [
|
||||
{ label: '任务编号', prop: 'task_no' },
|
||||
{
|
||||
label: '任务状态',
|
||||
render: (data: OrderPackageInvalidateTaskDetail) =>
|
||||
h(ElTag, { type: getStatusType(data.status) }, () => getStatusName(data))
|
||||
},
|
||||
{ label: '文件名', prop: 'file_name', formatter: (value: string) => value || '-' },
|
||||
{ label: '操作人', prop: 'creator_name', formatter: (value: string) => value || '-' },
|
||||
{
|
||||
label: '凭证附件',
|
||||
render: (data: OrderPackageInvalidateTaskDetail) =>
|
||||
hasVoucherKeys(data.voucher_keys)
|
||||
? h(
|
||||
ElButton,
|
||||
{
|
||||
link: true,
|
||||
type: 'primary',
|
||||
onClick: () => (voucherPreviewKeys.value = toVoucherKeyList(data.voucher_keys))
|
||||
},
|
||||
() => '查看凭证'
|
||||
)
|
||||
: h('span', '-')
|
||||
},
|
||||
{
|
||||
label: '运营备注',
|
||||
prop: 'remark',
|
||||
formatter: (value: string) => value || '-',
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '错误信息',
|
||||
prop: 'error_message',
|
||||
formatter: (value: string) => value || '-',
|
||||
fullWidth: true
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'created_at',
|
||||
formatter: (value: string) => formatDateTime(value)
|
||||
},
|
||||
{
|
||||
label: '开始时间',
|
||||
prop: 'started_at',
|
||||
formatter: (value: string) => (value ? formatDateTime(value) : '-')
|
||||
},
|
||||
{
|
||||
label: '完成时间',
|
||||
prop: 'completed_at',
|
||||
formatter: (value: string) => (value ? formatDateTime(value) : '-')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '统计信息',
|
||||
columns: 2,
|
||||
fields: [
|
||||
{ label: '总数', prop: 'total_count' },
|
||||
{ label: '成功数', prop: 'success_count' },
|
||||
{ label: '失败数', prop: 'fail_count' },
|
||||
{ label: '跳过数', prop: 'skip_count', formatter: (value: number) => String(value ?? 0) }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
const getTaskDetail = async () => {
|
||||
const taskId = Number(route.query.id)
|
||||
if (!taskId) {
|
||||
ElMessage.error('任务ID无效')
|
||||
return
|
||||
}
|
||||
const res = await OrderPackageInvalidateTaskService.getTaskDetail(taskId)
|
||||
if (res.code === 0) {
|
||||
taskDetail.value = res.data
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTaskDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.order-package-invalidate-task-detail-page {
|
||||
padding: 20px;
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,476 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="order-package-invalidate-task-page" id="table-full-screen">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:icon="Upload"
|
||||
@click="createDialogVisible = true"
|
||||
v-permission="'order_package_invalidate_task:create'"
|
||||
>
|
||||
新建作废任务
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="taskList"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="180"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<ElDialog
|
||||
v-model="createDialogVisible"
|
||||
title="新建订单套餐作废任务"
|
||||
width="560px"
|
||||
@closed="handleCreateDialogClosed"
|
||||
>
|
||||
<ElForm ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
|
||||
<ElFormItem label="CSV文件" prop="file">
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".csv,text/csv"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleFileRemove"
|
||||
>
|
||||
<ElIcon class="el-icon--upload"><UploadFilled /></ElIcon>
|
||||
<div class="el-upload__text">将 CSV 文件拖到此处,或<em>点击选择</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">仅支持 .csv 文件,必填列:order_no</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="凭证附件">
|
||||
<VoucherUpload
|
||||
ref="voucherUploadRef"
|
||||
v-model="createForm.voucher_keys"
|
||||
voucher-name="凭证附件"
|
||||
@uploading-change="voucherUploading = $event"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="运营备注" prop="remark">
|
||||
<ElInput
|
||||
v-model="createForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入运营备注(可选)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="createDialogVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="creating || voucherUploading"
|
||||
:disabled="voucherUploading"
|
||||
@click="handleCreateTask"
|
||||
>
|
||||
{{ voucherUploading ? '凭证上传中...' : '确认创建' }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<PaymentVoucherDialog :file-keys="voucherPreviewKeys" @close="voucherPreviewKeys = []" />
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
ElButton,
|
||||
ElCard,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElTag,
|
||||
ElUpload
|
||||
} from 'element-plus'
|
||||
import { Upload, UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules, UploadFile, UploadInstance } from 'element-plus'
|
||||
import { OrderPackageInvalidateTaskService, StorageService } from '@/api/modules'
|
||||
import type {
|
||||
CreateOrderPackageInvalidateTaskRequest,
|
||||
OrderPackageInvalidateTask,
|
||||
OrderPackageInvalidateTaskQueryParams,
|
||||
OrderPackageInvalidateTaskStatus
|
||||
} from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
import { getErrorMessage, hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
|
||||
defineOptions({ name: 'OrderPackageInvalidateTask' })
|
||||
|
||||
const router = useRouter()
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
const loading = ref(false)
|
||||
const creating = ref(false)
|
||||
const voucherUploading = ref(false)
|
||||
const tableRef = ref()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const voucherUploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const createFormRef = ref<FormInstance>()
|
||||
const createDialogVisible = ref(false)
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const taskList = ref<OrderPackageInvalidateTask[]>([])
|
||||
const voucherPreviewKeys = ref<string[]>([])
|
||||
|
||||
const searchForm = reactive<OrderPackageInvalidateTaskQueryParams>({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const createForm = reactive({
|
||||
file: '',
|
||||
voucher_keys: [] as string[],
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const createRules = reactive<FormRules>({
|
||||
file: [{ required: true, message: '请选择 CSV 文件', trigger: 'change' }],
|
||||
remark: [{ max: 1000, message: '运营备注最多1000字', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '任务状态',
|
||||
prop: 'status',
|
||||
type: 'select',
|
||||
config: { clearable: true, placeholder: '全部' },
|
||||
options: [
|
||||
{ label: '待处理', value: 1 },
|
||||
{ label: '处理中', value: 2 },
|
||||
{ label: '已完成', value: 3 },
|
||||
{ label: '失败', value: 4 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '任务编号', prop: 'task_no' },
|
||||
{ label: '任务状态', prop: 'status' },
|
||||
{ label: '总数', prop: 'total_count' },
|
||||
{ label: '成功数', prop: 'success_count' },
|
||||
{ label: '失败数', prop: 'fail_count' },
|
||||
{ label: '跳过数', prop: 'skip_count' },
|
||||
{ label: '文件名', prop: 'file_name' },
|
||||
{ label: '凭证', prop: 'voucher_keys' },
|
||||
{ label: '运营备注', prop: 'remark' },
|
||||
{ label: '操作人', prop: 'creator_name' },
|
||||
{ label: '错误信息', prop: 'error_message' },
|
||||
{ label: '创建时间', prop: 'created_at' },
|
||||
{ label: '开始时间', prop: 'started_at' },
|
||||
{ label: '完成时间', prop: 'completed_at' }
|
||||
]
|
||||
|
||||
const getStatusType = (status: OrderPackageInvalidateTaskStatus) => {
|
||||
const statusMap: Record<number, 'info' | 'warning' | 'success' | 'danger'> = {
|
||||
1: 'info',
|
||||
2: 'warning',
|
||||
3: 'success',
|
||||
4: 'danger'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
const getStatusName = (task: OrderPackageInvalidateTask) =>
|
||||
task.status_text || task.status_name || '-'
|
||||
|
||||
const handleNameClick = (row: OrderPackageInvalidateTask) => {
|
||||
if (!hasAuth('order_package_invalidate_task:detail')) {
|
||||
ElMessage.warning('您没有查看详情的权限')
|
||||
return
|
||||
}
|
||||
router.push({
|
||||
path: RoutesAlias.OrderPackageInvalidateTaskDetail,
|
||||
query: { id: row.id }
|
||||
})
|
||||
}
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'task_no',
|
||||
label: '任务编号',
|
||||
width: 220,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: OrderPackageInvalidateTask) =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
style: 'color: var(--el-color-primary); cursor: pointer; text-decoration: underline;',
|
||||
onClick: (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
handleNameClick(row)
|
||||
}
|
||||
},
|
||||
row.task_no
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '任务状态',
|
||||
width: 100,
|
||||
formatter: (row: OrderPackageInvalidateTask) =>
|
||||
h(ElTag, { type: getStatusType(row.status) }, () => getStatusName(row))
|
||||
},
|
||||
{ prop: 'total_count', label: '总数', width: 80 },
|
||||
{
|
||||
prop: 'success_count',
|
||||
label: '成功数',
|
||||
width: 80,
|
||||
formatter: (row: OrderPackageInvalidateTask) =>
|
||||
h('span', { style: { color: 'var(--el-color-success)' } }, row.success_count)
|
||||
},
|
||||
{
|
||||
prop: 'fail_count',
|
||||
label: '失败数',
|
||||
width: 80,
|
||||
formatter: (row: OrderPackageInvalidateTask) =>
|
||||
h('span', { style: { color: 'var(--el-color-danger)' } }, row.fail_count)
|
||||
},
|
||||
{
|
||||
prop: 'skip_count',
|
||||
label: '跳过数',
|
||||
width: 80,
|
||||
formatter: (row: OrderPackageInvalidateTask) => row.skip_count ?? 0
|
||||
},
|
||||
{ prop: 'file_name', label: '文件名', minWidth: 180, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'voucher_keys',
|
||||
label: '凭证',
|
||||
width: 100,
|
||||
formatter: (row: OrderPackageInvalidateTask) =>
|
||||
hasVoucherKeys(row.voucher_keys)
|
||||
? h(
|
||||
ElButton,
|
||||
{
|
||||
link: true,
|
||||
type: 'primary',
|
||||
onClick: () => (voucherPreviewKeys.value = toVoucherKeyList(row.voucher_keys))
|
||||
},
|
||||
() => '查看'
|
||||
)
|
||||
: '-'
|
||||
},
|
||||
{
|
||||
prop: 'remark',
|
||||
label: '运营备注',
|
||||
minWidth: 180,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: OrderPackageInvalidateTask) => row.remark || '-'
|
||||
},
|
||||
{
|
||||
prop: 'creator_name',
|
||||
label: '操作人',
|
||||
width: 140,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: OrderPackageInvalidateTask) => row.creator_name || '-'
|
||||
},
|
||||
{
|
||||
prop: 'error_message',
|
||||
label: '错误信息',
|
||||
minWidth: 200,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: OrderPackageInvalidateTask) => row.error_message || '-'
|
||||
},
|
||||
{
|
||||
prop: 'created_at',
|
||||
label: '创建时间',
|
||||
width: 180,
|
||||
formatter: (row: OrderPackageInvalidateTask) => formatDateTime(row.created_at)
|
||||
},
|
||||
{
|
||||
prop: 'started_at',
|
||||
label: '开始时间',
|
||||
width: 180,
|
||||
formatter: (row: OrderPackageInvalidateTask) =>
|
||||
row.started_at ? formatDateTime(row.started_at) : '-'
|
||||
},
|
||||
{
|
||||
prop: 'completed_at',
|
||||
label: '完成时间',
|
||||
width: 180,
|
||||
formatter: (row: OrderPackageInvalidateTask) =>
|
||||
row.completed_at ? formatDateTime(row.completed_at) : '-'
|
||||
}
|
||||
])
|
||||
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await OrderPackageInvalidateTaskService.getTasks({
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
status: searchForm.status
|
||||
})
|
||||
if (res.code === 0) {
|
||||
taskList.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载订单套餐作废任务失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.status = undefined
|
||||
pagination.page = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleRefresh = () => getTableData()
|
||||
const handleSizeChange = (pageSize: number) => {
|
||||
pagination.page_size = pageSize
|
||||
getTableData()
|
||||
}
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleFileChange = (uploadFile: UploadFile) => {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
if (!/\.csv$/i.test(file.name)) {
|
||||
ElMessage.error('仅支持 CSV 文件')
|
||||
uploadRef.value?.clearFiles()
|
||||
selectedFile.value = null
|
||||
createForm.file = ''
|
||||
return
|
||||
}
|
||||
selectedFile.value = file
|
||||
createForm.file = file.name
|
||||
createFormRef.value?.validateField('file')
|
||||
}
|
||||
|
||||
const handleFileRemove = () => {
|
||||
selectedFile.value = null
|
||||
createForm.file = ''
|
||||
createFormRef.value?.validateField('file')
|
||||
}
|
||||
|
||||
const uploadCsvFile = async (file: File) => {
|
||||
const contentType = file.type || 'text/csv'
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: contentType,
|
||||
purpose: 'attachment'
|
||||
})
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
throw new Error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
}
|
||||
|
||||
await StorageService.uploadFile(uploadUrlRes.data.upload_url, file, contentType)
|
||||
return uploadUrlRes.data.file_key
|
||||
}
|
||||
|
||||
const handleCreateTask = async () => {
|
||||
if (!createFormRef.value) return
|
||||
if (voucherUploading.value) {
|
||||
ElMessage.warning('凭证上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
const valid = await createFormRef.value.validate().catch(() => false)
|
||||
if (!valid || !selectedFile.value) return
|
||||
|
||||
creating.value = true
|
||||
try {
|
||||
const fileKey = await uploadCsvFile(selectedFile.value)
|
||||
const data: CreateOrderPackageInvalidateTaskRequest = {
|
||||
file_key: fileKey,
|
||||
file_name: selectedFile.value.name,
|
||||
voucher_keys: toVoucherKeyList(createForm.voucher_keys),
|
||||
remark: createForm.remark || undefined
|
||||
}
|
||||
await OrderPackageInvalidateTaskService.createTask(data)
|
||||
ElMessage.success('任务创建成功')
|
||||
createDialogVisible.value = false
|
||||
await getTableData()
|
||||
} catch (error) {
|
||||
console.error('创建订单套餐作废任务失败:', error)
|
||||
ElMessage.error(getErrorMessage(error, '任务创建失败'))
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateDialogClosed = () => {
|
||||
createFormRef.value?.resetFields()
|
||||
uploadRef.value?.clearFiles()
|
||||
voucherUploadRef.value?.clearFiles(false)
|
||||
selectedFile.value = null
|
||||
createForm.file = ''
|
||||
createForm.voucher_keys = []
|
||||
createForm.remark = ''
|
||||
voucherUploading.value = false
|
||||
}
|
||||
|
||||
const getActions = (row: OrderPackageInvalidateTask) => {
|
||||
const actions: any[] = []
|
||||
if (hasAuth('order_package_invalidate_task:detail')) {
|
||||
actions.push({ label: '详情', handler: () => handleNameClick(row), type: 'primary' })
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
</script>
|
||||
@@ -21,7 +21,7 @@
|
||||
<ElDivider content-position="left">
|
||||
<span class="section-title">失败记录 ({{ taskDetail.fail_count }})</span>
|
||||
</ElDivider>
|
||||
<ElTable :data="taskDetail.failed_items" border style="width: 100%">
|
||||
<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" />
|
||||
@@ -34,7 +34,7 @@
|
||||
<ElDivider content-position="left">
|
||||
<span class="section-title">跳过记录 ({{ taskDetail.skip_count }})</span>
|
||||
</ElDivider>
|
||||
<ElTable :data="taskDetail.skipped_items" border style="width: 100%">
|
||||
<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" />
|
||||
@@ -52,9 +52,10 @@
|
||||
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, IotCardImportTaskStatus } from '@/types/api/card'
|
||||
import type { IotCardImportTaskDetail } from '@/types/api/card'
|
||||
import type { DeviceImportTaskDetail } from '@/types/api/device'
|
||||
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
||||
import DetailPage from '@/components/common/DetailPage.vue'
|
||||
@@ -72,7 +73,7 @@
|
||||
const taskType = ref<TaskType>('card')
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status?: IotCardImportTaskStatus) => {
|
||||
const getStatusType = (status?: number): TagProps['type'] => {
|
||||
if (!status) return 'info'
|
||||
switch (status) {
|
||||
case 1:
|
||||
@@ -107,6 +108,11 @@
|
||||
},
|
||||
{ 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: '任务状态',
|
||||
render: (data: TaskDetail) => {
|
||||
|
||||
Reference in New Issue
Block a user